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
This commit is contained in:
TheMockTv 2026-09-04 02:19:49 +02:00
parent b4db3b3849
commit b2d933cb7c
7 changed files with 69 additions and 13 deletions

View file

@ -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

17
db.py
View file

@ -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

View file

@ -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 "
"<b>Begründung</b>: „storniert mit 2026-901“, „Storno zu 2026-900“, „Neuausstellung“. "
"So ist auch dann nachvollziehbar, was zusammengehört, wenn Rechnung und Storno in "
"<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")

View file

@ -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":

View file

@ -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:

View file

@ -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)

View file

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