diff --git a/pdf_renderer.py b/pdf_renderer.py index 9641cc4..c554322 100644 --- a/pdf_renderer.py +++ b/pdf_renderer.py @@ -10,6 +10,7 @@ im Layout der Excel-Vorlage: """ import os +import io import json 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.platypus import ( SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether, PageBreak, + Flowable, ) import config 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 --------------------------------------------------------- ACCENT = colors.HexColor("#1f5c3d") # gedecktes Gruen (Camping) GREY_BG = colors.HexColor("#eef1ee") @@ -80,7 +100,8 @@ def _styles(): # ---- 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"] canvas.saveState() @@ -134,6 +155,13 @@ def _kopf_fuss(canvas, doc, cfg): canvas.line(MARGIN_L, line_y, PAGE_W - MARGIN_R, line_y) # --- 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 canvas.setStrokeColor(LINE) canvas.setLineWidth(0.6) @@ -143,9 +171,12 @@ def _kopf_fuss(canvas, doc, cfg): col_l = MARGIN_L col_m = MARGIN_L + CONTENT_W * 0.40 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", "")] + for i, t in enumerate(rows_l): canvas.drawString(col_l, fy + 9 * mm - i * 3.6 * mm, t) 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)) # 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"])) # Positionstabelle @@ -262,29 +295,32 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont): Paragraph(f"{name} {b.satz} %:", st["info_l"]), Paragraph(eur(b.ust), st["cell_r"]), ]) - gr_idx = None - if rechnung.aufschlaege: - gr_idx = len(summen_rows) # Zeilen-Index der Zwischensumme - gr_l = st["total_l"] if gesamtrechnung_betont else st["info_l"] - gr_v = st["total_v"] if gesamtrechnung_betont else st["cell_r"] + # Auf dem Amt-Blatt taucht die Beherbergungssteuer NIRGENDS auf - weder als eigene + # Zeile, noch versteckt im Gesamtbetrag. Das Amt bekommt die Rechnung so, wie sie + # ohne die Steuer aussaehe: Netto, Umsatzsteuern, Gesamt. Genau dieser Gesamtbetrag + # ist die Bemessungsgrundlage, aus der es die Steuer selbst errechnet. + # + # 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([ - Paragraph("Zwischensumme:", gr_l), - Paragraph(eur(rechnung.brutto), gr_v), + Paragraph("Zwischensumme:", st["info_l"]), + Paragraph(eur(rechnung.brutto), st["cell_r"]), ]) - # Auf dem Amt-Blatt stehen nur noch die Umsatzsteuern (7 % / 19 %). Die - # Beherbergungssteuer selbst wird dort NICHT ausgewiesen - so will es das Amt. - # Es braucht nur die Zwischensumme als Bemessungsgrundlage und rechnet selbst. - # Auf der Kundenseite bleibt die Zeile stehen, dort gehoert sie hin. - if not gesamtrechnung_betont: - for a in rechnung.aufschlaege: - satz_txt = eur_kurz(a.satz).replace(",00", "") - summen_rows.append([ - Paragraph(f"zzgl. {a.bezeichnung or 'Aufschlag'} ({satz_txt} %):", st["info_l"]), - Paragraph(eur(a.betrag), st["cell_r"]), - ]) + for a in rechnung.aufschlaege: + satz_txt = eur_kurz(a.satz).replace(",00", "") + summen_rows.append([ + Paragraph(f"zzgl. {a.bezeichnung or 'Aufschlag'} ({satz_txt} %):", st["info_l"]), + Paragraph(eur(a.betrag), st["cell_r"]), + ]) + + endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt summen_rows.append([ Paragraph("Gesamtbetrag:", st["total_l"]), - Paragraph(eur(rechnung.gesamt), st["total_v"]), + Paragraph(eur(endsumme), st["total_v"]), ]) style_cmds = [ ("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), ("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.setStyle(TableStyle(style_cmds)) story.append(summen) @@ -365,27 +394,44 @@ def render_rechnung(pfad, cfg, rechnung, kopf): kopf : dict mit rechnungsnummer, datum, anreise, abreise, naechte, kunde_zeilen EIN PDF mit zwei Blaettern (falls Beherbergungssteuer anfaellt): - Seite 1 = Kunden-Rechnung. - Seite 2 = 1:1-Kopie fuers Amt, nur die Zwischensumme fett + zwei Streifen. + Seite 1 = Kunden-Rechnung, vollstaendig (Steuer ausgewiesen, Bankverbindung im Fuss). + Letzte Seite = Blatt fuers Amt: KEINE Beherbergungssteuer (auch nicht im Gesamt), + keine Zwischensumme, keine Bankverbindung, keine Steuernummer. """ 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( - story, - onFirstPage=lambda c, d: _kopf_fuss(c, d, cfg), - onLaterPages=lambda c, d: _kopf_fuss(c, d, cfg), - ) + def bauen(ziel, amt_seite): + doc = SimpleDocTemplate( + ziel, 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) # 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