diff --git a/bericht_pdf.py b/bericht_pdf.py
index c99ae50..23fe6aa 100644
--- a/bericht_pdf.py
+++ b/bericht_pdf.py
@@ -207,6 +207,17 @@ def _art_text(b, weg) -> str:
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"neu 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
diff --git a/db.py b/db.py
index 0040dea..8fc80df 100644
--- a/db.py
+++ b/db.py
@@ -36,6 +36,8 @@ CREATE TABLE IF NOT EXISTS buchungen (
storno_zu TEXT NOT NULL DEFAULT '',
storno_datum TEXT NOT NULL DEFAULT '',
vorgang TEXT NOT NULL DEFAULT '',
+ folge_nummer TEXT NOT NULL DEFAULT '',
+ folge_art TEXT NOT NULL DEFAULT '',
pdf_pfad TEXT NOT NULL DEFAULT '',
pdf_mtime REAL NOT NULL DEFAULT 0,
pdf_groesse INTEGER NOT NULL DEFAULT 0
@@ -127,7 +129,8 @@ class Journal:
"""
da = {z["name"] for z in self.con.execute("PRAGMA table_info('buchungen')")}
for name, vorgabe in (("art", "'rechnung'"), ("storno_zu", "''"),
- ("storno_datum", "''"), ("vorgang", "''")):
+ ("storno_datum", "''"), ("vorgang", "''"),
+ ("folge_nummer", "''"), ("folge_art", "''")):
if name not in da:
self.con.execute(
f"ALTER TABLE buchungen ADD COLUMN {name} TEXT NOT NULL DEFAULT {vorgabe}")
@@ -180,6 +183,8 @@ class Journal:
storno_zu=(r["storno_zu"] if "storno_zu" in r.keys() else ""),
storno_datum=(r["storno_datum"] if "storno_datum" in r.keys() else ""),
vorgang=(r["vorgang"] if "vorgang" in r.keys() else ""),
+ folge_nummer=(r["folge_nummer"] if "folge_nummer" in r.keys() else ""),
+ folge_art=(r["folge_art"] if "folge_art" in r.keys() else ""),
)
def _pdf_bekannt(self, pfad: str) -> bool:
@@ -204,8 +209,8 @@ class Journal:
"INSERT INTO buchungen "
" (jahr, rechnungsnummer, datum, nachname, naechte, gezahlt, satz, "
" quelle, pdf_pfad, pdf_mtime, pdf_groesse, art, storno_zu, "
- " storno_datum, vorgang) "
- "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
+ " storno_datum, vorgang, folge_nummer, folge_art) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
"ON CONFLICT(pdf_pfad) WHERE pdf_pfad <> '' DO UPDATE SET "
" jahr=excluded.jahr, rechnungsnummer=excluded.rechnungsnummer, "
" datum=excluded.datum, nachname=excluded.nachname, naechte=excluded.naechte, "
@@ -213,10 +218,12 @@ class Journal:
" pdf_pfad=excluded.pdf_pfad, pdf_mtime=excluded.pdf_mtime, "
" pdf_groesse=excluded.pdf_groesse, art=excluded.art, "
" storno_zu=excluded.storno_zu, storno_datum=excluded.storno_datum, "
- " vorgang=excluded.vorgang",
+ " vorgang=excluded.vorgang, folge_nummer=excluded.folge_nummer, "
+ " folge_art=excluded.folge_art",
(b.jahr, b.rechnungsnummer, b.datum.isoformat(), b.nachname, int(b.naechte or 0),
float(b.gezahlt or 0), float(b.satz or 0), b.quelle, b.pdf_pfad, mtime, groesse,
- b.art or "rechnung", b.storno_zu or "", b.storno_datum or "", b.vorgang or ""))
+ b.art or "rechnung", b.storno_zu or "", b.storno_datum or "", b.vorgang or "",
+ b.folge_nummer or "", b.folge_art or ""))
self.con.commit()
return cur.lastrowid
diff --git a/doku_aenderungen_pdf.py b/doku_aenderungen_pdf.py
index d56ba25..7bc9ffa 100644
--- a/doku_aenderungen_pdf.py
+++ b/doku_aenderungen_pdf.py
@@ -204,8 +204,21 @@ def bauen(pfad, cfg):
"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 "
- "Begründung: „storniert mit 2026-901“, „Storno zu 2026-900“, „Neuausstellung“. "
- "So ist auch dann nachvollziehbar, was zusammengehört, wenn Rechnung und Storno in "
+ "Begründung, und zwar als Kette, die vorwärts zeigt – 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")
diff --git a/gui_monat.py b/gui_monat.py
index 38ee93a..eaefa91 100644
--- a/gui_monat.py
+++ b/gui_monat.py
@@ -153,7 +153,13 @@ class MonatsTab(ttk.Frame):
else:
marke = (b.quelle,)
if b.storno_zu:
- art_text = f"Storno zu {b.storno_zu}"
+ # Kette vorwaerts: Rechnung -> Storno -> die Rechnung, die gilt.
+ if b.folge_nummer and b.folge_art == "vorhanden":
+ art_text = f"neu abgerechnet mit {b.folge_nummer}"
+ elif b.folge_nummer:
+ art_text = f"neue Rechnung {b.folge_nummer}"
+ else:
+ art_text = f"Storno zu {b.storno_zu}"
elif b.id in aufgehoben:
art_text = f"storniert mit {aufgehoben[b.id].voll_nummer}"
elif b.art == "korrektur":
diff --git a/modell.py b/modell.py
index 2e9f218..46a1594 100644
--- a/modell.py
+++ b/modell.py
@@ -57,6 +57,11 @@ class Buchung:
storno_zu: str = "" # Nummer der aufgehobenen Rechnung
storno_datum: str = "" # deren Datum (TT.MM.JJJJ) - Teil des Bezugs
vorgang: str = "" # Klammer um Rechnung, Storno und Neuausstellung
+ # Worauf der Storno VORWAERTS zeigt. Ohne das endet die Kette am Storno und
+ # niemand weiss, wo die Leistung nun abgerechnet ist (seine Ansage 04.09.2026:
+ # "wer soll denn sonst wissen, wo er suchen muss").
+ folge_nummer: str = "" # Nummer der Rechnung, die danach gilt
+ folge_art: str = "" # neu | vorhanden | "" (es folgt nichts)
@property
def steuer(self) -> float:
diff --git a/pdf_parser.py b/pdf_parser.py
index 4bc73eb..7462ad9 100644
--- a/pdf_parser.py
+++ b/pdf_parser.py
@@ -239,6 +239,10 @@ def parse_pdf(pfad: str, standard_satz: float = STANDARD_SATZ) -> Buchung:
storno_zu=str(meta.get("storno_zu") or ""),
storno_datum=str(meta.get("storno_datum") or ""),
vorgang=str(meta.get("vorgang") or ""),
+ folge_nummer=str(meta.get("korrektur_nummer") or ""),
+ folge_art=("" if meta.get("nur_storno")
+ else "vorhanden" if meta.get("ersatz_vorhanden")
+ else "neu" if meta.get("korrektur_nummer") else ""),
)
log.debug("aus Metadaten: %s | %s | %s | %s Nächte | %.2f €",
os.path.basename(pfad), b.voll_nummer, b.nachname, b.naechte, b.gezahlt)
diff --git a/pruef_storno_verrechnet.py b/pruef_storno_verrechnet.py
index a790c68..d131433 100644
--- a/pruef_storno_verrechnet.py
+++ b/pruef_storno_verrechnet.py
@@ -73,12 +73,14 @@ JAHR = 2026
FAKE = [
Buchung(date(JAHR, 8, 22), "900", JAHR, "Muster", 4, 52.0, quelle="manuell"),
Buchung(date(JAHR, 8, 23), "901", JAHR, "Muster", -4, -52.0, quelle="manuell",
- art="storno", storno_zu="2026-900", storno_datum="22.08.2026"),
+ art="storno", storno_zu="2026-900", storno_datum="22.08.2026",
+ folge_nummer="2026-902", folge_art="neu"),
Buchung(date(JAHR, 8, 23), "902", JAHR, "Muster", 3, 39.0, quelle="manuell",
art="korrektur"),
Buchung(date(JAHR, 8, 22), "910", JAHR, "Spaet", 4, 52.0, quelle="manuell"),
Buchung(date(JAHR, 9, 2), "911", JAHR, "Spaet", -4, -52.0, quelle="manuell",
- art="storno", storno_zu="2026-910", storno_datum="22.08.2026"),
+ art="storno", storno_zu="2026-910", storno_datum="22.08.2026",
+ folge_nummer="2026-905", folge_art="vorhanden"),
Buchung(date(JAHR, 8, 5), "905", JAHR, "Sauber", 2, 26.0, quelle="manuell"),
]
@@ -149,7 +151,15 @@ pruefe("storniert mit 2026-901" in text,
"die aufgehobene Rechnung nennt den Storno, der sie aufhebt")
pruefe("storniert mit 2026-911" in text,
"auch dann, wenn dieser Storno in einem ANDEREN Monat liegt")
-pruefe("Storno zu 2026-900" in text, "und der Storno nennt die Rechnung, die er aufhebt")
+pruefe("neue Rechnung 2026-902" in text,
+ "der Storno zeigt VORWAERTS auf die Rechnung, die nun gilt")
+# Der zweite Storno steht im SEPTEMBER - also dort nachsehen, nicht im August.
+pfad_sep = os.path.join(tmp, "September.pdf")
+bericht_pdf.render_monat(pfad_sep, firma_lib.lade(), JAHR, 9, alle)
+text_sep = " ".join((PdfReader(pfad_sep).pages[0].extract_text() or "").split())
+pruefe("neu abgerechnet mit 2026-905" in text_sep,
+ "und beim Doppelungsfall zeigt er auf die Rechnung, die bleibt")
+pruefe("-52,00" in text_sep, "der September traegt weiter seinen Minusbetrag")
pruefe("Neuausstellung" in text, "die neue Rechnung ist als Neuausstellung beschriftet")
# ---------------------------------------------------------------------------
@@ -164,8 +174,8 @@ pruefe(len(reihen) == 5, f"der August zeigt alle fuenf Zeilen ({len(reihen)})")
pruefe(not any("aufgehoben" in t for _nr, _a, t in reihen),
f"keine Zeile ist ausgegraut ({sorted({t for _n, _a, t in reihen})})")
arten = {nr: art for nr, art, _t in reihen}
-pruefe(arten.get("2026-901") == "Storno zu 2026-900",
- f"der Storno ist als Storno getaggt ({arten.get('2026-901')!r})")
+pruefe(arten.get("2026-901") == "neue Rechnung 2026-902",
+ f"der Storno zeigt auf die neue Rechnung ({arten.get('2026-901')!r})")
pruefe(arten.get("2026-900") == "storniert mit 2026-901",
f"die aufgehobene Rechnung nennt ihren Storno ({arten.get('2026-900')!r})")
pruefe(arten.get("2026-902") == "Neuausstellung",