diff --git a/pdf_renderer.py b/pdf_renderer.py index c554322..9641cc4 100644 --- a/pdf_renderer.py +++ b/pdf_renderer.py @@ -10,7 +10,6 @@ im Layout der Excel-Vorlage: """ import os -import io import json from reportlab.lib.pagesizes import A4 @@ -21,30 +20,11 @@ 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") @@ -100,8 +80,7 @@ def _styles(): # ---- Kopf + Fusszeile (auf jeder Seite gezeichnet) -------------------------- -def _kopf_fuss(canvas, doc, cfg, amt=False): - """Kopf und Fuss einer Seite. amt=True -> Blatt fuer das Amt (siehe Fusszeile).""" +def _kopf_fuss(canvas, doc, cfg): firma = cfg["firma"] canvas.saveState() @@ -155,13 +134,6 @@ def _kopf_fuss(canvas, doc, cfg, amt=False): 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) @@ -171,12 +143,9 @@ def _kopf_fuss(canvas, doc, cfg, amt=False): col_l = MARGIN_L col_m = MARGIN_L + CONTENT_W * 0.40 col_r = PAGE_W - MARGIN_R - - 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_l = [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")] + rows_m = [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): @@ -237,9 +206,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont): story.append(Spacer(1, 7 * mm)) # Ueberschrift (auf dem Amt-Blatt mit Zusatz) - # 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 "") + titel = "Rechnungsübersicht" + (" – Beherbergungssteuer (für das Amt)" if gesamtrechnung_betont else "") story.append(Paragraph(titel, st["h"])) # Positionstabelle @@ -295,32 +262,29 @@ 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"]), ]) - # 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: + 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"] summen_rows.append([ - Paragraph("Zwischensumme:", st["info_l"]), - Paragraph(eur(rechnung.brutto), st["cell_r"]), + Paragraph("Zwischensumme:", gr_l), + Paragraph(eur(rechnung.brutto), gr_v), ]) - 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 + # 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"]), + ]) summen_rows.append([ Paragraph("Gesamtbetrag:", st["total_l"]), - Paragraph(eur(endsumme), st["total_v"]), + Paragraph(eur(rechnung.gesamt), st["total_v"]), ]) style_cmds = [ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), @@ -330,6 +294,13 @@ 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) @@ -394,44 +365,27 @@ 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, vollstaendig (Steuer ausgewiesen, Bankverbindung im Fuss). - Letzte Seite = Blatt fuers Amt: KEINE Beherbergungssteuer (auch nicht im Gesamt), - keine Zwischensumme, keine Bankverbindung, keine Steuernummer. + Seite 1 = Kunden-Rechnung. + Seite 2 = 1:1-Kopie fuers Amt, nur die Zwischensumme fett + zwei Streifen. """ 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 - 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")) + doc.build( + story, + onFirstPage=lambda c, d: _kopf_fuss(c, d, cfg), + onLaterPages=lambda c, d: _kopf_fuss(c, d, cfg), + ) return pfad