Steuerrechnungstool/doku_aenderungen_pdf.py
TheMockTv 379afbd9c1 Amtsbericht: Spalte "Art" mit R/S/B und Legende unter der Tabelle
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
2026-09-04 02:29:48 +02:00

312 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""Erzeugt die Erklaerung fuers Amt / die Steuerberaterin als PDF.
python doku_aenderungen_pdf.py [Zielpfad.pdf]
Warum als eigenes Skript und nicht von Hand geschrieben: der Text nennt
Paragraphen und Betraege. Wenn sich etwas am Programm aendert, aendert man den
Text hier und erzeugt das Blatt neu - statt eine Word-Datei zu suchen, die
niemand mehr findet.
Briefkopf und Farben kommen aus derselben Konfiguration wie die Rechnungen
(firma.lade()), damit das Blatt erkennbar zum Betrieb gehoert.
"""
import os
import sys
from reportlab.lib import colors
from reportlab.lib.enums import TA_JUSTIFY
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus import (BaseDocTemplate, Frame, KeepTogether, PageTemplate,
Paragraph, Spacer, Table, TableStyle)
HIER = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HIER)
import firma as firma_lib # noqa: E402
ACCENT = colors.HexColor("#1f5c3d")
GREY_BG = colors.HexColor("#eef1ee")
LINE = colors.HexColor("#c8d0c8")
MUTED = colors.HexColor("#555555")
PAGE_W, PAGE_H = A4
MARGIN_L = MARGIN_R = 20 * mm
CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R
def stile():
return {
"titel": ParagraphStyle("titel", fontName="Helvetica-Bold", fontSize=17,
textColor=ACCENT, leading=21, spaceAfter=2),
"untertitel": ParagraphStyle("untertitel", fontName="Helvetica", fontSize=9.5,
textColor=MUTED, leading=13, spaceAfter=10),
"h": ParagraphStyle("h", fontName="Helvetica-Bold", fontSize=11.5,
textColor=ACCENT, leading=15, spaceBefore=9, spaceAfter=4),
"t": ParagraphStyle("t", fontName="Helvetica", fontSize=9.5, leading=13.5,
alignment=TA_JUSTIFY, spaceAfter=5),
"klein": ParagraphStyle("klein", fontName="Helvetica", fontSize=8, leading=11,
textColor=MUTED, spaceAfter=4),
"th": ParagraphStyle("th", fontName="Helvetica-Bold", fontSize=8.5, leading=11),
"z": ParagraphStyle("z", fontName="Helvetica", fontSize=8.5, leading=11.5),
}
def kopf_fuss(canvas, doc, cfg):
canvas.saveState()
f = cfg
canvas.setFont("Helvetica-Bold", 12)
canvas.setFillColor(ACCENT)
canvas.drawRightString(PAGE_W - MARGIN_R, PAGE_H - 15 * mm, f.get("name", ""))
canvas.setFont("Helvetica-Oblique", 8)
canvas.setFillColor(MUTED)
canvas.drawRightString(PAGE_W - MARGIN_R, PAGE_H - 20 * mm, f.get("untertitel", ""))
canvas.setStrokeColor(ACCENT)
canvas.setLineWidth(0.8)
canvas.line(MARGIN_L, PAGE_H - 23 * mm, PAGE_W - MARGIN_R, PAGE_H - 23 * mm)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(MUTED)
canvas.drawString(MARGIN_L, 12 * mm, f.get("absender", ""))
canvas.drawRightString(PAGE_W - MARGIN_R, 12 * mm, f"Seite {canvas.getPageNumber()}")
canvas.restoreState()
def tabelle(daten, st, breiten):
zeilen = [[Paragraph(z, st["th"] if i == 0 else st["z"]) for z in reihe]
for i, reihe in enumerate(daten)]
t = Table(zeilen, colWidths=breiten, repeatRows=1, hAlign="LEFT")
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("LINEBELOW", (0, 0), (-1, 0), 0.8, ACCENT),
("LINEBELOW", (0, 1), (-1, -1), 0.3, LINE),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
]))
return t
def bauen(pfad, cfg):
st = stile()
s = []
P = lambda text, art="t": s.append(Paragraph(text, st[art])) # noqa: E731
P("Änderungen an Rechnungsschreibung und Steuerbuch", "titel")
P("Campinghof Bartl · Stand 4. September 2026 · Rechnungstool v1.5.5, "
"Steuerbuch (Beherbergungssteuer) v1.6.7", "untertitel")
P("Warum überhaupt geändert wurde", "h")
P("Im laufenden Betrieb sind zwei Dinge aufgefallen, die beide dieselbe Ursache haben – "
"es gab keine feste Vorgabe, wie eine bereits geschriebene Rechnung korrigiert wird:")
P("<b>1. Dieselbe Leistung stand mehrfach im Rechnungsordner.</b> Wenn an einer Rechnung etwas "
"nicht stimmte, wurde mit dem alten Programm einfach eine neue geschrieben – mit einer neuen "
"Nummer. Die alte blieb liegen. Damit stand derselbe Aufenthalt zweimal im Ordner und wäre "
"zweimal in die Beherbergungssteuer-Anmeldung eingegangen.")
P("<b>2. Beim Empfänger stand teilweise nur der Vor- oder nur der Nachname.</b> Die "
"Anmeldekarten der Gäste enthalten alle Angaben; auf der Rechnung landete davon nicht immer "
"alles.")
P("Was jetzt gilt – feste Wege statt freier Hand", "h")
P("Eine geschriebene Rechnung lässt sich nicht mehr formlos ändern. Es gibt genau zwei Wege, "
"und beide erzeugen einen eigenen, nachvollziehbaren Beleg:")
s.append(tabelle([
["Fall", "Weg im Programm", "Was der Beleg sagt"],
["Angaben waren falsch, Leistung und Beträge stimmen "
"(z. B. Name, Anschrift)",
"<b>Berichtigung</b>",
"Eigenes Blatt, das sich auf die Rechnung bezieht. Die Rechnung <b>behält ihre "
"Nummer</b>, Beträge bleiben unverändert."],
["Die Rechnung selbst muss weg (falsche Leistung, "
"Doppelung, geplatzte Buchung)",
"<b>Storno</b> mit drei Auswahlmöglichkeiten",
"Eigene Stornorechnung mit Minusbeträgen, die die Rechnung nach Nummer, Datum und "
"Empfänger benennt."],
], st, [42 * mm, 38 * mm, CONTENT_W - 80 * mm]))
s.append(Spacer(1, 4 * mm))
P("Die drei Wege im Storno", "h")
P("Nach dem Storno muss auf dem Blatt stehen, wie es weitergeht – sonst wartet der Gast auf "
"eine Rechnung, die nie kommt, oder es bleibt eine Nummer unnötig reserviert. Deshalb fragt "
"das Programm zuerst:")
s.append(tabelle([
["", "Fall", "Folge auf dem Beleg"],
["A", "Es folgt eine neue, richtige Rechnung",
"Die Nummer der berichtigten Rechnung steht bereits auf dem Storno: "
"alte Nummer → Storno → neue Nummer."],
["B", "Die Leistung ist schon unter einer anderen Nummer abgerechnet "
"(der Doppelungsfall)",
"Das Storno verweist auf die <b>bleibende</b> Rechnung. Das Programm prüft, ob es diese "
"Nummer gibt und ob Gast, Zeitraum und Betrag dazu passen. Es wird keine Nummer "
"reserviert und kein Vorgang bleibt offen."],
["C", "Es wird gar nichts abgerechnet (Buchung geplatzt, Rechnung irrtümlich)",
"Auf dem Blatt steht, dass keine weitere Rechnung folgt. Es wird ebenfalls keine Nummer "
"reserviert."],
], st, [8 * mm, 52 * mm, CONTENT_W - 60 * mm]))
s.append(Spacer(1, 4 * mm))
P("Pflichtfelder beim Empfänger", "h")
P("Die Felder für den Empfänger sind jetzt <b>Pflicht</b>. Das ist keine Verschärfung durch "
"das Programm, sondern geltendes Recht: eine Rechnung muss den vollständigen Namen und die "
"vollständige Anschrift des Leistungsempfängers enthalten. Für die Beherbergungssteuer kommt "
"hinzu, dass die Meldescheine ohnehin Name, Wohnanschrift, Geburtsdatum sowie An- und "
"Abreise tragen und zusammen mit den Rechnungskopien aufzubewahren sind – die Angaben liegen "
"also vor und gehören auf den Beleg. Nur so lässt sich später zweifelsfrei sagen, "
"<b>wer zu welcher Rechnung gehört</b>.")
P("Die Rechnungsnummer steht fest", "h")
P("Eine einmal ausgestellte Rechnung behält ihre Nummer. Sie lässt sich im Programm nicht mehr "
"umschreiben – geändert wird ausschließlich über einen der beiden Belege: die "
"<b>Berichtigung</b> (die Rechnung behält ihre Nummer) oder den <b>Storno</b> (eigener Beleg "
"mit eigener Nummer). Vier Sperren sorgen dafür:")
s.append(tabelle([
["Sperre", "Wirkung"],
["Eine vergebene Nummer wird nicht wiederverwendet",
"Vor dem Schreiben prüft das Programm den Rechnungsordner <b>und</b> das gemeinsame "
"Nummernbuch. Ist die Nummer belegt, wird die vorhandene Rechnung <b>nicht</b> "
"überschrieben, sondern die nächste freie Nummer angeboten."],
["Der Zähler kann nicht zurück",
"Auch nicht über die Einstellungen. Als Quelle gelten die vorhandenen Rechnungs-PDFs und "
"das Nummernbuch – wer den Zähler kleiner setzt, hebt ihn beim nächsten Beleg "
"automatisch wieder an."],
["Beim Storno ist das Nummernfeld gesperrt",
"Dort vergibt ausschließlich das Programm: die Nummer des Stornos und – wenn eine neue "
"Rechnung folgt – gleich auch deren Nummer, damit die Kette auf dem Blatt steht."],
["Keine Nummer auf Vorrat",
"Verweist das Storno auf eine bereits bestehende Rechnung oder folgt gar nichts, wird "
"keine Nummer reserviert."],
], st, [58 * mm, CONTENT_W - 58 * mm]))
s.append(Spacer(1, 2 * mm))
P("Eine lückenlose Folge verlangt das Gesetz nicht – eine <b>einmalige</b> schon "
"(§ 14 Abs. 4 Nr. 4 UStG). Vorwärts darf die Nummerierung also springen, zurück nicht.",
"klein")
s.append(Spacer(1, 2 * mm))
P("Doppelte Rechnungsnummern werden angezeigt, nicht weggerechnet", "h")
P("Das Steuerbuch meldet jede Nummer, die mehr als einmal vergeben wurde – rot in der Liste, "
"im Monatsreiter markiert und in einer eigenen Übersicht mit allen betroffenen Belegen. "
"Gerechnet wird über das ganze Jahr, weil dieselbe Nummer in zwei Monaten liegen kann. "
"<b>Die Beträge bleiben dabei in allen Summen stehen</b>: das Buch soll mit dem "
"Rechnungsordner übereinstimmen. Was zu tun ist, entscheidet der Betrieb – der vorgesehene "
"Weg steht im Programm: eine der beiden Rechnungen stornieren und, falls nötig, mit freier "
"Nummer neu ausstellen. Sobald das geschehen ist, verschwindet die Meldung von selbst, weil "
"eine stornierte Rechnung keine offene mehr ist.")
P("Wie ein Storno in die Steueranmeldung eingeht", "h")
P("Die aufgehobene Rechnung bleibt mit ihrem Plus im Monat der Rechnung stehen, die "
"Stornorechnung steht mit ihrem Minus im Monat des Stornos. Über das Jahr gleicht sich das "
"aus. Bewusst wurde der sichere Weg gewählt: im ersten Monat wird eher zu viel gemeldet und "
"im zweiten entsprechend weniger – nie zuerst zu wenig. Fällt eine Korrektur in einen Monat, "
"der bereits angemeldet ist, ist eine berichtigte Anmeldung vorgesehen; das amtliche Formular "
"hat dafür ein eigenes Ankreuzfeld, und die Anmeldung steht ohnehin unter dem Vorbehalt der "
"Nachprüfung.")
P("Damit das Blatt fürs Amt aus sich heraus verständlich ist, trägt jede betroffene Zeile eine "
"<b>Begründung</b>, und zwar als <b>Kette, die vorwärts zeigt</b> – jede Zeile sagt, wo es "
"weitergeht:")
s.append(tabelle([
["Art", "Zeile im Bericht", "Begründung", "Bedeutung"],
["<b>R</b>", "die aufgehobene Rechnung", "storniert mit 2026-901",
"hier steht, welcher Beleg sie aufhebt"],
["<b>S</b>", "die Stornorechnung", "neue Rechnung 2026-902",
"hier steht, unter welcher Nummer die Leistung jetzt abgerechnet ist"],
["<b>S</b>", "Stornorechnung im Doppelungsfall", "abgerechnet mit 2026-343",
"die Leistung war bereits abgerechnet – das ist die Rechnung, die bleibt"],
["<b>B</b>", "die berichtigte Rechnung", "Neuausstellung",
"sie ersetzt die aufgehobene"],
], st, [9 * mm, 40 * mm, 40 * mm, CONTENT_W - 89 * mm]))
s.append(Spacer(1, 1 * mm))
P("Die Spalte <b>Art</b> markiert jeden Beleg mit einem Buchstaben – <b>R</b> Rechnung, "
"<b>S</b> Stornorechnung, <b>B</b> berichtigte Rechnung. Die Erklärung dazu steht auf jedem "
"Blatt unter der Tabelle.", "klein")
s.append(Spacer(1, 2 * mm))
P("So findet man von jedem Beleg aus den nächsten, auch wenn Rechnung und Storno in "
"verschiedenen Monaten liegen und gar nicht auf demselben Blatt stehen.")
P("Die Rechtsgrundlagen im Einzelnen", "h")
P("„Satzung“ meint die Satzung über die Erhebung einer Beherbergungssteuer in der Stadt "
"Markkleeberg vom 6. Februar 2024, anzuwenden auf Beherbergungsleistungen ab dem "
"1. Januar 2025.", "klein")
s.append(tabelle([
["Vorschrift", "Inhalt", "Was im Programm daraus folgt"],
["§ 14 Abs. 4 Nr. 1 UStG", "Vollständiger Name und vollständige Anschrift des "
"Leistungsempfängers", "Empfängerfelder sind Pflichtfelder"],
["§ 14 Abs. 4 Nr. 4 UStG", "Fortlaufende Nummer, vom Aussteller <b>einmalig</b> vergeben",
"Doppelte Nummern werden gemeldet; Stornos und Neuausstellungen bekommen eigene Nummern, "
"es werden keine reserviert, die nicht gebraucht werden"],
["§ 31 Abs. 5 UStDV", "Eine Rechnung kann berichtigt werden; das Dokument muss sich "
"<b>spezifisch und eindeutig</b> auf die Rechnung beziehen",
"Berichtigungsblatt bezieht sich auf Nummer und Datum; das Storno nennt zusätzlich den "
"Empfänger – bei einer doppelt vergebenen Nummer wäre die Nummer allein kein "
"eindeutiger Bezug"],
["§ 14c Abs. 1 UStG", "Wer in einer Rechnung mehr Steuer ausweist, als er schuldet, "
"schuldet auch den Mehrbetrag",
"Deshalb wird eine doppelt geschriebene Rechnung storniert und nicht einfach liegen "
"gelassen; das Berichtigungsblatt sagt ausdrücklich, dass es keine zweite Leistung ist"],
["§ 146 Abs. 4 AO", "Eine Buchung darf nicht so verändert werden, dass der ursprüngliche "
"Inhalt nicht mehr feststellbar ist",
"Kein freies Bearbeiten von Buchungen mehr; jede Änderung entsteht über einen eigenen "
"Beleg (Storno oder Berichtigung), gelöscht wird nichts"],
["§ 147 AO (über § 7 Abs. 4 der Satzung)", "Aufbewahrung der Rechnungskopien und "
"Meldescheine, Vorlage auf Anforderung", "Rechnungen bleiben als PDF im Ordner; das "
"Steuerbuch liest sie und legt sie nicht neu an"],
["§ 3 Abs. 1 und 3 der Satzung",
"Bemessungsgrundlage ist das Übernachtungsentgelt einschließlich Umsatzsteuer; "
"5 % je Übernachtung, abgerundet auf volle Cent; höchstens 21 Tage",
"Die Steuer wird auf die Zwischensumme der Rechnung gerechnet und auf dem Beleg "
"gesondert ausgewiesen"],
["§ 6 der Satzung", "Der Steueranspruch entsteht mit Beginn der Beherbergung; ohne Entgelt "
"entsteht keine Steuer", "Ein Angebot löst nichts aus; erst die Rechnung wird gebucht"],
["§ 7 Abs. 3 der Satzung", "Meldescheine mit Name, Wohnanschrift, Geburtsdatum sowie "
"An- und Abreise", "Die Angaben liegen vor und gehören auf die Rechnung"],
["§ 7 Abs. 5 der Satzung", "Die im Kalendermonat <b>vereinnahmte</b> Steuer ist bis zum "
"10. des Folgemonats selbst zu berechnen, anzumelden und abzuführen",
"Monatsbericht als Aufstellung zur Anmeldung; Nullmonate bleiben nachvollziehbar"],
["§§ 164, 168 AO (über § 3 Abs. 1 SächsKAG)", "Die Steueranmeldung steht einer Festsetzung "
"unter Vorbehalt der Nachprüfung gleich",
"Ein bereits angemeldeter Monat kann berichtigt werden – deshalb ist der sichere Weg, "
"im Zweifel eher zu viel zu melden"],
], st, [42 * mm, 55 * mm, CONTENT_W - 97 * mm]))
s.append(Spacer(1, 4 * mm))
P("Zwei Punkte, die noch offen sind", "h")
P("<b>1. Steuerbefreite Übernachtungen.</b> Nach § 4 Abs. 1 Nr. 1 der Satzung sind Kinder und "
"Jugendliche bis zum vollendeten 18. Lebensjahr von der Beherbergungssteuer befreit. Im "
"Leistungskatalog gibt es eine eigene Position „Personengebühr Kinder ab 2 Jahre bis 14 "
"Jahre“. Dieser Umsatz wird derzeit mitversteuert. Das amtliche Anmeldeformular sieht dafür "
"eine eigene Zeile vor („abzüglich Umsätze aus steuerbefreiten Übernachtungen“). Bis zur "
"Umstellung wird also eher zu viel als zu wenig gemeldet.")
P("<b>2. Abrundung je Übernachtung.</b> Nach § 3 Abs. 3 der Satzung wird der Steueranteil je "
"einzelner Übernachtung auf volle Cent abgerundet. Derzeit werden 5 % auf die Summe "
"gerechnet. Das amtliche Formular trägt dem mit einer eigenen Zeile Rechnung "
"(„tatsächlich einbehaltene Beherbergungssteuer“).")
P("Beide Punkte wirken zugunsten der Stadt und nicht zulasten – sie führen zu einer eher zu "
"hohen, nicht zu einer zu niedrigen Anmeldung.", "klein")
P("Zum Schluss", "h")
P("Die Programme ändern nichts rückwirkend und löschen nichts. Jede Korrektur ist ein eigener "
"Beleg mit eigener Nummer, der sagt, worauf er sich bezieht. Damit lässt sich zu jedem "
"Zeitpunkt nachvollziehen, welche Rechnung zu welchem Gast gehört, welche aufgehoben wurde "
"und womit – im Rechnungsordner, im Steuerbuch und auf dem Blatt für die Stadt.")
doc = BaseDocTemplate(pfad, pagesize=A4,
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
topMargin=30 * mm, bottomMargin=18 * mm,
title="Änderungen an Rechnungsschreibung und Steuerbuch",
author=cfg.get("name", ""))
rahmen = Frame(MARGIN_L, 18 * mm, CONTENT_W, PAGE_H - 30 * mm - 18 * mm, id="f")
doc.addPageTemplates([PageTemplate(id="seite", frames=[rahmen],
onPage=lambda c, d: kopf_fuss(c, d, cfg))])
doc.build(s)
return pfad
if __name__ == "__main__":
ziel = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
os.path.expanduser("~"), "Desktop",
"Campinghof_Aenderungen_Rechnungswesen_2026-09-04.pdf")
cfg = firma_lib.lade()
bauen(ziel, cfg)
print("geschrieben:", ziel, os.path.getsize(ziel), "Bytes")