diff --git a/requirements.txt b/requirements.txt index 7ffcd58..8036d51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ html5lib==1.1 idna==3.6 Jinja2==3.1.3 lxml==5.1.0 +Markdown==3.5.2 MarkupSafe==2.1.5 oscrypto==1.3.0 pillow==10.2.0 diff --git a/src/invoice_generator/html_generator.py b/src/invoice_generator/html_generator.py index 5dee2fa..7f15a65 100644 --- a/src/invoice_generator/html_generator.py +++ b/src/invoice_generator/html_generator.py @@ -2,6 +2,9 @@ from pathlib import Path from jinja2 import FileSystemLoader, TemplateNotFound, Environment from xhtml2pdf import pisa +from datetime import date, timedelta + +import markdown class HtmlTemplate: @@ -15,12 +18,42 @@ class HtmlTemplate: try: loader = FileSystemLoader(self.templates) env = Environment(loader=loader) + env.globals['format_float'] = self.format_float + env.globals['calculate_total'] = self.calculate_total + + env.filters['markdown_to_html'] = self.markdown_to_html + env.filters['named_replace'] = self.named_replace + env.filters['add_days'] = self.add_days template = env.get_template(self.template_name) return template.render(invoice=invoice_data, envelope=envelope_data) except TemplateNotFound: raise FileNotFoundError(f'Could not find template file: {self.template_name}') + @staticmethod + def calculate_total(invoice_data): + return sum( + (pos['Quantity'] * (pos['PricePerUnit'] or invoice_data.PricePerUnit)) for pos in invoice_data.Positions) + + @staticmethod + def named_replace(value, **replacements): + for key, replacement in replacements.items(): + value = value.replace(f"%%{key}%%", str(replacement)) + return value + + @staticmethod + def format_float(value): + return f'{value:,.2f}' + + @staticmethod + def add_days(date, number_of_days): + return date + timedelta(days=number_of_days) + + @staticmethod + def markdown_to_html(md_content): + html_content = markdown.markdown(md_content) + return html_content + @staticmethod def convert_html_to_pdf(source_html, output_filename): # open output file for writing (truncated binary) diff --git a/src/main.py b/src/main.py index 6154887..e0a8d2a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,9 @@ import argparse import yaml +import locale + from pathlib import Path +from datetime import datetime from invoice_generator import html_generator @@ -26,10 +29,24 @@ def check_file(file_path): return None +def parse_date(date_string): + return datetime.strptime(date_string, '%Y-%m-%d').date() + + +def merge_envelope_data_into_invoice_data(invoice_data, source_data): + if (source_data is None) or (invoice_data is None): + return + for key, value in source_data.items(): + if not hasattr(invoice_data, key): + setattr(invoice_data, key, value) + + # Utility function def main(): + locale.setlocale(locale.LC_ALL, 'de_DE.utf8') + print('Simple invoice generator for freelancers and small businesses by Torsten Ueberschar') print() parser = argparse.ArgumentParser(description='Read invoice and envelope data from yaml file') @@ -73,6 +90,16 @@ def main(): print('Invoice data:') invoice_data = DataObject(**invoice) + merge_envelope_data_into_invoice_data(invoice_data, envelope_data.Invoice) + + selected_customer = next((x for x in envelope_data.Customers if x['CustomerId'] == invoice_data.CustomerId), None) + merge_envelope_data_into_invoice_data(invoice_data, selected_customer) + + if not hasattr(invoice_data, 'InvoiceDate'): + invoice_data.InvoiceDate = datetime.now().date() + else: + invoice_data.InvoiceDate = parse_date(invoice_data.InvoiceDate) + if not hasattr(invoice_data, 'Id'): invoice_data.Id = None diff --git a/test_data/RG004711.yaml b/test_data/RG004711.yaml index 39f2955..ef54ad0 100644 --- a/test_data/RG004711.yaml +++ b/test_data/RG004711.yaml @@ -3,20 +3,20 @@ CustomerId: KD01234 #InvoiceDate: 2023-12-23 Positions: - - Title: "Zuckerwatte fressen" + - Title: "Zuckerwatte fressen Ganz besonders langer Text" SubTitle: "Leistungszeitraum: 11/2022" - PricePerUnit: - Quantity: 123.45 - + PricePerUnit: 100 + Quantity: 100 + - Title: "Aschlecken" SubTitle: "Leistungszeitraum: 11/2022" PricePerUnit: 99.99 Quantity: 12 - + - Title: "Aschkriechen" SubTitle: "Leistungszeitraum: 10/2022" PricePerUnit: 77.88 Quantity: 3 - + diff --git a/test_data/envelope.yaml b/test_data/envelope.yaml index 41450d4..05d7283 100644 --- a/test_data/envelope.yaml +++ b/test_data/envelope.yaml @@ -4,47 +4,49 @@ Config: AddressContent: LogoFile: Images/logo.svg - AddressBoxSender: Torsten Ueberschar - Pfarrweg 1 - 57439 Attendorn + AddressBoxSender: "Abs.: Torsten Ueberschar - Pfarrweg 1 - 57439 Attendorn" Contents: - - Head: - Text: | - Torsten Ueberschar - Pfarrweg 1 + - Text: | +  + Torsten Ueberschar + Pfarrweg 1 57439 Attendorn - - Head: Kontakt - Text: | - tu@uesome.de - +49 2734 4239271 - - Head: Steuern - Text: | - St.-Nr.: 02/178/51224 - USt-IdNr.: DE313460724 - - Head: Bankverbindung - Text: | - Torsten Ueberschar - DE67 1001 1001 2626 8627 86 - NTSBDEB1XXX - N26 Bank GmbH + - Text: | + **Kontakt** + tu@uesome.de + +49 2734 4239271 + - Text: | + **Steuern** + USt-IdNr.: DE313460724 + - Text: | + **Bankverbindung** + Torsten Ueberschar + DE67 1001 1001 2626 8627 86 + NTSBDEB1XXX + N26 Bank GmbH Invoice: Vat: 19.0 - Salutation: Sehr geehrte Damen und Herren, - - Introduction: > + Introduction: | + ### Rechnung + + *Sehr geehrte Damen und Herren*, + für folgende in Ihrem Auftrag ausgeführten Leistungen erlaube ich mir zu berechnen - Footer: > - Bitte überweisen Sie den Rechnungsbetrag unter Angabe der Rechnungsnummer auf mein Konto bis zum {ZahlungsZiel}. - - + Footer: | + Bitte überweisen Sie den Rechnungsbetrag unter Angabe der Rechnungsnummer auf mein Konto bis zum %%ZahlungsZiel%%. + Mit freundlichen Grüßen + *Torsten Uebeschar* + Customers: - - Id: KD01234 + - CustomerId: KD01234 PricePerUnit: 88.88 DueDate: 30 AddressField: | - Klaus Peter Klausen - Am Klausenhof 1 + Klaus Peter Klausen + Am Klausenhof 1 04711 Klausenhausen diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Black.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Black.ttf new file mode 100644 index 0000000..55eebbc Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Black.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-BlackItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-BlackItalic.ttf new file mode 100644 index 0000000..89b820f Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-BlackItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf new file mode 100644 index 0000000..217ef02 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf new file mode 100644 index 0000000..031ee3a Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-ExtraBold.ttf b/test_data/templates/fonts/BarlowSemiCondensed-ExtraBold.ttf new file mode 100644 index 0000000..530a811 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-ExtraBold.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-ExtraBoldItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-ExtraBoldItalic.ttf new file mode 100644 index 0000000..64dccf8 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-ExtraBoldItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-ExtraLight.ttf b/test_data/templates/fonts/BarlowSemiCondensed-ExtraLight.ttf new file mode 100644 index 0000000..f29fff6 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-ExtraLight.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-ExtraLightItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-ExtraLightItalic.ttf new file mode 100644 index 0000000..585d68e Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-ExtraLightItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Italic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Italic.ttf new file mode 100644 index 0000000..232d54b Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Italic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Light.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Light.ttf new file mode 100644 index 0000000..8ddb9a3 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Light.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf new file mode 100644 index 0000000..c9a9929 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Medium.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Medium.ttf new file mode 100644 index 0000000..6b1921b Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Medium.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-MediumItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-MediumItalic.ttf new file mode 100644 index 0000000..00c9ced Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-MediumItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf new file mode 100644 index 0000000..71ff2d9 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-SemiBold.ttf b/test_data/templates/fonts/BarlowSemiCondensed-SemiBold.ttf new file mode 100644 index 0000000..7d737f2 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-SemiBold.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-SemiBoldItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-SemiBoldItalic.ttf new file mode 100644 index 0000000..631e180 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-SemiBoldItalic.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-Thin.ttf b/test_data/templates/fonts/BarlowSemiCondensed-Thin.ttf new file mode 100644 index 0000000..4d837a6 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-Thin.ttf differ diff --git a/test_data/templates/fonts/BarlowSemiCondensed-ThinItalic.ttf b/test_data/templates/fonts/BarlowSemiCondensed-ThinItalic.ttf new file mode 100644 index 0000000..d9d5060 Binary files /dev/null and b/test_data/templates/fonts/BarlowSemiCondensed-ThinItalic.ttf differ diff --git a/test_data/templates/images/logo.svg b/test_data/templates/images/logo.svg new file mode 100644 index 0000000..27e635b --- /dev/null +++ b/test_data/templates/images/logo.svg @@ -0,0 +1,83 @@ + + + diff --git a/test_data/templates/invoice.html b/test_data/templates/invoice.html index 2302a1a..f5a91ba 100644 --- a/test_data/templates/invoice.html +++ b/test_data/templates/invoice.html @@ -1,11 +1,44 @@
Absender
- Anschrift - in - mehreren - Zeilen +{{ envelope.AddressContent.AddressBoxSender }}
+ + {{ invoice.AddressField | markdown_to_html }}{{ address.Head }}
- {% endif %} -{{ address.Text | replace('\n', '
') }}
{{ address.Text | markdown_to_html }}
{% endfor %}| Rechnungsnummer: | Kundennummer: | Rechnungsdatum: | @@ -76,48 +153,58 @@|
| {{ invoice.Id }} | {{ invoice.CustomerId }} | -{{ invoice.InvoiceDate }} | +{{ invoice.InvoiceDate.strftime('%d. %B %Y') }} |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum.
+{{ invoice.Introduction | markdown_to_html }}
| Aktivität | +Anzahl | +Einheit | +Betrag | +
|---|---|---|---|
| {{ position.Title }} {{ position.SubTitle }} |
+ {{ format_float(position.Quantity | float) }} | +{{ format_float((position.PricePerUnit or invoice.PricePerUnit) | float) }} + | +{{ format_float(position.Quantity * ( position.PricePerUnit or + invoice.PricePerUnit ))}} + | +
| + | + | Nettosumme: | +{{ format_float(calculate_total(invoice)) }} | +
| + | + | USt. ({{ format_float(invoice.Vat) }}%): | +{{ format_float(calculate_total(invoice) * ((invoice.Vat / 100))) }} | +
| + | + | Gesamt Summe: | +{{ format_float(calculate_total(invoice) * ((invoice.Vat / 100)+1)) }} | +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum.
-{{ invoice.Footer | markdown_to_html | named_replace(ZahlungsZiel=(invoice.InvoiceDate | + add_days(invoice.DueDate)).strftime('%d. %B %Y')) }}