Steuerrechnungstool/doku_aenderungen_pdf.py
TheMockTv b2d933cb7c Amtsbericht: die Kette zeigt vorwaerts - Storno nennt die neue Rechnung
Sein Fund: "an der storno verweist du auf die alte rechnung, muss aber
lauten neu zu und neue rechnungs nummer. heisst kette ist so: rechnung
storniert weist auf die storno nummer, storno verweist auf neue rechnung.
wer soll denn sonst wissen wo er suchen muss."

Er hat recht: eine Zeile, die nur zurueckzeigt, laesst den Leser stehen.
Jetzt sagt jede Zeile, wo es WEITERGEHT:

  Rechnung   -> "storniert mit 2026-901"
  Storno     -> "neue Rechnung 2026-902"      (Fall A)
             -> "neu abgerechnet mit 2026-343" (Fall B, Doppelung)
             -> "Storno zu 2026-900"           (Fall C / alte Belege ohne Angabe)
  neue R.    -> "Neuausstellung"

Dafuer wandert die Folgenummer aus den PDF-Kenndaten bis ins Journal:
Buchung.folge_nummer + folge_art (neu | vorhanden | ""), gelesen aus
korrektur_nummer/ersatz_vorhanden/nur_storno, zwei neue DB-Spalten, alte
Journale bauen sich wie gehabt selbst um.

An seinen ECHTEN Belegen geprueft: Storno 2026-455 -> folge 2026-369
(vorhanden), 2026-456 -> 2026-343 (vorhanden). Die Angabe steht in den
Kenndaten schon drin, sie wurde bisher nur nicht gelesen.

Fenster und Blatt zeigen dieselben Worte. Erklaerungs-PDF um eine
Kettentabelle ergaenzt. Pruefkette gruen (storno_verrechnet inkl.
September-Blatt, doppelte_leistung, nur_storno).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
2026-09-04 02:19:49 +02:00

308 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([
["Zeile im Bericht", "Begründung", "Bedeutung"],
["die aufgehobene Rechnung", "storniert mit 2026-901",
"hier steht, welcher Beleg sie aufhebt"],
["die Stornorechnung", "neue Rechnung 2026-902",
"hier steht, unter welcher Nummer die Leistung jetzt abgerechnet ist"],
["die Stornorechnung im Doppelungsfall", "neu abgerechnet mit 2026-343",
"die Leistung war bereits abgerechnet – das ist die Rechnung, die bleibt"],
["die neue Rechnung", "Neuausstellung",
"sie ersetzt die aufgehobene"],
], st, [45 * mm, 47 * mm, CONTENT_W - 92 * mm]))
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")