first try

This commit is contained in:
Torsten Ueberschar
2024-02-07 17:05:53 +01:00
parent 17346d5197
commit 2c2066450c
15 changed files with 556 additions and 162 deletions

164
.gitignore vendored
View File

@@ -1,162 +1,2 @@
# ---> Python .idea/
# Byte-compiled / optimized / DLL files .code/
__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/

10
.idea/freelance_invoice.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (freelance_invoice)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (freelance_invoice)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/freelance_invoice.iml" filepath="$PROJECT_DIR$/.idea/freelance_invoice.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

132
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="7672063a-a324-4a9c-a8d3-c38955c8d763" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/freelance_invoice.iml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/setup.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/invoice_generator/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/invoice_generator/html_generator.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_data/RG004711.yaml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_data/envelope.yaml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_data/templates/invoice.html" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ChangesViewManager">
<option name="groupingKeys">
<option value="directory" />
</option>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
<option value="HTML File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="2c21ZczAqxeYNrytEGCIucesn9A" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultHtmlFileTemplate": "HTML File",
"Python.main.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "/home/torsten/PycharmProjects/freelance_invoice/test_data",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "preferences.pluginManager",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/test_data" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src" />
</key>
</component>
<component name="RunManager">
<configuration name="main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="freelance_invoice" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/main.py" />
<option name="PARAMETERS" value="--invoice RG004711 --base test_data/ --template ./test_data/templates/" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-5a2391486177-2887949eec09-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-233.13763.11" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="7672063a-a324-4a9c-a8d3-c38955c8d763" name="Changes" comment="" />
<created>1707294915620</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1707294915620</updated>
<workItem from="1707294917219" duration="20842000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/freelance_invoice$main.coverage" NAME="main Coverage Results" MODIFIED="1707321806053" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

35
requirements.txt Normal file
View File

@@ -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

30
setup.py Normal file
View File

@@ -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
],
)

View File

View File

@@ -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

104
src/main.py Normal file
View File

@@ -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()

21
test_data/RG004711.yaml Normal file
View File

@@ -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

50
test_data/envelope.yaml Normal file
View File

@@ -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

View File

@@ -0,0 +1,107 @@
<html>
<head>
<style>
@page {
size: a4 portrait;
margin: 1cm 1cm 1cm 2cm;
font-size: 12pt;
font-family: "Barlow Semi Condensed";
@frame address_frame {
/* Static Frame */
-pdf-frame-content: address_frame_content;
left: 2.5cm;
width: 8.5cm;
top: 5.5cm;
height: 4cm;
-pdf-frame-border: 1;
}
@frame letter_head_frame {
/* Another static Frame */
-pdf-frame-content: letter_head_content;
left: 15.5cm;
width: 4cm;
top: 1cm;
}
@frame content_first_page_frame {
/* Content Frame */
left: 2cm;
width: 13cm;
top: 11cm;
bottom: 1cm;
-pdf-frame-border: 1;
}
}
#address_frame_content {
background-color: lightcoral;
}
#letter_head_content {
background-color: lightblue;
}
</style>
</head>
<body>
<!-- Content for Static Frame 'header_frame' -->
<div id="address_frame_content">
<p>Absender</p>
<address>Anschrift
in
mehreren
Zeilen
</address>
</div>
<div id="letter_head_content">
{% for address in envelope.AddressContent.Contents %}
{% if address.Head %}
<p><strong>{{ address.Head }}</strong></p>
{% endif %}
<p>{{ address.Text | replace('\n', '<br>') }}</p>
{% endfor %}
</div>
<!-- HTML Content -->
To PDF or not to PDF
<section>
<h2>About Us</h2>
<p>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.</p>
</section>
<section>
<h2>Our Services</h2>
<ul>
<li>Service 1</li>
<li>Service 2</li>
<li>Service 3</li>
</ul>
</section>
<section>
<h2>About Us</h2>
<p>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.</p>
</section>
<section>
<h2>Our Services</h2>
<ul>
<li>Service 1</li>
<li>Service 2</li>
<li>Service 3</li>
</ul>
</section>
</body>
</html>