feature/tu/first_implementation (#2)
Co-authored-by: Torsten Ueberschar <torsten@ueberschar.de> Reviewed-on: torsten/freelance_invoice#2
This commit is contained in:
171
.gitignore
vendored
171
.gitignore
vendored
@@ -1,162 +1,19 @@
|
|||||||
# ---> Python
|
.idea
|
||||||
# Byte-compiled / optimized / DLL files
|
.code
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
# python
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
# Virtualenv
|
||||||
.Python
|
.Python
|
||||||
build/
|
__pycache__
|
||||||
develop-eggs/
|
[Bb]in
|
||||||
dist/
|
[Ii]nclude
|
||||||
downloads/
|
[Ll]ib
|
||||||
eggs/
|
[Ll]ib64
|
||||||
.eggs/
|
[Ll]ocal
|
||||||
lib/
|
[Ss]cripts
|
||||||
lib64/
|
pyvenv.cfg
|
||||||
parts/
|
venv
|
||||||
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
|
.venv
|
||||||
env/
|
pip-selfcheck.json
|
||||||
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/
|
|
||||||
|
|
||||||
|
|||||||
97
README.md
97
README.md
@@ -1,2 +1,97 @@
|
|||||||
# 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`.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Use python venv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Build executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade build
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
See where it is installed:
|
||||||
|
```bash
|
||||||
|
which invoice
|
||||||
|
```
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
invoice -h
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
There are two yaml files describing your invoice:
|
||||||
|
|
||||||
|
- [RG004211.yaml](test_data/RG004211.yaml) Contains the invoice data. The filename is takes as the invoice id-number.
|
||||||
|
- [envelope.yaml](test_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
|
||||||
|
|
||||||
|
|
||||||
|
```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
|
||||||
|
invoice -b test_data -i invoice.yaml -e envelope.yaml -t test_data/templates
|
||||||
|
```
|
||||||
|
|
||||||
|
## to do
|
||||||
|
|
||||||
|
- give more structure to code
|
||||||
|
- make code testable
|
||||||
|
|
||||||
|
## see also
|
||||||
|
|
||||||
|
- https://xhtml2pdf.readthedocs.io/en/latest/index.html
|
||||||
|
- https://jinja.palletsprojects.com/
|
||||||
|
|
||||||
|
|||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[project]
|
||||||
|
name = "simple-invoice-generator"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Simple invoice generator for freelancers and small businesses"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [{name = "Torsten Ueberschar", email = "tu@uesome.de"}]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers, Freelancers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
'PyYAML',
|
||||||
|
'Jinja2',
|
||||||
|
'xhtml2pdf',
|
||||||
|
'Markdown'
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
invoice = "main:main"
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Jinja2==3.1.3
|
||||||
|
Markdown==3.5.2
|
||||||
|
PyYAML==6.0.1
|
||||||
|
xhtml2pdf==0.2.15
|
||||||
0
src/invoice_generator/__init__.py
Normal file
0
src/invoice_generator/__init__.py
Normal file
72
src/invoice_generator/html_generator.py
Normal file
72
src/invoice_generator/html_generator.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
self.path_to_template = Path(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']) 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
|
||||||
|
|
||||||
|
def convert_html_to_pdf(self, 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
|
||||||
|
path=str(self.path_to_template / 'fonts'),
|
||||||
|
dest=result_file
|
||||||
|
) # file handle to receive result
|
||||||
|
|
||||||
|
# close output file
|
||||||
|
result_file.close() # close output file
|
||||||
|
|
||||||
|
# return False on success and True on errors
|
||||||
|
return pisa_status.err
|
||||||
156
src/main.py
Normal file
156
src/main.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import argparse
|
||||||
|
import yaml
|
||||||
|
import locale
|
||||||
|
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
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', encoding='utf-8') 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')
|
||||||
|
|
||||||
|
cwd = Path.cwd().resolve()
|
||||||
|
|
||||||
|
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=False, default=cwd,
|
||||||
|
help='base directory for invoice and envelope files (default: (current working directory) %(default)s)')
|
||||||
|
parser.add_argument('-e', '--envelope', type=str, required=False, default='envelope.yaml',
|
||||||
|
help='Envelope file name (default: %(default)s)')
|
||||||
|
parser.add_argument('-t', '--template', type=str, required=False, default='templates',
|
||||||
|
help='directory for template files (default: %(default)s)')
|
||||||
|
parser.add_argument('-i', '--invoice', type=str, required=True, help='Invoice file name')
|
||||||
|
|
||||||
|
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('<--->')
|
||||||
|
print(yaml.dump(envelope_data))
|
||||||
|
print('</--->')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not hasattr(invoice_data, 'Positions'):
|
||||||
|
invoice_data.Positions = []
|
||||||
|
|
||||||
|
for position in invoice_data.Positions:
|
||||||
|
if 'PricePerUnit' not in position or position['PricePerUnit'] is None:
|
||||||
|
position['PricePerUnit'] = invoice_data.PricePerUnit
|
||||||
|
|
||||||
|
invoice_data.Id = invoice_data.Id or invoice_file.stem
|
||||||
|
print('<--->')
|
||||||
|
print(yaml.dump(invoice_data))
|
||||||
|
print('</--->')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not PurePath(args.template).is_absolute():
|
||||||
|
template_dir = (Path(args.base) / args.template).resolve()
|
||||||
|
else:
|
||||||
|
template_dir = Path(args.template).resolve()
|
||||||
|
|
||||||
|
generator = html_generator.HtmlTemplate(template_dir)
|
||||||
|
template = generator.prepare_template(invoice_data, envelope_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print('Generating invoice...')
|
||||||
|
output_path = Path(args.base) / 'pdf'
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = output_path / f'{invoice_data.Id}.pdf'
|
||||||
|
invoice_pdf = output_path.resolve()
|
||||||
|
print(f'Invoice PDF: {invoice_pdf}')
|
||||||
|
generator.convert_html_to_pdf(template, invoice_pdf)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error: {e}')
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
20
test_data/RG004211.yaml
Normal file
20
test_data/RG004211.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#Id: RG004712
|
||||||
|
CustomerId: KD01234
|
||||||
|
#InvoiceDate: 2023-12-23
|
||||||
|
|
||||||
|
Positions:
|
||||||
|
- Title: "Zuckerwatte essen"
|
||||||
|
SubTitle: "Leistungszeitraum: 11/2022"
|
||||||
|
PricePerUnit: 100
|
||||||
|
Quantity: 100
|
||||||
|
|
||||||
|
- Title: "Sinnloses Zeug"
|
||||||
|
PricePerUnit: 99.99
|
||||||
|
Quantity: 12
|
||||||
|
|
||||||
|
- Title: "Irgend was anderes"
|
||||||
|
SubTitle: "Leistungszeitraum: 10/2022"
|
||||||
|
Quantity: 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
47
test_data/envelope.yaml
Normal file
47
test_data/envelope.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
AddressContent:
|
||||||
|
LogoFile: Images/logo.svg
|
||||||
|
AddressBoxSender: "Abs.: Klaus Kleinkariert, Musterstraße 1, 12345 Musterhausen"
|
||||||
|
Contents:
|
||||||
|
- Text: |
|
||||||
|

|
||||||
|
Klaus Kleinkariert
|
||||||
|
Musterstraße 1
|
||||||
|
12345 Musterhausen
|
||||||
|
- Text: |
|
||||||
|
**Kontakt**
|
||||||
|
klaus@klaus-superheld.de
|
||||||
|
+49 800 12345678
|
||||||
|
- Text: |
|
||||||
|
**Steuern**
|
||||||
|
USt-IdNr.: DE123456789
|
||||||
|
- Text: |
|
||||||
|
**Bankverbindung**
|
||||||
|
Klaus Kleinkariert
|
||||||
|
DE12 1234 1234 1234 1234 12
|
||||||
|
ABCDTTCXXX
|
||||||
|
Geiz 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
|
||||||
|
|
||||||
|
*Klaus Musterhausen*
|
||||||
|
|
||||||
|
Customers:
|
||||||
|
- CustomerId: KD01234
|
||||||
|
PricePerUnit: 43.50
|
||||||
|
DueDate: 14
|
||||||
|
AddressField: |
|
||||||
|
Beispiel GmbH
|
||||||
|
Beispielstraße 12
|
||||||
|
12345 Musterhausen
|
||||||
417
test_data/pdf/RG004211.pdf
Normal file
417
test_data/pdf/RG004211.pdf
Normal file
File diff suppressed because one or more lines are too long
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.
BIN
test_data/templates/images/logo.png
Normal file
BIN
test_data/templates/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
227
test_data/templates/invoice.html
Normal file
227
test_data/templates/invoice.html
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "Barlow Semi Condensed";
|
||||||
|
src: url("fonts/BarlowSemiCondensed-Regular.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Barlow Semi Condensed";
|
||||||
|
src: url("fonts/BarlowSemiCondensed-Light.ttf");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Barlow Semi Condensed";
|
||||||
|
src: url("fonts/BarlowSemiCondensed-LightItalic.ttf");
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Barlow Semi Condensed";
|
||||||
|
src: url("fonts/BarlowSemiCondensed-Bold.ttf");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Barlow Semi Condensed";
|
||||||
|
src: url("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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
#address_frame_content {
|
||||||
|
}
|
||||||
|
|
||||||
|
#letter_head_content {
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionen {
|
||||||
|
padding-top: 1pt;
|
||||||
|
padding-bottom: 1pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_data {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_data td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#letter_head_content img {
|
||||||
|
display: block;
|
||||||
|
margin-left: -.55cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position_top {
|
||||||
|
padding-top: 1pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position_bottom {
|
||||||
|
padding-bottom: 1pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Content for Static Frame 'header_frame' -->
|
||||||
|
<div id="address_frame_content">
|
||||||
|
<p class="underline small center">{{ 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 %}
|
||||||
|
{{ address.Text | markdown_to_html }}
|
||||||
|
{% 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>
|
||||||
|
<td class="links position_top">{{ position.Title }}</td>
|
||||||
|
<td class="rechts" rowspan="2">{{ format_float(position.Quantity | float) }}</td>
|
||||||
|
<td class="rechts" rowspan="2">{{ format_float((position.PricePerUnit or invoice.PricePerUnit) | float) }}
|
||||||
|
</td>
|
||||||
|
<td class="rechts" rowspan="2">{{ format_float(position.Quantity * ( position.PricePerUnit or
|
||||||
|
invoice.PricePerUnit ))}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="links small position_bottom">{{ position.SubTitle }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="summe">
|
||||||
|
<td></td>
|
||||||
|
<td colspan="2" class="rechts">Nettosumme:</td>
|
||||||
|
<td class="rechts">{{ format_float(calculate_total(invoice)) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td colspan="2" 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 colspan="2" 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