Storno statt Ueberschreiben: Rechnungskette 005 -> Storno -> Korrektur
Eine ausgestellte Rechnung darf nicht geaendert und nicht geloescht werden (§ 146 Abs. 4 AO), und jede Nummer gibt es nur einmal (§ 14 Abs. 4 Nr. 4 UStG). Korrigiert wird deshalb ueber ein eigenes Dokument mit neuer Nummer, das sich auf die alte bezieht (§ 31 Abs. 5 UStDV). - Knopf "Rechnung stornieren": alte Nummer waehlen, Programm liest deren PDF und schreibt eine Stornorechnung mit eigener neuer Nummer, Betraege negativ, mit Bezug auf Nummer und Datum der alten Rechnung. - Danach steht das Formular auf der naechsten Nummer, gefuellt aus der alten Rechnung; die Nummer ist gesperrt. "PDF erstellen" fragt noch einmal nach und legt bei Nein das Storno-PDF wieder weg - dann bleibt allein das Original stehen. - Die alte Rechnung wird nie angefasst. - Metadaten v3: art / storno_zu / ersetzt / storno_nummer, dazu Anschrift und Positionen, damit eine Rechnung wieder ins Formular geladen werden kann. Alle Felder aus v1/v2 bleiben unveraendert - daran haengt das Steuerjournal. - Zaehler laeuft nur noch vorwaerts; eine vergebene Nummer wird nicht mehr ueberschrieben, sondern die naechste freie angeboten. - Combobox im Storno-Dialog blieb leer: die StringVar hing nur an einer lokalen Variable und wurde weggeraeumt. Jetzt ohne textvariable. Prueflauf (pruef_storno.py, 36 Pruefungen gruen): echte PDFs in einem Wegwerf-Ordner, danach liest der Steuerrechner den Ordner - 001 + Storno + Korrektur ergeben genau die berichtigte Rechnung, kein Beleg unsicher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
This commit is contained in:
parent
1564d769c9
commit
790ac4c0f6
4 changed files with 739 additions and 16 deletions
|
|
@ -208,10 +208,21 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
if len(kunde_flow) == 1:
|
||||
kunde_flow.append(Paragraph(" ", st["addr"]))
|
||||
|
||||
art = kopf.get("art", "rechnung")
|
||||
info_rows = [
|
||||
("Rechnungsnummer:", kopf.get("rechnungsnummer", "")),
|
||||
("Stornonummer:" if art == "storno" else "Rechnungsnummer:",
|
||||
kopf.get("rechnungsnummer", "")),
|
||||
("Rechnungsdatum:", kopf.get("datum", "")),
|
||||
]
|
||||
# Der Bezug auf die alte Rechnung ist Pflicht (§ 31 Abs. 5 UStDV): Nummer UND Datum.
|
||||
if art == "storno" and kopf.get("storno_zu"):
|
||||
# Nummer und Datum in zwei Zeilen - zusammen brechen sie in der schmalen
|
||||
# Spalte um und der Bezug sieht aus wie zwei halbe Angaben.
|
||||
info_rows.append(("Storno zu Rechnung:", kopf.get("storno_zu", "")))
|
||||
if kopf.get("storno_datum"):
|
||||
info_rows.append(("Rechnung vom:", kopf["storno_datum"]))
|
||||
elif art == "korrektur" and kopf.get("ersetzt"):
|
||||
info_rows.append(("Ersetzt Rechnung:", kopf.get("ersetzt", "")))
|
||||
if kopf.get("anreise"):
|
||||
info_rows.append(("Anreise:", kopf.get("anreise", "")))
|
||||
if kopf.get("abreise"):
|
||||
|
|
@ -239,7 +250,10 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
# 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 "")
|
||||
# Das Wort "Stornorechnung" muss auf dem Blatt stehen, damit der Beleg
|
||||
# eindeutig als Aufhebung erkennbar ist.
|
||||
titel = "Stornorechnung" if art == "storno" else "Rechnungsübersicht"
|
||||
titel += " (für das Amt)" if gesamtrechnung_betont else ""
|
||||
story.append(Paragraph(titel, st["h"]))
|
||||
|
||||
# Positionstabelle
|
||||
|
|
@ -342,22 +356,44 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
continue
|
||||
satz_txt = eur_kurz(a.satz).replace(",00", "")
|
||||
story.append(Spacer(1, 1.5 * mm))
|
||||
story.append(Paragraph(
|
||||
f"Die {a.bezeichnung} ({satz_txt} %) wird gemäß kommunaler Satzung auf die "
|
||||
f"Zwischensumme erhoben und ist im Gesamtbetrag enthalten.", st["dank"]))
|
||||
if art == "storno":
|
||||
# Auf dem Storno wird nichts erhoben, sondern zurueckgenommen.
|
||||
story.append(Paragraph(
|
||||
f"Die mit der stornierten Rechnung erhobene {a.bezeichnung} "
|
||||
f"({satz_txt} %) wird mit aufgehoben.", st["dank"]))
|
||||
else:
|
||||
story.append(Paragraph(
|
||||
f"Die {a.bezeichnung} ({satz_txt} %) wird gemäß kommunaler Satzung auf die "
|
||||
f"Zwischensumme erhoben und ist im Gesamtbetrag enthalten.", st["dank"]))
|
||||
|
||||
story.append(Spacer(1, 8 * mm))
|
||||
|
||||
# ---- Danktexte + Hinweis (nur Kundenseite; auf dem Amt-Blatt unnoetig) ----
|
||||
# ---- Schlusstexte (nur Kundenseite; auf dem Amt-Blatt unnoetig) ----
|
||||
if not gesamtrechnung_betont:
|
||||
texte = cfg.get("texte", {})
|
||||
block = []
|
||||
if texte.get("dank1"):
|
||||
block.append(Paragraph(texte["dank1"], st["dank"]))
|
||||
if texte.get("dank2"):
|
||||
block.append(Paragraph(texte["dank2"], st["dank"]))
|
||||
if texte.get("hinweis"):
|
||||
block.append(Paragraph(texte["hinweis"], st["hinweis"]))
|
||||
if art == "storno":
|
||||
# Auf einem Storno sind Dank- und Zahlungstexte falsch - hier wird
|
||||
# nichts geschuldet, sondern aufgehoben.
|
||||
bezug = kopf.get("storno_zu", "")
|
||||
if kopf.get("storno_datum"):
|
||||
bezug += f" vom {kopf['storno_datum']}"
|
||||
block.append(Paragraph(
|
||||
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
|
||||
"Bereits gezahlte Beträge werden erstattet oder mit der berichtigten "
|
||||
"Rechnung verrechnet.", st["dank"]))
|
||||
else:
|
||||
if art == "korrektur" and kopf.get("ersetzt"):
|
||||
hinweis = f"Diese Rechnung ersetzt die Rechnung {kopf['ersetzt']}"
|
||||
if kopf.get("storno_nummer"):
|
||||
hinweis += f", die mit der Stornorechnung {kopf['storno_nummer']} aufgehoben wurde"
|
||||
block.append(Paragraph(hinweis + ".", st["dank"]))
|
||||
if texte.get("dank1"):
|
||||
block.append(Paragraph(texte["dank1"], st["dank"]))
|
||||
if texte.get("dank2"):
|
||||
block.append(Paragraph(texte["dank2"], st["dank"]))
|
||||
if texte.get("hinweis"):
|
||||
block.append(Paragraph(texte["hinweis"], st["hinweis"]))
|
||||
story.append(KeepTogether(block))
|
||||
|
||||
|
||||
|
|
@ -374,11 +410,21 @@ def _kenndaten(rechnung, kopf):
|
|||
stecken auf einer Rechnung gemischt drin. Alle Felder der Version 1 bleiben
|
||||
unveraendert stehen - am "zwischensumme"/"steuer_satz"/"steuer_betrag" haengt
|
||||
der Import des Steuerjournals, das darf nicht brechen.
|
||||
|
||||
Version 3 kommt mit dem Storno dazu:
|
||||
"art" - "rechnung", "storno" oder "korrektur"
|
||||
"storno_zu" - beim Storno die Nummer der aufgehobenen Rechnung
|
||||
"ersetzt" - bei der Korrektur die Nummer der falschen Rechnung
|
||||
"kunde" / "positionen" / "anreise" / "abreise" - der komplette Formularstand,
|
||||
damit eine Rechnung spaeter wieder ins Formular geladen werden kann, statt
|
||||
sie abzutippen. Aeltere PDFs (v1/v2) haben das nicht.
|
||||
Beim Storno sind alle Betraege NEGATIV - das Steuerjournal und der
|
||||
Steuerrechner summieren dadurch von allein richtig (+100 -100 +120 = 120).
|
||||
"""
|
||||
aufschlag = rechnung.aufschlaege[0] if rechnung.aufschlaege else None
|
||||
daten = {
|
||||
"quelle": "rechnungstool",
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"rechnungsnummer": kopf.get("rechnungsnummer", ""),
|
||||
"datum": kopf.get("datum", ""),
|
||||
"nachname": kopf.get("nachname", ""),
|
||||
|
|
@ -396,6 +442,16 @@ def _kenndaten(rechnung, kopf):
|
|||
"brutto": runde(b.brutto)}
|
||||
for b in rechnung.bloecke
|
||||
],
|
||||
# neu in Version 3
|
||||
"art": kopf.get("art", "rechnung"),
|
||||
"storno_zu": kopf.get("storno_zu", ""),
|
||||
"storno_datum": kopf.get("storno_datum", ""),
|
||||
"ersetzt": kopf.get("ersetzt", ""),
|
||||
"storno_nummer": kopf.get("storno_nummer", ""),
|
||||
"anreise": kopf.get("anreise", ""),
|
||||
"abreise": kopf.get("abreise", ""),
|
||||
"kunde": kopf.get("kunde", {}),
|
||||
"positionen": kopf.get("positionen", []),
|
||||
}
|
||||
return json.dumps(daten, ensure_ascii=False)
|
||||
|
||||
|
|
@ -419,7 +475,8 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
|
|||
ziel, pagesize=A4,
|
||||
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
|
||||
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
|
||||
title=f"Rechnung {kopf.get('rechnungsnummer','')}",
|
||||
title=("Stornorechnung" if kopf.get("art") == "storno" else "Rechnung")
|
||||
+ f" {kopf.get('rechnungsnummer','')}",
|
||||
author=cfg["firma"].get("name", ""),
|
||||
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue