Compare commits
6 Commits
17346d5197
...
8371715072
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8371715072 | ||
|
|
f7dfcb762d | ||
|
|
4345cfb499 | ||
|
|
7c2703f2fe | ||
|
|
1720f1b89e | ||
|
|
2c2066450c |
164
.gitignore
vendored
164
.gitignore
vendored
@@ -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/
|
||||
10
.idea/freelance_invoice.iml
generated
Normal file
10
.idea/freelance_invoice.iml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
132
.idea/workspace.xml
generated
Normal 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>
|
||||
153
README.md
153
README.md
@@ -1,2 +1,153 @@
|
||||
# freelance_invoice
|
||||
# Freelance Invoice
|
||||
|
||||
is a simple command line tool to generate invoices for freelance work. It uses a simple template system based
|
||||
on `jinja2` to generate the
|
||||
invoice in HTML and then converts it to a PDF using `xhtml2pdf`.
|
||||
|
||||
## inatallation
|
||||
|
||||
Use python venv:
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
Install the requirements:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
There are two yaml files describing your invoice:
|
||||
|
||||
- `invoice.yaml` contains the invoice data
|
||||
- `envelope.yaml` contains the address data of the sender and the recipient
|
||||
|
||||
The invoice data is read from the `invoice.yaml` file and the address data is read from the `envelope.yaml` file. The
|
||||
invoice data is then used to fill in the invoice template and the address data is used to fill in the address fields of
|
||||
the invoice.
|
||||
|
||||
Some data are globaly available from the envelope template. Some of them can be overriden by the invoice template.
|
||||
|
||||
invoice.yaml - The Name of the invoice file is used as the invoice number but you are free to override it in the
|
||||
invoice.yaml file.
|
||||
|
||||
```yaml
|
||||
Id: RG004712 # overrides the file name as invlice Number
|
||||
CustomerId: KD01234 # The customer id is used to identify the customer and is beeing looked up in the envelope.yaml file.
|
||||
InvoiceDate: 2023-12-23 # The date of the invoice. If not set the current date is used.
|
||||
|
||||
Positions:
|
||||
- Title: "Zuckerwatte fressen Ganz besonders langer Text"
|
||||
SubTitle: "Leistungszeitraum: 11/2022"
|
||||
PricePerUnit: 100 # The price per unit is taken from the envelope.yaml file using the customer id but can be overriden here.
|
||||
Quantity: 100
|
||||
|
||||
- Title: "Aschlecken"
|
||||
SubTitle: "Leistungszeitraum: 11/2022"
|
||||
PricePerUnit: 99.99
|
||||
Quantity: 12
|
||||
|
||||
- Title: "Aschkriechen"
|
||||
SubTitle: "Leistungszeitraum: 10/2022"
|
||||
PricePerUnit: 77.88
|
||||
Quantity: 3
|
||||
```
|
||||
|
||||
envelope.yaml
|
||||
|
||||
```yaml
|
||||
AddressContent:
|
||||
# AddressBoxSender is is used as the sender address in the letter head inside the address windows.
|
||||
AddressBoxSender: "Abs.: Torsten Ueberschar - Pfarrweg 1 - 57439 Attendorn"
|
||||
# The Content is a place to put your personal data. E.G. it is used in the footer of the invoice.
|
||||
# you may put markdown in here.
|
||||
Contents:
|
||||
- Text: |
|
||||

|
||||
Torsten Ueberschar
|
||||
Pfarrweg 1
|
||||
57439 Attendorn
|
||||
- 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
|
||||
|
||||
# The Invoice field is used to put default values for the invoice. You may override them in the invoice.yaml file.
|
||||
Invoice:
|
||||
Vat: 19.0
|
||||
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%%.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
*Torsten Uebeschar*
|
||||
|
||||
# Customers is a list of customers. The customer id is used to identify the customer and is beeing looked up in the invoice.yaml file.
|
||||
Customers:
|
||||
- CustomerId: KD01234
|
||||
PricePerUnit: 88.88
|
||||
DueDate: 30 # DueDate is the number of days the customer has to pay the invoice.
|
||||
# The AddressField is used to fill in the address field window of the invoice.
|
||||
AddressField: |
|
||||
Klaus Peter Klausen
|
||||
Am Klausenhof 1
|
||||
04711 Klausenhausen
|
||||
```
|
||||
|
||||
```bash
|
||||
python src/main.py --help
|
||||
Simple invoice generator for freelancers and small businesses by Torsten Ueberschar
|
||||
|
||||
usage: main.py [-h] -b BASE -i INVOICE [-e ENVELOPE] [-t TEMPLATE]
|
||||
|
||||
Read invoice and envelope data from yaml file
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-b BASE, --base BASE base directory for invoice and envelope files
|
||||
-i INVOICE, --invoice INVOICE
|
||||
Invoice file name
|
||||
-e ENVELOPE, --envelope ENVELOPE
|
||||
Envelope file name
|
||||
-t TEMPLATE, --template TEMPLATE
|
||||
directory for template files
|
||||
```
|
||||
|
||||
## example
|
||||
|
||||
```bash
|
||||
python src/main.py -b test_data -i invoice.yaml -e envelope.yaml -t test_data/templates
|
||||
```
|
||||
|
||||
## to do
|
||||
|
||||
- make image path in invoice.yaml relative to invoice.yaml
|
||||
- find out how to move an image to the right place in the pdf
|
||||
- give more structure to code
|
||||
- make code testable
|
||||
|
||||
## see also
|
||||
|
||||
- https://xhtml2pdf.readthedocs.io/en/latest/index.html
|
||||
- https://jinja.palletsprojects.com/
|
||||
|
||||
|
||||
36
requirements.txt
Normal file
36
requirements.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
Markdown==3.5.2
|
||||
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
30
setup.py
Normal 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
|
||||
],
|
||||
)
|
||||
0
src/invoice_generator/__init__.py
Normal file
0
src/invoice_generator/__init__.py
Normal file
71
src/invoice_generator/html_generator.py
Normal file
71
src/invoice_generator/html_generator.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from pathlib import Path
|
||||
from jinja2 import FileSystemLoader, TemplateNotFound, Environment
|
||||
|
||||
from xhtml2pdf import pisa
|
||||
from datetime import date, timedelta
|
||||
|
||||
import markdown
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
133
src/main.py
Normal file
133
src/main.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import argparse
|
||||
import yaml
|
||||
import locale
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
|
||||
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')
|
||||
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('Envelope data:')
|
||||
envelope_data = DataObject(**envelope)
|
||||
print(envelope_data.__dict__)
|
||||
|
||||
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
|
||||
|
||||
invoice_data.Id = invoice_data.Id or invoice_file.stem
|
||||
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:
|
||||
print('Generating invoice...')
|
||||
invoice_pdf = Path(invoice_data.Id).with_suffix('.pdf')
|
||||
print(f'Invoice PDF: {invoice_pdf}')
|
||||
html_generator.HtmlTemplate.convert_html_to_pdf(template, invoice_pdf)
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
test_data/RG004711.yaml
Normal file
22
test_data/RG004711.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
#Id: RG004712
|
||||
CustomerId: KD01234
|
||||
#InvoiceDate: 2023-12-23
|
||||
|
||||
Positions:
|
||||
- Title: "Zuckerwatte fressen Ganz besonders langer Text"
|
||||
SubTitle: "Leistungszeitraum: 11/2022"
|
||||
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
|
||||
|
||||
|
||||
|
||||
48
test_data/envelope.yaml
Normal file
48
test_data/envelope.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
AddressContent:
|
||||
LogoFile: Images/logo.svg
|
||||
AddressBoxSender: "Abs.: Torsten Ueberschar - Pfarrweg 1 - 57439 Attendorn"
|
||||
Contents:
|
||||
- Text: |
|
||||

