feature/tu/first_implementation (#2)
Co-authored-by: Torsten Ueberschar <torsten@ueberschar.de> Reviewed-on: torsten/freelance_invoice#2
This commit is contained in:
+14
-157
@@ -1,162 +1,19 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.idea
|
||||
.code
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# python
|
||||
|
||||
# Distribution / packaging
|
||||
# Virtualenv
|
||||
.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
|
||||
__pycache__
|
||||
[Bb]in
|
||||
[Ii]nclude
|
||||
[Ll]ib
|
||||
[Ll]ib64
|
||||
[Ll]ocal
|
||||
[Ss]cripts
|
||||
pyvenv.cfg
|
||||
venv
|
||||
.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/
|
||||
pip-selfcheck.json
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
Jinja2==3.1.3
|
||||
Markdown==3.5.2
|
||||
PyYAML==6.0.1
|
||||
xhtml2pdf==0.2.15
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -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