Amt-Blatt strikt vom Kundenblatt trennen
Auf dem Blatt fuers Amt taucht die Beherbergungssteuer nirgends mehr auf - auch nicht versteckt im Gesamtbetrag (der zeigt jetzt die Summe OHNE die Steuer, also genau die Bemessungsgrundlage). Die Zwischensumme entfaellt dort, sie waere betragsgleich. Ebenso raus: Bankverbindung und Steuernummer im Fuss, und das Wort "Beherbergungssteuer" in der Ueberschrift. Die Kundenseite bleibt vollstaendig: Steuer ausgewiesen, im Gesamtbetrag enthalten, Bankverbindung im Fuss - das ist die Rechnung, die der Gast bezahlt. Damit Kopf und Fuss die Seiten unterscheiden koennen, wird das PDF zweimal gebaut: der erste Lauf ermittelt nur, auf welcher Seite das Amt-Blatt beginnt. Auf "Seite 2 ist das Amt" zu wetten waere falsch, sobald die Kundenrechnung ueber eine Seite laeuft - dann stuende die Bankverbindung auf dem Amt-Blatt und fehlte beim Kunden.
This commit is contained in:
parent
5388203e56
commit
b5799a60a1
1 changed files with 96 additions and 50 deletions
146
pdf_renderer.py
146
pdf_renderer.py
|
|
@ -10,6 +10,7 @@ im Layout der Excel-Vorlage:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
|
|
@ -20,11 +21,30 @@ from reportlab.lib.enums import TA_RIGHT, TA_LEFT, TA_CENTER
|
||||||
from reportlab.lib.styles import ParagraphStyle
|
from reportlab.lib.styles import ParagraphStyle
|
||||||
from reportlab.platypus import (
|
from reportlab.platypus import (
|
||||||
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether, PageBreak,
|
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether, PageBreak,
|
||||||
|
Flowable,
|
||||||
)
|
)
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from berechnung import eur, eur_kurz, runde
|
from berechnung import eur, eur_kurz, runde
|
||||||
|
|
||||||
|
|
||||||
|
class _AmtMarke(Flowable):
|
||||||
|
"""Unsichtbar. Haelt fest, auf welcher Seite das Amt-Blatt beginnt.
|
||||||
|
|
||||||
|
Nur so kann die Fusszeile die Kundenseite vom Amt-Blatt unterscheiden - sie wird
|
||||||
|
gezeichnet, bevor der Seiteninhalt steht, und weiss von sich aus nichts davon.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, merker):
|
||||||
|
super().__init__()
|
||||||
|
self._merker = merker
|
||||||
|
|
||||||
|
def wrap(self, *_):
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
self._merker["seite"] = self.canv.getPageNumber()
|
||||||
|
|
||||||
# ---- Farben / Masse ---------------------------------------------------------
|
# ---- Farben / Masse ---------------------------------------------------------
|
||||||
ACCENT = colors.HexColor("#1f5c3d") # gedecktes Gruen (Camping)
|
ACCENT = colors.HexColor("#1f5c3d") # gedecktes Gruen (Camping)
|
||||||
GREY_BG = colors.HexColor("#eef1ee")
|
GREY_BG = colors.HexColor("#eef1ee")
|
||||||
|
|
@ -80,7 +100,8 @@ def _styles():
|
||||||
|
|
||||||
|
|
||||||
# ---- Kopf + Fusszeile (auf jeder Seite gezeichnet) --------------------------
|
# ---- Kopf + Fusszeile (auf jeder Seite gezeichnet) --------------------------
|
||||||
def _kopf_fuss(canvas, doc, cfg):
|
def _kopf_fuss(canvas, doc, cfg, amt=False):
|
||||||
|
"""Kopf und Fuss einer Seite. amt=True -> Blatt fuer das Amt (siehe Fusszeile)."""
|
||||||
firma = cfg["firma"]
|
firma = cfg["firma"]
|
||||||
canvas.saveState()
|
canvas.saveState()
|
||||||
|
|
||||||
|
|
@ -134,6 +155,13 @@ def _kopf_fuss(canvas, doc, cfg):
|
||||||
canvas.line(MARGIN_L, line_y, PAGE_W - MARGIN_R, line_y)
|
canvas.line(MARGIN_L, line_y, PAGE_W - MARGIN_R, line_y)
|
||||||
|
|
||||||
# --- Fusszeile ---
|
# --- Fusszeile ---
|
||||||
|
#
|
||||||
|
# STRIKTE TRENNUNG Kunde / Amt:
|
||||||
|
# Kundenseite : voller Briefkopf-Fuss - Bankverbindung und Steuernummer MUESSEN
|
||||||
|
# drauf, das ist die Rechnung, die der Gast bezahlt.
|
||||||
|
# Amt-Blatt : KEINE Bankverbindung, KEINE Steuernummer. Das Amt zieht nichts
|
||||||
|
# ein und ueberweist nichts. Es bleiben Inhaber (wer meldet) und
|
||||||
|
# die Kontaktdaten fuer Rueckfragen.
|
||||||
fy = MARGIN_BOT - 4 * mm
|
fy = MARGIN_BOT - 4 * mm
|
||||||
canvas.setStrokeColor(LINE)
|
canvas.setStrokeColor(LINE)
|
||||||
canvas.setLineWidth(0.6)
|
canvas.setLineWidth(0.6)
|
||||||
|
|
@ -143,9 +171,12 @@ def _kopf_fuss(canvas, doc, cfg):
|
||||||
col_l = MARGIN_L
|
col_l = MARGIN_L
|
||||||
col_m = MARGIN_L + CONTENT_W * 0.40
|
col_m = MARGIN_L + CONTENT_W * 0.40
|
||||||
col_r = PAGE_W - MARGIN_R
|
col_r = PAGE_W - MARGIN_R
|
||||||
rows_l = [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")]
|
|
||||||
rows_m = [firma.get("inhaber", ""), firma.get("steuernummer", "")]
|
rows_l = [] if amt else [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")]
|
||||||
|
rows_m = [firma.get("inhaber", "")] if amt else [firma.get("inhaber", ""),
|
||||||
|
firma.get("steuernummer", "")]
|
||||||
rows_r = [firma.get("homepage", ""), firma.get("email", ""), firma.get("telefon", "")]
|
rows_r = [firma.get("homepage", ""), firma.get("email", ""), firma.get("telefon", "")]
|
||||||
|
|
||||||
for i, t in enumerate(rows_l):
|
for i, t in enumerate(rows_l):
|
||||||
canvas.drawString(col_l, fy + 9 * mm - i * 3.6 * mm, t)
|
canvas.drawString(col_l, fy + 9 * mm - i * 3.6 * mm, t)
|
||||||
for i, t in enumerate(rows_m):
|
for i, t in enumerate(rows_m):
|
||||||
|
|
@ -206,7 +237,9 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
||||||
story.append(Spacer(1, 7 * mm))
|
story.append(Spacer(1, 7 * mm))
|
||||||
|
|
||||||
# Ueberschrift (auf dem Amt-Blatt mit Zusatz)
|
# Ueberschrift (auf dem Amt-Blatt mit Zusatz)
|
||||||
titel = "Rechnungsübersicht" + (" – Beherbergungssteuer (für das Amt)" if gesamtrechnung_betont else "")
|
# Das Wort "Beherbergungssteuer" steht auf dem Amt-Blatt nirgends mehr - auch nicht
|
||||||
|
# in der Ueberschrift. Der Zusatz sagt nur noch, fuer wen das Blatt ist.
|
||||||
|
titel = "Rechnungsübersicht" + (" (für das Amt)" if gesamtrechnung_betont else "")
|
||||||
story.append(Paragraph(titel, st["h"]))
|
story.append(Paragraph(titel, st["h"]))
|
||||||
|
|
||||||
# Positionstabelle
|
# Positionstabelle
|
||||||
|
|
@ -262,29 +295,32 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
||||||
Paragraph(f"{name} {b.satz} %:", st["info_l"]),
|
Paragraph(f"{name} {b.satz} %:", st["info_l"]),
|
||||||
Paragraph(eur(b.ust), st["cell_r"]),
|
Paragraph(eur(b.ust), st["cell_r"]),
|
||||||
])
|
])
|
||||||
gr_idx = None
|
# Auf dem Amt-Blatt taucht die Beherbergungssteuer NIRGENDS auf - weder als eigene
|
||||||
if rechnung.aufschlaege:
|
# Zeile, noch versteckt im Gesamtbetrag. Das Amt bekommt die Rechnung so, wie sie
|
||||||
gr_idx = len(summen_rows) # Zeilen-Index der Zwischensumme
|
# ohne die Steuer aussaehe: Netto, Umsatzsteuern, Gesamt. Genau dieser Gesamtbetrag
|
||||||
gr_l = st["total_l"] if gesamtrechnung_betont else st["info_l"]
|
# ist die Bemessungsgrundlage, aus der es die Steuer selbst errechnet.
|
||||||
gr_v = st["total_v"] if gesamtrechnung_betont else st["cell_r"]
|
#
|
||||||
|
# Deshalb entfaellt hier auch die Zwischensumme: sie waere betragsgleich mit dem
|
||||||
|
# Gesamtbetrag und wuerde nur zweimal dasselbe zeigen.
|
||||||
|
#
|
||||||
|
# Auf der Kundenseite bleibt alles wie gehabt - dort MUSS die Steuer ausgewiesen
|
||||||
|
# sein und im Gesamtbetrag stecken, das ist der Betrag, den der Gast zahlt.
|
||||||
|
if rechnung.aufschlaege and not gesamtrechnung_betont:
|
||||||
summen_rows.append([
|
summen_rows.append([
|
||||||
Paragraph("Zwischensumme:", gr_l),
|
Paragraph("Zwischensumme:", st["info_l"]),
|
||||||
Paragraph(eur(rechnung.brutto), gr_v),
|
Paragraph(eur(rechnung.brutto), st["cell_r"]),
|
||||||
])
|
])
|
||||||
# Auf dem Amt-Blatt stehen nur noch die Umsatzsteuern (7 % / 19 %). Die
|
for a in rechnung.aufschlaege:
|
||||||
# Beherbergungssteuer selbst wird dort NICHT ausgewiesen - so will es das Amt.
|
satz_txt = eur_kurz(a.satz).replace(",00", "")
|
||||||
# Es braucht nur die Zwischensumme als Bemessungsgrundlage und rechnet selbst.
|
summen_rows.append([
|
||||||
# Auf der Kundenseite bleibt die Zeile stehen, dort gehoert sie hin.
|
Paragraph(f"zzgl. {a.bezeichnung or 'Aufschlag'} ({satz_txt} %):", st["info_l"]),
|
||||||
if not gesamtrechnung_betont:
|
Paragraph(eur(a.betrag), st["cell_r"]),
|
||||||
for a in rechnung.aufschlaege:
|
])
|
||||||
satz_txt = eur_kurz(a.satz).replace(",00", "")
|
|
||||||
summen_rows.append([
|
endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt
|
||||||
Paragraph(f"zzgl. {a.bezeichnung or 'Aufschlag'} ({satz_txt} %):", st["info_l"]),
|
|
||||||
Paragraph(eur(a.betrag), st["cell_r"]),
|
|
||||||
])
|
|
||||||
summen_rows.append([
|
summen_rows.append([
|
||||||
Paragraph("Gesamtbetrag:", st["total_l"]),
|
Paragraph("Gesamtbetrag:", st["total_l"]),
|
||||||
Paragraph(eur(rechnung.gesamt), st["total_v"]),
|
Paragraph(eur(endsumme), st["total_v"]),
|
||||||
])
|
])
|
||||||
style_cmds = [
|
style_cmds = [
|
||||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
|
@ -294,13 +330,6 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
||||||
("LINEBELOW", (0, -1), (1, -1), 0.8, ACCENT),
|
("LINEBELOW", (0, -1), (1, -1), 0.8, ACCENT),
|
||||||
("TOPPADDING", (0, -1), (-1, -1), 4),
|
("TOPPADDING", (0, -1), (-1, -1), 4),
|
||||||
]
|
]
|
||||||
if gesamtrechnung_betont and gr_idx is not None:
|
|
||||||
# Zwischensumme wie der Gesamtbetrag hervorheben: zwei Streifen
|
|
||||||
style_cmds += [
|
|
||||||
("LINEABOVE", (0, gr_idx), (1, gr_idx), 0.8, ACCENT),
|
|
||||||
("LINEBELOW", (0, gr_idx), (1, gr_idx), 0.8, ACCENT),
|
|
||||||
("TOPPADDING", (0, gr_idx), (-1, gr_idx), 4),
|
|
||||||
]
|
|
||||||
summen = Table(summen_rows, colWidths=[CONTENT_W - 44 * mm, 44 * mm])
|
summen = Table(summen_rows, colWidths=[CONTENT_W - 44 * mm, 44 * mm])
|
||||||
summen.setStyle(TableStyle(style_cmds))
|
summen.setStyle(TableStyle(style_cmds))
|
||||||
story.append(summen)
|
story.append(summen)
|
||||||
|
|
@ -365,27 +394,44 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
|
||||||
kopf : dict mit rechnungsnummer, datum, anreise, abreise, naechte, kunde_zeilen
|
kopf : dict mit rechnungsnummer, datum, anreise, abreise, naechte, kunde_zeilen
|
||||||
|
|
||||||
EIN PDF mit zwei Blaettern (falls Beherbergungssteuer anfaellt):
|
EIN PDF mit zwei Blaettern (falls Beherbergungssteuer anfaellt):
|
||||||
Seite 1 = Kunden-Rechnung.
|
Seite 1 = Kunden-Rechnung, vollstaendig (Steuer ausgewiesen, Bankverbindung im Fuss).
|
||||||
Seite 2 = 1:1-Kopie fuers Amt, nur die Zwischensumme fett + zwei Streifen.
|
Letzte Seite = Blatt fuers Amt: KEINE Beherbergungssteuer (auch nicht im Gesamt),
|
||||||
|
keine Zwischensumme, keine Bankverbindung, keine Steuernummer.
|
||||||
"""
|
"""
|
||||||
st = _styles()
|
st = _styles()
|
||||||
doc = SimpleDocTemplate(
|
|
||||||
pfad, pagesize=A4,
|
|
||||||
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
|
|
||||||
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
|
||||||
title=f"Rechnung {kopf.get('rechnungsnummer','')}",
|
|
||||||
author=cfg["firma"].get("name", ""),
|
|
||||||
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
|
||||||
)
|
|
||||||
story = []
|
|
||||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Seite 1: Kunde
|
|
||||||
if rechnung.aufschlaege:
|
|
||||||
story.append(PageBreak())
|
|
||||||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Seite 2: Amt
|
|
||||||
|
|
||||||
doc.build(
|
def bauen(ziel, amt_seite):
|
||||||
story,
|
doc = SimpleDocTemplate(
|
||||||
onFirstPage=lambda c, d: _kopf_fuss(c, d, cfg),
|
ziel, pagesize=A4,
|
||||||
onLaterPages=lambda c, d: _kopf_fuss(c, d, cfg),
|
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
|
||||||
)
|
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
||||||
|
title=f"Rechnung {kopf.get('rechnungsnummer','')}",
|
||||||
|
author=cfg["firma"].get("name", ""),
|
||||||
|
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
||||||
|
)
|
||||||
|
story = []
|
||||||
|
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Kunde
|
||||||
|
if rechnung.aufschlaege:
|
||||||
|
story.append(PageBreak())
|
||||||
|
story.append(_AmtMarke(marke))
|
||||||
|
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
|
||||||
|
|
||||||
|
def seite(c, d):
|
||||||
|
_kopf_fuss(c, d, cfg, amt=(amt_seite is not None and c.getPageNumber() >= amt_seite))
|
||||||
|
|
||||||
|
doc.build(story, onFirstPage=seite, onLaterPages=seite)
|
||||||
|
|
||||||
|
# Zwei Durchlaeufe, und zwar aus einem konkreten Grund:
|
||||||
|
#
|
||||||
|
# Kopf und Fuss werden VOR dem Seiteninhalt gezeichnet - beim Zeichnen weiss die
|
||||||
|
# Seite also noch nicht, ob auf ihr gleich die Kunden- oder die Amts-Seite landet.
|
||||||
|
# Auf "Seite 2 ist das Amt" zu wetten, waere falsch, sobald die Kundenrechnung
|
||||||
|
# laenger als eine Seite wird: dann stuende die Bankverbindung ploetzlich auf dem
|
||||||
|
# Amt-Blatt und fehlte beim Kunden.
|
||||||
|
#
|
||||||
|
# Darum: erster Lauf ins Nichts, nur um zu erfahren, auf welcher Seite das
|
||||||
|
# Amt-Blatt beginnt. Zweiter Lauf schreibt die echte Datei.
|
||||||
|
marke = {}
|
||||||
|
bauen(io.BytesIO(), None)
|
||||||
|
bauen(pfad, marke.get("seite"))
|
||||||
return pfad
|
return pfad
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue