# -*- coding: utf-8 -*- """ PDF-Bericht fürs Amt – im gleichen Layout wie die Rechnungen des Rechnungstools. Firmenkopf (Logo) -> Titel + Zeitraum -> Tabelle der Rechnungen -> Summenblock (Steuer hervorgehoben) -> Unterschriftszeile -> Bankfußzeile render_monat(pfad, firma, jahr, monat, buchungen) render_jahr (pfad, firma, jahr, buchungen) – ein Blatt mit allen 12 Monaten """ import os import logging from datetime import date from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm from reportlab.lib.utils import ImageReader from reportlab.lib import colors from reportlab.lib.enums import TA_RIGHT, TA_LEFT, TA_CENTER from reportlab.lib.styles import ParagraphStyle from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from modell import MONATE, summiere, eur, eur_kurz log = logging.getLogger("bst.bericht") # Farben wie in der Rechnung ACCENT = colors.HexColor("#1f5c3d") GREY_BG = colors.HexColor("#eef1ee") LINE = colors.HexColor("#9aa39c") DARK = colors.HexColor("#222222") MARGIN_L = MARGIN_R = 18 * mm MARGIN_TOP = 40 * mm MARGIN_BOT = 34 * mm PAGE_W, PAGE_H = A4 CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R def _styles(): return { "zelle": ParagraphStyle("zelle", fontName="Helvetica", fontSize=8.5, leading=10.5, textColor=DARK, alignment=TA_LEFT), "zelle_r": ParagraphStyle("zelle_r", fontName="Helvetica", fontSize=8.5, leading=10.5, textColor=DARK, alignment=TA_RIGHT), "zelle_c": ParagraphStyle("zelle_c", fontName="Helvetica", fontSize=8.5, leading=10.5, textColor=DARK, alignment=TA_CENTER), "th": ParagraphStyle("th", fontName="Helvetica-Bold", fontSize=8, leading=9.3, textColor=DARK, alignment=TA_CENTER), "th_l": ParagraphStyle("th_l", fontName="Helvetica-Bold", fontSize=8, leading=9.3, textColor=DARK, alignment=TA_LEFT), "h2": ParagraphStyle("h2", fontName="Helvetica-Bold", fontSize=15, leading=19, textColor=ACCENT, spaceAfter=2), "unter": ParagraphStyle("unter", fontName="Helvetica", fontSize=9.5, leading=13, textColor=DARK, spaceAfter=6), "sum_l": ParagraphStyle("sum_l", fontName="Helvetica-Bold", fontSize=9, leading=13, textColor=DARK, alignment=TA_RIGHT), "sum_r": ParagraphStyle("sum_r", fontName="Helvetica", fontSize=9, leading=13, textColor=DARK, alignment=TA_RIGHT), "total_l": ParagraphStyle("total_l", fontName="Helvetica-Bold", fontSize=10.5, leading=14, textColor=DARK, alignment=TA_RIGHT), "total_v": ParagraphStyle("total_v", fontName="Helvetica-Bold", fontSize=10.5, leading=14, textColor=ACCENT, alignment=TA_RIGHT), "klein": ParagraphStyle("klein", fontName="Helvetica", fontSize=8.5, leading=11, textColor=DARK), } def _kopf_fuss(canvas, doc, firma): canvas.saveState() line_y = PAGE_H - 25 * mm logo = (firma.get("logo") or "").strip() logo_ok = False if logo and os.path.exists(logo): try: img = ImageReader(logo) iw, ih = img.getSize() scale = min(60 * mm / iw, 20 * mm / ih) dw, dh = iw * scale, ih * scale ly = PAGE_H - 9 * mm - dh canvas.drawImage(img, MARGIN_L, ly, width=dw, height=dh, preserveAspectRatio=True, mask="auto") logo_ok = True line_y = ly - 3 * mm except Exception as e: # noqa: BLE001 log.warning("Logo nicht ladbar (%s): %s", logo, e) if logo_ok: top = PAGE_H - 15 * mm canvas.setFillColor(ACCENT) canvas.setFont("Helvetica-Bold", 16) canvas.drawRightString(PAGE_W - MARGIN_R, top, firma.get("name", "")) canvas.setFillColor(DARK) canvas.setFont("Helvetica-Oblique", 9.5) canvas.drawRightString(PAGE_W - MARGIN_R, top - 5.5 * mm, firma.get("untertitel", "")) else: top = PAGE_H - 16 * mm canvas.setFillColor(ACCENT) canvas.setFont("Helvetica-Bold", 19) canvas.drawString(MARGIN_L, top, firma.get("name", "")) canvas.setFillColor(DARK) canvas.setFont("Helvetica-Oblique", 10.5) canvas.drawString(MARGIN_L, top - 6.2 * mm, firma.get("untertitel", "")) line_y = top - 9.5 * mm canvas.setStrokeColor(ACCENT) canvas.setLineWidth(1.1) canvas.line(MARGIN_L, line_y, PAGE_W - MARGIN_R, line_y) # Fußzeile fy = MARGIN_BOT - 4 * mm canvas.setStrokeColor(LINE) canvas.setLineWidth(0.6) canvas.line(MARGIN_L, fy + 13 * mm, PAGE_W - MARGIN_R, fy + 13 * mm) canvas.setFont("Helvetica", 7.3) canvas.setFillColor(DARK) links = [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")] mitte = [firma.get("inhaber", ""), firma.get("steuernummer", "")] rechts = [firma.get("homepage", ""), firma.get("email", ""), firma.get("telefon", "")] for i, t in enumerate(links): canvas.drawString(MARGIN_L, fy + 9 * mm - i * 3.6 * mm, t) for i, t in enumerate(mitte): canvas.drawString(MARGIN_L + CONTENT_W * 0.40, fy + 9 * mm - i * 3.6 * mm, t) for i, t in enumerate(rechts): canvas.drawRightString(PAGE_W - MARGIN_R, fy + 9 * mm - i * 3.6 * mm, t) canvas.setFont("Helvetica", 7.3) canvas.drawCentredString(PAGE_W / 2, fy + 1 * mm, f"Seite {canvas.getPageNumber()}") canvas.restoreState() def _titel(story, st, firma, titel, zeitraum): story.append(Paragraph( f"{firma.get('absender','')}", ParagraphStyle("abs", fontName="Helvetica", fontSize=7.5, textColor=DARK))) story.append(Spacer(1, 6 * mm)) story.append(Paragraph(titel, st["h2"])) story.append(Paragraph(zeitraum, st["unter"])) story.append(Spacer(1, 2 * mm)) def _summenblock(story, st, s, satz_text=""): zeilen = [ [Paragraph("Rechnungen:", st["sum_l"]), Paragraph(str(s.buchungen), st["sum_r"])], [Paragraph("Übernachtungen:", st["sum_l"]), Paragraph(str(s.naechte), st["sum_r"])], [Paragraph("Steuerpflichtiges Entgelt:", st["sum_l"]), Paragraph(eur(s.gezahlt), st["sum_r"])], [Paragraph(f"Beherbergungssteuer{satz_text}:", st["total_l"]), Paragraph(eur(s.steuer), st["total_v"])], ] t = Table(zeilen, colWidths=[CONTENT_W - 44 * mm, 44 * mm]) t.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 1.5), ("BOTTOMPADDING", (0, 0), (-1, -1), 1.5), ("LINEABOVE", (0, -1), (1, -1), 0.8, ACCENT), ("LINEBELOW", (0, -1), (1, -1), 0.8, ACCENT), ("TOPPADDING", (0, -1), (-1, -1), 4), ])) story.append(Spacer(1, 4 * mm)) story.append(t) def _unterschrift(story, st, ort): story.append(Spacer(1, 14 * mm)) heute = date.today().strftime("%d.%m.%Y") t = Table([[Paragraph(f"{ort}, den {heute}", st["klein"]), Paragraph("Unterschrift / Stempel", st["klein"])]], colWidths=[CONTENT_W * 0.5, CONTENT_W * 0.5]) t.setStyle(TableStyle([ ("LINEABOVE", (0, 0), (0, 0), 0.6, LINE), ("LINEABOVE", (1, 0), (1, 0), 0.6, LINE), ("TOPPADDING", (0, 0), (-1, -1), 2), ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (1, 0), (1, 0), 0), ])) story.append(t) def _dokument(pfad, firma, titel): return SimpleDocTemplate( pfad, pagesize=A4, leftMargin=MARGIN_L, rightMargin=MARGIN_R, topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT, title=titel, author=firma.get("name", "")) def _ort(firma): """Ort aus der Absenderzeile ziehen ('… - 04416 Markkleeberg OT Wachau').""" teile = [t.strip() for t in str(firma.get("absender", "")).split("-")] if teile and teile[-1]: worte = teile[-1].split() if worte and worte[0].isdigit(): worte = worte[1:] # Postleitzahl weg return " ".join(worte) or "Markkleeberg" return "Markkleeberg" # ------------------------------------------------------------- Monat -------- def render_monat(pfad, firma, jahr: int, monat: int, buchungen): st = _styles() liste = sorted([b for b in buchungen if b.jahr == jahr and b.monat == monat], key=lambda b: (b.datum, b.rechnungsnummer)) s = summiere(liste) saetze = {b.satz for b in liste} satz_text = f" ({eur_kurz(list(saetze)[0]).replace(',00','')} %)" if len(saetze) == 1 else "" story = [] _titel(story, st, firma, "Beherbergungssteuer – Steueranmeldung", f"Zeitraum: {MONATE[monat - 1]} {jahr}") kopf = ["Datum", "Rechnung", "Name", "Nächte", "Entgelt", "Satz %", "Steuer"] stile = ["th", "th", "th_l", "th", "th", "th", "th"] daten = [[Paragraph(k, st[s_]) for k, s_ in zip(kopf, stile)]] for b in liste: daten.append([ Paragraph(b.datum.strftime("%d.%m.%Y"), st["zelle_c"]), Paragraph(b.voll_nummer, st["zelle_c"]), Paragraph(b.nachname, st["zelle"]), Paragraph(str(b.naechte), st["zelle_c"]), Paragraph(eur_kurz(b.gezahlt), st["zelle_r"]), Paragraph(eur_kurz(b.satz).replace(",00", ""), st["zelle_c"]), Paragraph(eur_kurz(b.steuer), st["zelle_r"]), ]) if len(daten) == 1: daten.append([Paragraph("", st["zelle_c"]), Paragraph("(keine Buchungen in diesem Monat)", st["zelle"]), "", "", "", "", ""]) # Namensspalte nur so breit wie der breiteste Name (Rest der Tabelle ist fest) breiteste = max([stringWidth(b.nachname, "Helvetica", 8.5) for b in liste] + [stringWidth("Name", "Helvetica-Bold", 8)]) name_w = min(max(breiteste + 6 * mm, 25 * mm), CONTENT_W - 120 * mm) tabelle = Table(daten, repeatRows=1, hAlign="LEFT", colWidths=[22 * mm, 22 * mm, name_w, 15 * mm, 22 * mm, 14 * mm, 25 * mm]) tabelle.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), GREY_BG), ("LINEBELOW", (0, 0), (-1, 0), 0.8, ACCENT), ("LINEBELOW", (0, 1), (-1, -1), 0.35, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3), ])) story.append(tabelle) _summenblock(story, st, s, satz_text) story.append(Spacer(1, 3 * mm)) story.append(Paragraph( "Die Beherbergungssteuer wird gemäß kommunaler Satzung auf das steuerpflichtige " "Entgelt der Beherbergungsleistungen erhoben und mit den Gästen abgerechnet.", st["klein"])) _unterschrift(story, st, _ort(firma)) _dokument(pfad, firma, f"Beherbergungssteuer {MONATE[monat - 1]} {jahr}").build( story, onFirstPage=lambda c, d: _kopf_fuss(c, d, firma), onLaterPages=lambda c, d: _kopf_fuss(c, d, firma)) log.info("Monatsbericht %s: %d Buchungen, Steuer %.2f €", pfad, s.buchungen, s.steuer) return pfad # -------------------------------------------------------------- Jahr -------- def render_jahr(pfad, firma, jahr: int, buchungen): st = _styles() liste = [b for b in buchungen if b.jahr == jahr] gesamt = summiere(liste) story = [] _titel(story, st, firma, "Beherbergungssteuer – Jahresübersicht", f"Zeitraum: Jahr {jahr}") kopf = ["Monat", "Rechnungen", "Übernachtungen", "Entgelt", "Steuer"] daten = [[Paragraph(k, st["th_l"] if i == 0 else st["th"]) for i, k in enumerate(kopf)]] for m in range(1, 13): s = summiere([b for b in liste if b.monat == m]) daten.append([ Paragraph(MONATE[m - 1], st["zelle"]), Paragraph(str(s.buchungen), st["zelle_c"]), Paragraph(str(s.naechte), st["zelle_c"]), Paragraph(eur_kurz(s.gezahlt), st["zelle_r"]), Paragraph(eur_kurz(s.steuer), st["zelle_r"]), ]) daten.append([ Paragraph("Summe", st["total_l"]), Paragraph(str(gesamt.buchungen), st["total_v"]), Paragraph(str(gesamt.naechte), st["total_v"]), Paragraph(eur_kurz(gesamt.gezahlt), st["total_v"]), Paragraph(eur_kurz(gesamt.steuer), st["total_v"]), ]) tabelle = Table(daten, repeatRows=1, colWidths=[40 * mm, 28 * mm, 34 * mm, 34 * mm, 38 * mm]) tabelle.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), GREY_BG), ("LINEBELOW", (0, 0), (-1, 0), 0.8, ACCENT), ("LINEBELOW", (0, 1), (-1, -2), 0.35, LINE), ("LINEABOVE", (0, -1), (-1, -1), 0.8, ACCENT), ("LINEBELOW", (0, -1), (-1, -1), 0.8, ACCENT), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 3.5), ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5), ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3), ])) story.append(tabelle) _unterschrift(story, st, _ort(firma)) _dokument(pfad, firma, f"Beherbergungssteuer Jahresübersicht {jahr}").build( story, onFirstPage=lambda c, d: _kopf_fuss(c, d, firma), onLaterPages=lambda c, d: _kopf_fuss(c, d, firma)) log.info("Jahresbericht %s: %d Buchungen, Steuer %.2f €", pfad, gesamt.buchungen, gesamt.steuer) return pfad