From 2c2066450c7ac30fb73952701bb7dced86cbb141 Mon Sep 17 00:00:00 2001 From: Torsten Ueberschar Date: Wed, 7 Feb 2024 17:05:53 +0100 Subject: [PATCH] first try --- .gitignore | 164 +----------------- .idea/freelance_invoice.iml | 10 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 132 ++++++++++++++ requirements.txt | 35 ++++ setup.py | 30 ++++ src/invoice_generator/__init__.py | 0 src/invoice_generator/html_generator.py | 38 ++++ src/main.py | 104 +++++++++++ test_data/RG004711.yaml | 21 +++ test_data/envelope.yaml | 50 ++++++ test_data/templates/invoice.html | 107 ++++++++++++ 15 files changed, 556 insertions(+), 162 deletions(-) create mode 100644 .idea/freelance_invoice.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 src/invoice_generator/__init__.py create mode 100644 src/invoice_generator/html_generator.py create mode 100644 src/main.py create mode 100644 test_data/RG004711.yaml create mode 100644 test_data/envelope.yaml create mode 100644 test_data/templates/invoice.html diff --git a/.gitignore b/.gitignore index 5d381cc..f25f1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,162 +1,2 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - +.idea/ +.code/ \ No newline at end of file diff --git a/.idea/freelance_invoice.iml b/.idea/freelance_invoice.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/freelance_invoice.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5fee4e1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0bfdaa1 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..af53adb --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1707294915620 + + + + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ffcd58 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,35 @@ +arabic-reshaper==3.0.0 +asn1crypto==1.5.1 +certifi==2024.2.2 +cffi==1.16.0 +chardet==5.2.0 +charset-normalizer==3.3.2 +click==8.1.7 +cryptography==42.0.2 +cssselect2==0.7.0 +html5lib==1.1 +idna==3.6 +Jinja2==3.1.3 +lxml==5.1.0 +MarkupSafe==2.1.5 +oscrypto==1.3.0 +pillow==10.2.0 +pycparser==2.21 +pyHanko==0.21.0 +pyhanko-certvalidator==0.26.3 +pypdf==4.0.1 +pypng==0.20220715.0 +python-bidi==0.4.2 +PyYAML==6.0.1 +qrcode==7.4.2 +reportlab==4.0.9 +requests==2.31.0 +six==1.16.0 +svglib==1.5.1 +tinycss2==1.2.1 +typing_extensions==4.9.0 +tzlocal==5.2 +uritools==4.0.2 +urllib3==2.2.0 +webencodings==0.5.1 +xhtml2pdf==0.2.14 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..506c257 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, find_packages + +setup( + name='simple-invoice-generator', + version='0.1.0', + description='Simple invoice generator for freelancers and small businesses', + long_description=open('README.md').read(), + long_description_content_type='text/markdown', + author='Torsten Ueberschar', + author_email='tu@uesome.de', + url='https://git.uesome.de/torsten/simple-invoice-generator', + packages=find_packages('src'), + package_dir={'': 'src'}, + install_requires=[ + 'PyYAML', + 'Jinja2', + 'xhtml2pdf', + ], + extras_require={ + 'dev': ['pytest', 'coverage'], + # Extra dependencies for development and testing + }, + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers, Freelancers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.10', + # Add more classifiers as needed + ], +) diff --git a/src/invoice_generator/__init__.py b/src/invoice_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/invoice_generator/html_generator.py b/src/invoice_generator/html_generator.py new file mode 100644 index 0000000..5dee2fa --- /dev/null +++ b/src/invoice_generator/html_generator.py @@ -0,0 +1,38 @@ +from pathlib import Path +from jinja2 import FileSystemLoader, TemplateNotFound, Environment + +from xhtml2pdf import pisa + + +class HtmlTemplate: + def __init__(self, templates): + self.template_name = 'invoice.html' + if not Path(templates).exists(): + raise FileNotFoundError(f'Inivalid path to template files: {templates}') + self.templates = templates + + def prepare_template(self, invoice_data, envelope_data): + try: + loader = FileSystemLoader(self.templates) + env = Environment(loader=loader) + + 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 convert_html_to_pdf(source_html, output_filename): + # open output file for writing (truncated binary) + result_file = open(output_filename, "w+b") + + # convert HTML to PDF + pisa_status = pisa.CreatePDF( + source_html, # the HTML to convert + dest=result_file) # file handle to recieve result + + # close output file + result_file.close() # close output file + + # return False on success and True on errors + return pisa_status.err diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..9ac8e90 --- /dev/null +++ b/src/main.py @@ -0,0 +1,104 @@ +import argparse +import yaml +from pathlib import Path + +from invoice_generator import html_generator + + +class DataObject: + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +def parse_yaml_file(file_path): + with open(file_path, 'r') as file: + return yaml.safe_load(file) + + +def check_file(file_path): + try_out_extensions = ['.yaml', '.yml'] + + for ext in try_out_extensions: + if file_path.exists(): + return file_path + file_path = file_path.with_suffix(ext) + return None + + +# Utility function + + +def main(): + 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') + parser.add_argument('-b', '--base', type=str, required=True, help='base directory for invoice and envelope files') + parser.add_argument('-i', '--invoice', type=str, required=True, help='Invoice file name') + parser.add_argument('-e', '--envelope', type=str, required=False, default='envelope.yaml', + help='Envelope file name') + parser.add_argument('-t', '--template', type=str, required=False, default='template', + help='directory for template files') + + args = parser.parse_args() + + invoice_file = args.invoice + envelope_file = args.envelope + + print(f'Searching for invoice files in: {args.base}') + + invoice_file = Path(args.base) / invoice_file + envelope_file = Path(args.base) / envelope_file + + invoice_file = check_file(invoice_file) + envelope_file = check_file(envelope_file) + + print(f'Found invoice file: {invoice_file}') + print(f'Found envelope file: {envelope_file}') + + if invoice_file is None: + print('Error: No invoice file found') + return + if envelope_file is None: + print('Error: No envelope file found') + return + + try: + invoice = parse_yaml_file(invoice_file) + envelope = parse_yaml_file(envelope_file) + + print('Invoice data:') + print(invoice) + + print(envelope) + + print('Envelope data:') + envelope_data = DataObject(**envelope) + print(envelope_data.__dict__) + + print('Invoice data:') + invoice_data = DataObject(**invoice) + print(invoice_data.__dict__) + + except FileNotFoundError as e: + print(f'Error: {e}') + return + except yaml.YAMLError as e: + print(f'Error: {e}') + return + except Exception as e: + print(f'Error: {e}') + return + + generator = html_generator.HtmlTemplate(args.template) + template = generator.prepare_template(invoice_data, envelope_data) + + try: + html_generator.HtmlTemplate.convert_html_to_pdf(template, 'invoice.pdf') + except Exception as e: + print(f'Error: {e}') + return + + +if __name__ == "__main__": + main() diff --git a/test_data/RG004711.yaml b/test_data/RG004711.yaml new file mode 100644 index 0000000..4838742 --- /dev/null +++ b/test_data/RG004711.yaml @@ -0,0 +1,21 @@ +CustomerId: KD01234 +#InvoiceDate: 2023-12-23 + +Positions: + - Title: "Zuckerwatte fressen" + SubTitle: "Leistungszeitraum: 11/2022" + PricePerUnit: + Quantity: 123.45 + + - 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 new file mode 100644 index 0000000..41450d4 --- /dev/null +++ b/test_data/envelope.yaml @@ -0,0 +1,50 @@ +Config: + DefaultFont: Barlow Semi Condensed + MonoFont: Barlow Semi Condensed + +AddressContent: + LogoFile: Images/logo.svg + AddressBoxSender: Torsten Ueberschar - Pfarrweg 1 - 57439 Attendorn + Contents: + - Head: + 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 + + +Invoice: + Vat: 19.0 + Salutation: Sehr geehrte Damen und Herren, + + Introduction: > + 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}. + + + Mit freundlichen Grüßen + +Customers: + - Id: KD01234 + PricePerUnit: 88.88 + DueDate: 30 + AddressField: | + Klaus Peter Klausen + Am Klausenhof 1 + 04711 Klausenhausen diff --git a/test_data/templates/invoice.html b/test_data/templates/invoice.html new file mode 100644 index 0000000..96c06ea --- /dev/null +++ b/test_data/templates/invoice.html @@ -0,0 +1,107 @@ + + + + + + + +
+

Absender

+
Anschrift + in + mehreren + Zeilen +
+
+
+ {% for address in envelope.AddressContent.Contents %} + {% if address.Head %} +

{{ address.Head }}

+ {% endif %} +

{{ address.Text | replace('\n', '
') }}

+ {% endfor %} +
+ + + +To PDF or not to PDF + +
+

About Us

+

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.

+
+ +
+

Our Services

+ +
+ +
+

About Us

+

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.

+
+ +
+

Our Services

+ +
+ + \ No newline at end of file