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",