# -*- coding: utf-8 -*- """Pruefstand: Version C - nur Storno, es folgt nichts. Seine Ansage vom 04.09.2026: "des wegen sollte mann auch eine version c mit only storno machen weil das gibt es auch" Es gab bisher nur zwei Wege aus dem Storno heraus: A - es folgt eine neue Rechnung B - die Leistung ist schon abgerechnet -> Verweis auf die bleibende Nummer Beide behaupten etwas, das im dritten Fall nicht stimmt: die Buchung ist geplatzt oder die Rechnung war komplett irrtuemlich, es wird also gar nichts abgerechnet. Mit A wartet der Gast ewig auf eine Rechnung, die nie kommt (und eine Nummer waere unnoetig reserviert), mit B muesste man auf eine Rechnung verweisen, die es nicht gibt. Geprueft wird am echten Fenster: der Knopf wird wirklich gedrueckt, danach werden Blatt, Kenndaten, Nummernzaehler und das Steuerjournal angesehen. Aufruf: python -u pruef_nur_storno.py """ import json import os import shutil import sys import tempfile import threading QUELLE = r"C:\claude\rechnungstool" JOURNAL = r"C:\claude\beherbergungssteuer" sys.path.insert(0, QUELLE) def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog") os._exit(2) _wecker = threading.Timer(150, _reissleine) _wecker.daemon = True _wecker.start() os.environ["CAMPINGHOF_TEST"] = "1" tmp = tempfile.mkdtemp(prefix="nur_storno_") ordner = os.path.join(tmp, "rechnungen") os.makedirs(ordner) import config # noqa: E402 shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json")) config.CONFIG_PATH = os.path.join(tmp, "config.json") roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0 roh["nummer_eingerichtet"] = True roh["output_dir"] = ordner json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) import app as A # noqa: E402 import bestand as BE # noqa: E402 from tkinter import messagebox # noqa: E402 from pruefhelfer import fehler, pruefe # Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst. # Geprueft wird er eigens in pruef_hinweis.py. messagebox.askyesno = lambda *a, **k: True messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: None messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "") f = A.RechnungsApp() f.cfg["output_dir"] = ordner f._storno_dialog_zeigen = True # das Storno-Popup soll wirklich kommen def widgets(w, klassen, aus=None): aus = [] if aus is None else aus for k in w.winfo_children(): if k.winfo_class() in klassen: aus.append(k) widgets(k, klassen, aus) return aus def druecke(top, text): for k in widgets(top, ("TButton", "Button")): if text.lower() in str(k.cget("text")).lower(): k.invoke() return True return False # ---- Eine Rechnung, die gleich wieder aufgehoben wird --------------------- f.reset_formular() f.k_vorname.set("Anna") f.k_name.set("Platzer") f.k_strasse.set("Musterweg 1") f.k_plzort.set("12345 Musterstadt") f.r_anreise.set("01.08.2026") f.r_abreise.set("04.08.2026") f.anzahl_vars[sorted(f.anzahl_vars)[0]].set("3") f._testwahl = "rechnung" f.update() f.pdf_erstellen() pruefe(os.listdir(ordner) == ["Rechnung_2026-001_Platzer.pdf"], f"die Rechnung liegt im Ordner ({os.listdir(ordner)})") zaehler_vorher = f.cfg.get("rechnung_zaehler") # ---- Storno, im Fenster auf "C: nur Storno" ------------------------------ gedrueckt = {"c": False, "knoepfe": []} def klick(rest=60): tops = [w for w in f.winfo_children() if w.winfo_class() == "Toplevel"] if not tops: if rest: f.after(100, lambda: klick(rest - 1)) return top = tops[-1] gedrueckt["knoepfe"] = [str(k.cget("text")) for k in widgets(top, ("TButton", "Button"))] gedrueckt["c"] = druecke(top, "C: nur Storno") if not gedrueckt["c"] and rest: f.after(100, lambda: klick(rest - 1)) meta = [m for m in BE.rechnungen_im_ordner(ordner) if str(m.get("rechnungsnummer")) == "2026-001"][0] f.after(200, klick) f._storno_anlegen(meta, ordner) pruefe(gedrueckt["c"], f"der Knopf 'C: nur Storno' ist im Fenster ({gedrueckt['knoepfe']})") pruefe(len(gedrueckt["knoepfe"]) == 4, f"und die Wahl steht in EINEM Schritt: A, B, C, Abbrechen ({gedrueckt['knoepfe']})") storno_datei = [n for n in os.listdir(ordner) if n.startswith("Storno_")] pruefe(len(storno_datei) == 1, f"das Storno-Blatt liegt im Ordner ({storno_datei})") # ---- Was auf dem Blatt steht --------------------------------------------- pfad = os.path.join(ordner, storno_datei[0]) m = BE.kenndaten_lesen(pfad) or {} pruefe(m.get("art") == "storno", f"art=storno ({m.get('art')!r})") pruefe(m.get("nur_storno") is True, f"die Kenndaten sagen 'nur_storno' ({m.get('nur_storno')!r})") pruefe(not m.get("korrektur_nummer"), f"KEINE Folgenummer in den Kenndaten ({m.get('korrektur_nummer')!r})") pruefe(m.get("ersatz_vorhanden") is False, f"und kein Verweis auf eine bleibende Rechnung ({m.get('ersatz_vorhanden')!r})") pruefe(str(m.get("naechte")) == "-3", f"der Storno traegt -3 Naechte ({m.get('naechte')!r})") from pypdf import PdfReader # noqa: E402 text = " ".join((s.extract_text() or "") for s in PdfReader(pfad).pages).replace("\n", " ") pruefe("Die Leistung wird nicht abgerechnet" in text, "auf dem Blatt steht, dass die Leistung nicht abgerechnet wird") pruefe("berichtigte Rechnung" not in text and "Berichtigte Rechnung" not in text, "und KEIN Wort von einer berichtigten Rechnung, die nie kommt") pruefe("bereits mit der Rechnung" not in text, "und kein Verweis auf eine andere Rechnung") # ---- Nummern und offener Vorgang ----------------------------------------- pruefe(f.storno_stand is None, "es bleibt KEIN Vorgang offen - das Formular wartet auf nichts") pruefe(f.cfg.get("rechnung_zaehler") == zaehler_vorher + 1, f"nur die Storno-Nummer ist vergeben, keine zweite reserviert " f"({zaehler_vorher} -> {f.cfg.get('rechnung_zaehler')})") # ---- Und was das Steuerjournal daraus macht ------------------------------ sys.path.insert(0, JOURNAL) import ablage # noqa: E402 ablage.daten_ordner = lambda: tmp from db import Journal # noqa: E402 from modell import summiere, aufgehobene # noqa: E402 j = Journal(os.path.join(tmp, "journal.sqlite3")) j.scanne(ordner, 5.0, voll=True) buchungen = j.buchungen(jahr=2026) weg = aufgehobene(buchungen) # Verrechnet, nicht weggelassen (Entscheidung vom 04.09.2026, siehe # modell.summiere im Steuerjournal): beide Zeilen stehen mit ihrem Vorzeichen # in der Summe und heben sich gegenseitig auf. s = summiere(buchungen) print("--- was im Journal steht ---") for b in buchungen: print(f" {b.voll_nummer} {b.nachname:10s} {b.naechte:>3} Naechte {b.gezahlt:>8.2f}") pruefe(len(buchungen) == 2, f"Rechnung und Storno stehen beide im Journal ({len(buchungen)})") pruefe(len(weg) == 1, f"die Rechnung ist als aufgehoben erkannt ({len(weg)})") pruefe(s.naechte == 0 and abs(s.gezahlt) < 0.005, f"unterm Strich bleibt nichts uebrig: 0 Naechte, 0,00 Euro " f"(ist {s.naechte}, {s.gezahlt})") pruefe(s.buchungen == 2, f"gezaehlt werden aber beide Belege - verrechnet, nicht weggelassen " f"(ist {s.buchungen})") j.schliessen() f.destroy() _wecker.cancel() shutil.rmtree(tmp, ignore_errors=True) print("---") print("alles gruen" if not fehler else f"{len(fehler)} FEHLER") sys.exit(1 if fehler else 0)