Sein Einwand: "du musst die markieren als rechnung und storno oder berichtigung, und das kannst einfach mit buchstaben machen: r fuer rechnung, s fuer storno, b fuer berichtigung. am footer kannst eine erklaerung fuer die buchstaben machen." Er hat recht - ohne die Marke muss man aus der Begruendung raten, was fuer ein Beleg die Zeile ueberhaupt ist. Mit dem Buchstaben liest sich die Kette von selbst: 2026-900 R storniert mit 2026-901 2026-901 S neue Rechnung 2026-902 2026-902 B Neuausstellung 2026-911 S abgerechnet mit 2026-905 (Doppelungsfall) Unter der Tabelle steht die Legende: "R = Rechnung, S = Stornorechnung, B = berichtigte Rechnung (Neuausstellung nach einem Storno)". Bewusst direkt unter die Tabelle und nicht in die Fusszeile - wer eine einzelne Zeile prueft, hat sie dann vor Augen und muss nicht blaettern. Dazu "neu abgerechnet mit" auf "abgerechnet mit" gekuerzt: mit dem S davor ist das "neu" ueberfluessig, und die Zeile bricht nicht mehr um. Spaltenbreiten neu verteilt (18/18/9/42/Rest/12/18/9/18 mm). Erklaerungs-PDF um die Art-Spalte ergaenzt. Pruefkette gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
391 lines
17 KiB
Python
391 lines
17 KiB
Python
# -*- 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, aufhebungen, 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: nur die Seitenzahl.
|
||
#
|
||
# Hier stand vorher der komplette Briefkopf-Fuß – Bankverbindung, Steuernummer,
|
||
# Inhaber, Homepage, E-Mail, Telefon. Zwei Gründe, warum das weg ist:
|
||
#
|
||
# 1. Es ist eine Meldung, keine Rechnung. Das Amt zieht nichts ein und überweist
|
||
# nichts; Bankdaten und Steuernummer haben darin nichts verloren. Auf der
|
||
# Rechnung (Rechnungstool) bleiben sie, dort werden sie gebraucht.
|
||
# 2. Der Block lag ÜBER dem unteren Seitenrand (bottomMargin) und ragte damit in
|
||
# die Tabelle hinein – auf vollen Seiten stand er quer über der letzten Zeile.
|
||
#
|
||
# Die Seitenzahl sitzt bewusst unterhalb von MARGIN_BOT, sonst passiert dasselbe
|
||
# wieder.
|
||
canvas.setFont("Helvetica", 7.3)
|
||
canvas.setFillColor(DARK)
|
||
canvas.drawCentredString(PAGE_W / 2, MARGIN_BOT - 12 * mm,
|
||
f"Seite {canvas.getPageNumber()}")
|
||
canvas.restoreState()
|
||
|
||
|
||
def _titel(story, st, firma, titel, zeitraum):
|
||
story.append(Paragraph(
|
||
f"<u>{firma.get('absender','')}</u>",
|
||
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 _art_kuerzel(b) -> str:
|
||
"""Ein Buchstabe je Beleg - seine Ansage vom 04.09.2026: *"du musst die
|
||
markieren als rechnung und storno oder berichtigung, und das kannst einfach
|
||
mit buchstaben machen"*. Ohne die Marke muss man aus der Begruendung raten,
|
||
was fuer ein Beleg die Zeile ueberhaupt ist.
|
||
|
||
R = Rechnung
|
||
S = Stornorechnung
|
||
B = berichtigte Rechnung (die Neuausstellung nach einem Storno)
|
||
"""
|
||
if b.storno_zu or b.art == "storno":
|
||
return "S"
|
||
if b.art == "korrektur":
|
||
return "B"
|
||
return "R"
|
||
|
||
|
||
def _art_text(b, weg) -> str:
|
||
"""Was in der Rechnungsspalte unter der Nummer steht.
|
||
|
||
Ohne das steht im Bericht fuers Amt eine Stornonummer neben einer voellig
|
||
anderen Rechnungsnummer, und niemand sieht, was zusammengehoert. Sein Wort
|
||
dazu am 04.09.2026: *"musst dann rein schreiben storniert oder so was dran
|
||
setzen"*. Dieselben Worte wie in der Spalte "Art" im Fenster.
|
||
"""
|
||
if b.storno_zu:
|
||
# Die Kette zeigt VORWAERTS: Rechnung -> Storno -> Rechnung, die nun gilt.
|
||
# Ein Storno, der nur zurueckzeigt, laesst den Leser stehen ("wer soll
|
||
# denn sonst wissen, wo er suchen muss" - 04.09.2026).
|
||
if b.folge_nummer and b.folge_art == "vorhanden":
|
||
return f"abgerechnet mit {b.folge_nummer}"
|
||
if b.folge_nummer:
|
||
return f"neue Rechnung {b.folge_nummer}"
|
||
if b.folge_art == "" and b.art == "storno" and not b.folge_nummer:
|
||
# Entweder "nur Storno" oder ein alter Beleg ohne die Angabe. Dann
|
||
# bleibt der Rueckbezug - besser als gar kein Hinweis.
|
||
return f"Storno zu {b.storno_zu}"
|
||
return f"Storno zu {b.storno_zu}"
|
||
if b.id in weg:
|
||
# Nicht nur "storniert": auch WOMIT. Sonst steht auf dem Blatt ein
|
||
# Vermerk ohne Gegenstueck und man muss die Zeile suchen - erst recht,
|
||
# wenn der Storno in einem anderen Monat liegt und gar nicht auf
|
||
# demselben Blatt steht. Sein Hinweis vom 04.09.2026.
|
||
return f"storniert mit {weg[b.id].voll_nummer}"
|
||
if b.art == "korrektur":
|
||
return "Neuausstellung"
|
||
return ""
|
||
|
||
|
||
def render_monat(pfad, firma, jahr: int, monat: int, buchungen):
|
||
st = _styles()
|
||
# Ueber ALLE uebergebenen Buchungen bestimmt (das ganze Jahr): der Storno
|
||
# kann in einem anderen Monat liegen als die Rechnung, die er aufhebt.
|
||
weg = aufhebungen(buchungen)
|
||
# Storno und aufgehobene Rechnung stehen BEIDE im Bericht, jede in ihrem
|
||
# Monat und mit ihrem Vorzeichen - so ist die Meldung aus sich heraus
|
||
# nachvollziehbar (Entscheidung des Auftraggebers, siehe modell.summiere).
|
||
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}")
|
||
|
||
# Eigene Spalte "Grund" statt einer zweiten Zeile unter der Nummer - seine
|
||
# Ansage vom 04.09.2026: "mach eine neue spalte mit grund oder so was".
|
||
kopf = ["Datum", "Rechnung", "Art", "Begründung", "Name",
|
||
"Nächte", "Entgelt", "Satz", "Steuer"]
|
||
stile = ["th", "th", "th", "th_l", "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(f'<b>{_art_kuerzel(b)}</b>', st["zelle_c"]),
|
||
Paragraph(_art_text(b, weg), st["zelle"]),
|
||
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("<i>(keine Buchungen in diesem Monat)</i>", 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)])
|
||
# Die Zahlenspalten brauchen wenig - "Naechte", "Satz" und die Betraege
|
||
# standen frueher mit viel Luft nebeneinander. Sein Hinweis vom 04.09.2026:
|
||
# *"da ist noch so viel platz, die naechte und satz alleine nehmen so viel
|
||
# raum ein, da kannst locker zeile begruendung setzen"*. Also enger gestellt
|
||
# und der Begruendung so viel gegeben, dass "storniert mit 2026-901" in EINE
|
||
# Zeile passt - in normaler Schriftgroesse, nicht kleingedruckt.
|
||
name_w = min(max(breiteste + 6 * mm, 22 * mm), CONTENT_W - 144 * mm)
|
||
|
||
tabelle = Table(daten, repeatRows=1, hAlign="LEFT",
|
||
colWidths=[18 * mm, 18 * mm, 9 * mm, 42 * mm, name_w,
|
||
12 * mm, 18 * mm, 9 * mm, 18 * 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)
|
||
# Legende zu den Buchstaben - direkt unter der Tabelle, damit sie beim Lesen
|
||
# da ist und nicht auf der naechsten Seite. Wer nur eine Zeile prueft, muss
|
||
# nicht raten, was fuer ein Beleg sie ist.
|
||
story.append(Spacer(1, 1.5 * mm))
|
||
story.append(Paragraph(
|
||
"<b>Art:</b> R = Rechnung · S = Stornorechnung · B = berichtigte Rechnung "
|
||
"(Neuausstellung nach einem Storno). Die Spalte Begründung nennt den Beleg, "
|
||
"bei dem es weitergeht.", st["klein"]))
|
||
|
||
_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
|