|
||||
Torsten Ueberschar
|
||||
Pfarrweg 1
|
||||
57439 Attendorn
|
||||
- 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
|
||||
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%%.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
*Torsten Uebeschar*
|
||||
|
||||
Customers:
|
||||
- CustomerId: KD01234
|
||||
PricePerUnit: 88.88
|
||||
DueDate: 30
|
||||
AddressField: |
|
||||
Klaus Peter Klausen
|
||||
Am Klausenhof 1
|
||||
04711 Klausenhausen
|
||||
BIN
test_data/templates/fonts/BarlowSemiCondensed-Black.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Black.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-BlackItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-ExtraBold.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-ExtraBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-ExtraLight.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-ExtraLight.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Italic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Italic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Light.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Light.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Medium.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Medium.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-MediumItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-SemiBold.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-SemiBoldItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-Thin.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-Thin.ttf
Normal file
Binary file not shown.
BIN
test_data/templates/fonts/BarlowSemiCondensed-ThinItalic.ttf
Normal file
BIN
test_data/templates/fonts/BarlowSemiCondensed-ThinItalic.ttf
Normal file
Binary file not shown.
83
test_data/templates/images/logo.svg
Normal file
83
test_data/templates/images/logo.svg
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2119 1061" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g transform="matrix(1,0,0,1,-169.939,-343.077)">
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-430.189,551.712)">
|
||||
<path d="M461.264,6.409L461.264,147.034C461.264,156.409 465.951,161.097 475.326,161.097L503.451,161.097C512.826,161.097 517.514,156.409 517.514,147.034L517.514,6.409L559.701,6.409L559.701,147.034C559.701,184.534 540.951,203.284 503.451,203.284L475.326,203.284C437.826,203.284 419.076,184.534 419.076,147.034L419.076,6.409L461.264,6.409Z" style="fill:rgb(27,118,215);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-462.013,551.712)">
|
||||
<path d="M714.389,83.753L714.389,125.941L630.014,125.941L630.014,161.097L714.389,161.097L714.389,203.284L587.826,203.284L587.826,6.409L714.389,6.409L714.389,48.597L630.014,48.597L630.014,83.753L714.389,83.753Z" style="fill:rgb(27,118,215);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-620.427,551.712)">
|
||||
<path d="M742.514,203.284L742.514,161.097L826.889,161.097C836.264,161.097 840.951,156.409 840.951,147.034L840.951,140.003C840.951,130.628 836.264,125.941 826.889,125.941L798.764,125.941C761.264,125.941 742.514,107.191 742.514,69.691L742.514,62.659C742.514,25.159 761.264,6.409 798.764,6.409L883.139,6.409L883.139,48.597L798.764,48.597C789.389,48.597 784.701,53.284 784.701,62.659L784.701,69.691C784.701,79.066 789.389,83.753 798.764,83.753L826.889,83.753C864.389,83.753 883.139,102.503 883.139,140.003L883.139,147.034C883.139,184.534 864.389,203.284 826.889,203.284L742.514,203.284Z" style="fill:rgb(15,168,89);fill-rule:nonzero;"/>
|
||||
<clipPath id="_clip1">
|
||||
<path d="M742.514,203.284L742.514,161.097L826.889,161.097C836.264,161.097 840.951,156.409 840.951,147.034L840.951,140.003C840.951,130.628 836.264,125.941 826.889,125.941L798.764,125.941C761.264,125.941 742.514,107.191 742.514,69.691L742.514,62.659C742.514,25.159 761.264,6.409 798.764,6.409L883.139,6.409L883.139,48.597L798.764,48.597C789.389,48.597 784.701,53.284 784.701,62.659L784.701,69.691C784.701,79.066 789.389,83.753 798.764,83.753L826.889,83.753C864.389,83.753 883.139,102.503 883.139,140.003L883.139,147.034C883.139,184.534 864.389,203.284 826.889,203.284L742.514,203.284Z" clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<g transform="matrix(1,0,0,1,73.2367,0)">
|
||||
<path d="M714.389,83.753L714.389,125.941L630.014,125.941L630.014,161.097L714.389,161.097L714.389,203.284L587.826,203.284L587.826,6.409L714.389,6.409L714.389,48.597L630.014,48.597L630.014,83.753L714.389,83.753Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-814.758,551.712)">
|
||||
<path d="M1087.05,6.409L1146.11,6.409L1192.51,125.941L1238.92,6.409L1297.98,6.409L1297.98,203.284L1255.8,203.284L1255.8,83.753L1210.8,203.284L1174.23,203.284L1129.23,83.753L1129.23,203.284L1087.05,203.284L1087.05,6.409Z" style="fill:rgb(202,4,4);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-661.589,551.712)">
|
||||
<path d="M1058.92,147.034C1058.92,184.534 1040.17,203.284 1002.67,203.284L967.514,203.284C930.014,203.284 911.264,184.534 911.264,147.034L911.264,62.659C911.264,25.159 930.014,6.409 967.514,6.409L1002.67,6.409C1040.17,6.409 1058.92,25.159 1058.92,62.659L1058.92,147.034ZM953.451,147.034C953.451,156.409 958.139,161.097 967.514,161.097L1002.67,161.097C1012.05,161.097 1016.73,156.409 1016.73,147.034L1016.73,62.659C1016.73,53.284 1012.05,48.597 1002.67,48.597L967.514,48.597C958.139,48.597 953.451,53.284 953.451,62.659L953.451,147.034Z" style="fill:rgb(15,168,89);fill-rule:nonzero;"/>
|
||||
<clipPath id="_clip2">
|
||||
<path d="M1058.92,147.034C1058.92,184.534 1040.17,203.284 1002.67,203.284L967.514,203.284C930.014,203.284 911.264,184.534 911.264,147.034L911.264,62.659C911.264,25.159 930.014,6.409 967.514,6.409L1002.67,6.409C1040.17,6.409 1058.92,25.159 1058.92,62.659L1058.92,147.034ZM953.451,147.034C953.451,156.409 958.139,161.097 967.514,161.097L1002.67,161.097C1012.05,161.097 1016.73,156.409 1016.73,147.034L1016.73,62.659C1016.73,53.284 1012.05,48.597 1002.67,48.597L967.514,48.597C958.139,48.597 953.451,53.284 953.451,62.659L953.451,147.034Z" clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<g transform="matrix(1,0,0,1,-70.8119,0)">
|
||||
<path d="M1087.05,6.409L1146.11,6.409L1192.51,125.941L1238.92,6.409L1297.98,6.409L1297.98,203.284L1255.8,203.284L1255.8,83.753L1210.8,203.284L1174.23,203.284L1129.23,83.753L1129.23,203.284L1087.05,203.284L1087.05,6.409Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(2.16305,0,0,2.16305,-853.475,551.712)">
|
||||
<path d="M1452.67,83.753L1452.67,125.941L1368.3,125.941L1368.3,161.097L1452.67,161.097L1452.67,203.284L1326.11,203.284L1326.11,6.409L1452.67,6.409L1452.67,48.597L1368.3,48.597L1368.3,83.753L1452.67,83.753Z" style="fill:rgb(202,4,4);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(6.69295,0,0,6.69295,-1739.71,-1575.25)">
|
||||
<path d="M344.179,415.947C344.179,414.531 343.651,413.379 342.739,412.587C341.971,411.891 341.227,411.603 339.643,411.363L337.651,411.051C336.763,410.907 335.947,410.619 335.419,410.163C334.891,409.707 334.651,409.011 334.651,408.171C334.651,406.251 335.971,404.979 338.275,404.979C340.051,404.979 341.227,405.483 342.259,406.467L343.435,405.291C341.971,403.995 340.531,403.419 338.347,403.419C334.963,403.419 332.875,405.315 332.875,408.219C332.875,409.611 333.331,410.667 334.195,411.435C334.963,412.107 335.971,412.515 337.315,412.731L339.307,413.019C340.531,413.211 341.011,413.379 341.539,413.859C342.115,414.363 342.379,415.083 342.379,415.995C342.379,418.011 340.843,419.163 338.323,419.163C336.403,419.163 335.083,418.731 333.667,417.315L332.443,418.539C334.027,420.147 335.731,420.795 338.275,420.795C341.851,420.795 344.179,418.947 344.179,415.947Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M359.299,412.107C359.299,408.195 359.179,406.707 357.571,405.099C356.515,404.043 355.099,403.419 353.323,403.419C351.547,403.419 350.107,404.043 349.051,405.099C347.443,406.707 347.347,408.195 347.347,412.107C347.347,416.019 347.443,417.507 349.051,419.115C350.107,420.171 351.547,420.795 353.323,420.795C355.099,420.795 356.515,420.171 357.571,419.115C359.179,417.507 359.299,416.019 359.299,412.107ZM357.475,412.107C357.475,415.611 357.355,416.859 356.275,417.963C355.483,418.755 354.475,419.163 353.323,419.163C352.171,419.163 351.163,418.755 350.371,417.963C349.291,416.859 349.171,415.611 349.171,412.107C349.171,408.603 349.291,407.355 350.371,406.251C351.163,405.459 352.171,405.051 353.323,405.051C354.475,405.051 355.483,405.459 356.275,406.251C357.355,407.355 357.475,408.603 357.475,412.107Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M374.203,405.195L374.203,403.563L363.595,403.563L363.595,420.651L365.419,420.651L365.419,413.115L372.907,413.115L372.907,411.483L365.419,411.483L365.419,405.195L374.203,405.195Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M387.475,405.195L387.475,403.563L375.763,403.563L375.763,405.195L380.707,405.195L380.707,420.651L382.531,420.651L382.531,405.195L387.475,405.195Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M408.307,403.563L406.363,403.563L403.027,417.339L399.259,403.563L397.627,403.563L393.859,417.339L390.523,403.563L388.579,403.563L392.947,420.651L394.651,420.651L398.443,406.971L402.235,420.651L403.939,420.651L408.307,403.563Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M422.827,420.651L416.563,403.563L415.027,403.563L408.739,420.651L410.683,420.651L412.051,416.787L419.515,416.787L420.883,420.651L422.827,420.651ZM418.987,415.203L412.603,415.203L415.819,406.179L418.987,415.203Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M437.539,420.651L433.483,412.899C435.691,412.443 437.203,410.811 437.203,408.315C437.203,405.363 435.115,403.563 432.139,403.563L425.587,403.563L425.587,420.651L427.411,420.651L427.411,413.067L431.515,413.067L435.403,420.651L437.539,420.651ZM435.379,408.339C435.379,410.427 433.963,411.459 431.971,411.459L427.411,411.459L427.411,405.195L431.971,405.195C433.963,405.195 435.379,406.251 435.379,408.339Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M451.939,420.651L451.939,419.019L443.155,419.019L443.155,412.851L450.643,412.851L450.643,411.219L443.155,411.219L443.155,405.195L451.939,405.195L451.939,403.563L441.331,403.563L441.331,420.651L451.939,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M473.395,415.011L473.395,403.563L471.571,403.563L471.571,414.867C471.571,417.459 469.915,419.163 467.419,419.163C464.923,419.163 463.291,417.459 463.291,414.867L463.291,403.563L461.467,403.563L461.467,415.011C461.467,418.419 463.963,420.795 467.419,420.795C470.875,420.795 473.395,418.419 473.395,415.011Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M490.723,420.651L490.723,403.563L488.899,403.563L488.899,417.219L479.827,403.563L478.099,403.563L478.099,420.651L479.923,420.651L479.923,406.947L488.995,420.651L490.723,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M507.523,412.107C507.523,409.491 507.523,406.995 506.011,405.363C504.859,404.115 503.443,403.563 501.523,403.563L495.667,403.563L495.667,420.651L501.523,420.651C503.443,420.651 504.859,420.099 506.011,418.851C507.523,417.219 507.523,414.723 507.523,412.107ZM505.699,412.107C505.699,414.483 505.675,416.595 504.571,417.771C503.707,418.731 502.507,419.019 501.211,419.019L497.491,419.019L497.491,405.195L501.211,405.195C502.507,405.195 503.707,405.483 504.571,406.443C505.675,407.619 505.699,409.731 505.699,412.107Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M532.435,420.651L532.435,403.563L530.611,403.563L525.187,415.683L519.619,403.563L517.795,403.563L517.795,420.651L519.619,420.651L519.619,407.667L524.371,417.963L525.931,417.963L530.611,407.667L530.611,420.651L532.435,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M547.987,420.651L547.987,419.019L539.203,419.019L539.203,412.851L546.691,412.851L546.691,411.219L539.203,411.219L539.203,405.195L547.987,405.195L547.987,403.563L537.379,403.563L537.379,420.651L547.987,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M563.635,412.107C563.635,409.491 563.635,406.995 562.123,405.363C560.971,404.115 559.555,403.563 557.635,403.563L551.779,403.563L551.779,420.651L557.635,420.651C559.555,420.651 560.971,420.099 562.123,418.851C563.635,417.219 563.635,414.723 563.635,412.107ZM561.811,412.107C561.811,414.483 561.787,416.595 560.683,417.771C559.819,418.731 558.619,419.019 557.323,419.019L553.603,419.019L553.603,405.195L557.323,405.195C558.619,405.195 559.819,405.483 560.683,406.443C561.787,407.619 561.811,409.731 561.811,412.107Z" style="fill-rule:nonzero;"/>
|
||||
<rect x="567.931" y="403.563" width="1.824" height="17.088" style="fill-rule:nonzero;"/>
|
||||
<path d="M585.307,420.651L585.307,419.019L576.523,419.019L576.523,412.851L584.011,412.851L584.011,411.219L576.523,411.219L576.523,405.195L585.307,405.195L585.307,403.563L574.699,403.563L574.699,420.651L585.307,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M601.723,420.651L601.723,403.563L599.899,403.563L599.899,417.219L590.827,403.563L589.099,403.563L589.099,420.651L590.923,420.651L590.923,406.947L599.995,420.651L601.723,420.651Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M345.571,439.251L345.571,427.803L343.747,427.803L343.747,439.107C343.747,441.699 342.091,443.403 339.595,443.403C337.099,443.403 335.467,441.699 335.467,439.107L335.467,427.803L333.643,427.803L333.643,439.251C333.643,442.659 336.139,445.035 339.595,445.035C343.051,445.035 345.571,442.659 345.571,439.251Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M360.883,444.891L360.883,443.259L352.099,443.259L352.099,437.091L359.587,437.091L359.587,435.459L352.099,435.459L352.099,429.435L360.883,429.435L360.883,427.803L350.275,427.803L350.275,444.891L360.883,444.891Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M376.411,440.163C376.411,438.123 375.259,436.707 373.627,436.107C375.067,435.579 376.147,434.163 376.147,432.363C376.147,429.507 374.083,427.803 371.107,427.803L364.675,427.803L364.675,444.891L371.323,444.891C374.347,444.891 376.411,443.259 376.411,440.163ZM374.587,440.115C374.587,442.035 373.267,443.259 371.155,443.259L366.499,443.259L366.499,436.995L371.155,436.995C373.267,436.995 374.587,438.195 374.587,440.115ZM374.323,432.387C374.323,434.427 372.859,435.363 370.963,435.363L366.499,435.363L366.499,429.435L370.963,429.435C372.859,429.435 374.323,430.347 374.323,432.387Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M391.315,444.891L391.315,443.259L382.531,443.259L382.531,437.091L390.019,437.091L390.019,435.459L382.531,435.459L382.531,429.435L391.315,429.435L391.315,427.803L380.707,427.803L380.707,444.891L391.315,444.891Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M407.059,444.891L403.003,437.139C405.211,436.683 406.723,435.051 406.723,432.555C406.723,429.603 404.635,427.803 401.659,427.803L395.107,427.803L395.107,444.891L396.931,444.891L396.931,437.307L401.035,437.307L404.923,444.891L407.059,444.891ZM404.899,432.579C404.899,434.667 403.483,435.699 401.491,435.699L396.931,435.699L396.931,429.435L401.491,429.435C403.483,429.435 404.899,430.491 404.899,432.579Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M421.147,440.187C421.147,438.771 420.619,437.619 419.707,436.827C418.939,436.131 418.195,435.843 416.611,435.603L414.619,435.291C413.731,435.147 412.915,434.859 412.387,434.403C411.859,433.947 411.619,433.251 411.619,432.411C411.619,430.491 412.939,429.219 415.243,429.219C417.019,429.219 418.195,429.723 419.227,430.707L420.403,429.531C418.939,428.235 417.499,427.659 415.315,427.659C411.931,427.659 409.843,429.555 409.843,432.459C409.843,433.851 410.299,434.907 411.163,435.675C411.931,436.347 412.939,436.755 414.283,436.971L416.275,437.259C417.499,437.451 417.979,437.619 418.507,438.099C419.083,438.603 419.347,439.323 419.347,440.235C419.347,442.251 417.811,443.403 415.291,443.403C413.371,443.403 412.051,442.971 410.635,441.555L409.411,442.779C410.995,444.387 412.699,445.035 415.243,445.035C418.819,445.035 421.147,443.187 421.147,440.187Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M436.219,439.947L434.395,439.947C433.939,442.059 432.427,443.403 430.291,443.403C429.139,443.403 428.131,442.995 427.339,442.203C426.259,441.099 426.139,439.851 426.139,436.347C426.139,432.843 426.259,431.595 427.339,430.491C428.131,429.699 429.139,429.291 430.291,429.291C432.427,429.291 433.891,430.635 434.347,432.747L436.219,432.747C435.619,429.555 433.387,427.659 430.291,427.659C428.515,427.659 427.075,428.283 426.019,429.339C424.411,430.947 424.315,432.435 424.315,436.347C424.315,440.259 424.411,441.747 426.019,443.355C427.075,444.411 428.515,445.035 430.291,445.035C433.363,445.035 435.619,443.139 436.219,439.947Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M452.011,444.891L452.011,427.803L450.187,427.803L450.187,435.459L442.075,435.459L442.075,427.803L440.251,427.803L440.251,444.891L442.075,444.891L442.075,437.091L450.187,437.091L450.187,444.891L452.011,444.891Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M468.859,444.891L462.595,427.803L461.059,427.803L454.771,444.891L456.715,444.891L458.083,441.027L465.547,441.027L466.915,444.891L468.859,444.891ZM465.019,439.443L458.635,439.443L461.851,430.419L465.019,439.443Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M483.571,444.891L479.515,437.139C481.723,436.683 483.235,435.051 483.235,432.555C483.235,429.603 481.147,427.803 478.171,427.803L471.619,427.803L471.619,444.891L473.443,444.891L473.443,437.307L477.547,437.307L481.435,444.891L483.571,444.891ZM481.411,432.579C481.411,434.667 479.995,435.699 478.003,435.699L473.443,435.699L473.443,429.435L478.003,429.435C479.995,429.435 481.411,430.491 481.411,432.579Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.2512,0,0,12.2512,-6531.47,-6259.35)">
|
||||
<g transform="matrix(1,0,0,1,20.1367,39.3522)">
|
||||
<rect x="535.943" y="508.648" width="9.08" height="9.08"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,11.0571,39.3522)">
|
||||
<rect x="535.943" y="508.648" width="9.08" height="9.08" style="fill:rgb(27,118,215);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,20.1367,30.2726)">
|
||||
<rect x="535.943" y="508.648" width="9.08" height="9.08" style="fill:rgb(202,4,4);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,20.1367,48.4318)">
|
||||
<rect x="535.943" y="508.648" width="9.08" height="9.08" style="fill:rgb(0,159,77);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
210
test_data/templates/invoice.html
Normal file
210
test_data/templates/invoice.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Barlow Semi Condensed";
|
||||
src: url("test_data/templates/fonts/BarlowSemiCondensed-Regular.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Barlow Semi Condensed";
|
||||
src: url("test_data/templates/fonts/BarlowSemiCondensed-Light.ttf");
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Barlow Semi Condensed";
|
||||
src: url("test_data/templates/fonts/BarlowSemiCondensed-LightItalic.ttf");
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Barlow Semi Condensed";
|
||||
src: url("test_data/templates/fonts/BarlowSemiCondensed-Bold.ttf");
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Barlow Semi Condensed";
|
||||
src: url("test_data/templates/fonts/BarlowSemiCondensed-BoldItalic.ttf");
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: a4 portrait;
|
||||
margin: 1cm 1cm 1cm 2cm;
|
||||
font-size: 12pt;
|
||||
|
||||
@frame address_frame {
|
||||
/* Static Frame */
|
||||
-pdf-frame-content: address_frame_content;
|
||||
left: 2.5cm;
|
||||
width: 8.5cm;
|
||||
top: 5.5cm;
|
||||
height: 4cm;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Barlow Semi Condensed';
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
table {
|
||||
font-family: 'Barlow Semi Condensed', sans-serif !important;
|
||||
}
|
||||
|
||||
#address_frame_content {
|
||||
}
|
||||
|
||||
#letter_head_content {
|
||||
}
|
||||
|
||||
.head {
|
||||
}
|
||||
|
||||
.positionen {
|
||||
padding-top: 1mm;
|
||||
padding-bottom: 1mm;
|
||||
}
|
||||
|
||||
.links {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rechts {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.summe {
|
||||
padding-top: 1mm;
|
||||
border-top: .1pt solid black;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.underline {
|
||||
border-bottom: .1pt solid black;
|
||||
}
|
||||
|
||||
.overline {
|
||||
border-top: .1pt solid black;
|
||||
}
|
||||
|
||||
.head_data {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.head_data td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 8pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Content for Static Frame 'header_frame' -->
|
||||
<div id="address_frame_content">
|
||||
<p class="underline">{{ envelope.AddressContent.AddressBoxSender }}</p>
|
||||
<address>
|
||||
{{ invoice.AddressField | markdown_to_html }}
|
||||
</address>
|
||||
</div>
|
||||
<div id="letter_head_content" class="small">
|
||||
{% for address in envelope.AddressContent.Contents %}
|
||||
<p>{{ address.Text | markdown_to_html }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- HTML Content -->
|
||||
<table class="head_data">
|
||||
<tr class="small">
|
||||
<td>Rechnungsnummer:</td>
|
||||
<td>Kundennummer:</td>
|
||||
<td>Rechnungsdatum:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ invoice.Id }}</td>
|
||||
<td>{{ invoice.CustomerId }}</td>
|
||||
<td>{{ invoice.InvoiceDate.strftime('%d. %B %Y') }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<section>
|
||||
<p>{{ invoice.Introduction | markdown_to_html }}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<table>
|
||||
<tr class="head underline">
|
||||
<th class="links" style="width: 55%">Aktivität</th>
|
||||
<th class="rechts"style="width: 15%">Anzahl</th>
|
||||
<th class="rechts"style="width: 15%">Einheit</th>
|
||||
<th class="rechts"style="width: 15%">Betrag</th>
|
||||
</tr>
|
||||
{% for position in invoice.Positions %}
|
||||
<tr class="positionen">
|
||||
<td class="links">{{ position.Title }}<br><span class="small">{{ position.SubTitle }}</span></td>
|
||||
<td class="rechts">{{ format_float(position.Quantity | float) }}</td>
|
||||
<td class="rechts">{{ format_float((position.PricePerUnit or invoice.PricePerUnit) | float) }}
|
||||
</td>
|
||||
<td class="rechts">{{ format_float(position.Quantity * ( position.PricePerUnit or
|
||||
invoice.PricePerUnit ))}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="summe">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="rechts">Nettosumme:</td>
|
||||
<td class="rechts">{{ format_float(calculate_total(invoice)) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="rechts">USt. ({{ format_float(invoice.Vat) }}%):</td>
|
||||
<td class="rechts">{{ format_float(calculate_total(invoice) * ((invoice.Vat / 100))) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="rechts bold overline">Gesamt Summe:</td>
|
||||
<td class="rechts bold overline">{{ format_float(calculate_total(invoice) * ((invoice.Vat / 100)+1)) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>{{ invoice.Footer | markdown_to_html | named_replace(ZahlungsZiel=(invoice.InvoiceDate |
|
||||
add_days(invoice.DueDate)).strftime('%d. %B %Y')) }}</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user