# -*- coding: utf-8 -*- """Pruefstand: was macht das Steuerjournal mit Storno und Berichtigung? Seit dem 02.09.2026 kann das Rechnungstool drei Sorten Blatt schreiben: Rechnung - die Uebernachtung, mit Beherbergungssteuer Storno - hebt sie auf, alle Betraege NEGATIV Berichtigung - reicht nur die Kundenangaben nach; traegt die Nummer DER RECHNUNG und lauter Nullen Alle drei liegen im selben Ordner, den dieses Programm einliest. Der gefaehrliche Fall ist das Berichtigungsblatt: unter dem alten Schluessel (jahr, rechnungsnummer) hat es die echte Buchung ueberschrieben - im Amtsbericht fehlte die Uebernachtung dann, ohne ein Wort. Geprueft wird an ECHTEN PDFs, die der Renderer des Rechnungstools schreibt. """ import os import shutil import sys import tempfile HIER = r"C:\claude\beherbergungssteuer" TOOL = r"C:\claude\rechnungstool" sys.path.insert(0, HIER) # --- Reissleine: ein Pruefstand darf nie haengen --- import threading as _th def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") os._exit(2) _wecker = _th.Timer(120, _reissleine) _wecker.daemon = True _wecker.start() tmp = tempfile.mkdtemp(prefix="bst_pruef_") ordner = os.path.join(tmp, "rechnungen") os.makedirs(ordner) # ---- PDFs mit dem echten Renderer des Rechnungstools schreiben -------------- sys.path.insert(0, TOOL) import config as tool_config # noqa: E402 shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json")) tool_config.CONFIG_PATH = os.path.join(tmp, "config.json") import berechnung as tb # noqa: E402 import pdf_renderer # noqa: E402 from pruefhelfer import fehler, pruefe cfg_tool = tool_config.load_config() def schreibe(datei, nummer, name, datum, tage=3, **extra): r = tb.berechne(cfg_tool["katalog"], {1: {"anzahl": tage, "personen": 0}}, cfg_tool) kopf = {"rechnungsnummer": nummer, "datum": datum, "anreise": "", "abreise": "", "naechte": str(tage), "nachname": name, "vorname": "Anna", "kunde_zeilen": ["", f"Anna {name}", "", "Musterweg 1", "12345 Musterstadt"]} kopf.update(extra) pdf_renderer.render_rechnung(os.path.join(ordner, datei), cfg_tool, r, kopf) return r rechnung = schreibe("Rechnung_2026-014_Meier.pdf", "2026-014", "Meier", "12.08.2026", 3) steuer_soll = round(sum(a.betrag for a in rechnung.aufschlaege), 2) # Berichtigungsblatt zu genau dieser Rechnung - traegt DEREN Nummer schreibe("Berichtigte Rechnung_2026-08-20_2026-014.pdf", "2026-014", "Meier", "12.08.2026", 3, art="berichtigung", berichtigt_zu="2026-014", berichtigt_am="20.08.2026", vorgang="2026-014") # und der Altbestand: dieselbe Nummer zweimal, verschiedene Gaeste schreibe("Rechnung_2026-020_Erst.pdf", "2026-020", "Erst", "01.08.2026", 2) schreibe("Rechnung_2026-020_Zweit.pdf", "2026-020", "Zweit", "05.08.2026", 4) # ---- jetzt das Steuerjournal ----------------------------------------------- sys.path.remove(TOOL) for name in ("config", "berechnung"): sys.modules.pop(name, None) import db as db_mod # noqa: E402 journal = db_mod.Journal(os.path.join(tmp, "journal.sqlite3")) neu, akt, uebersprungen, kaputt = journal.scanne(ordner, 5.0, voll=True) # Direkt aus der Tabelle lesen - so sieht man auch, was WIRKLICH gespeichert # wurde, nicht nur, was das Modell daraus macht. buchungen = [dict(r) for r in journal.con.execute( "SELECT * FROM buchungen WHERE jahr=2026 ORDER BY rechnungsnummer, id")] print("--- was im Journal steht ---") for b in buchungen: print(f" {b['rechnungsnummer']:>6} {b['datum']} {b['nachname']:10} " f"{b['naechte']} Nächte {b['gezahlt']:8.2f} {os.path.basename(b['pdf_pfad'])}") nummern = [b["rechnungsnummer"] for b in buchungen] # 1. Das Berichtigungsblatt darf die Rechnung NICHT ueberschrieben haben zu_014 = [b for b in buchungen if b["rechnungsnummer"] in ("014", "2026-014")] pruefe(len(zu_014) == 1, f"zu 2026-014 steht genau EINE Buchung im Journal ({len(zu_014)})") pruefe(zu_014 and zu_014[0]["naechte"] == 3, f"und sie hat die 3 Nächte der Rechnung, nicht 0 ({zu_014[0]['naechte'] if zu_014 else '-'})") pruefe(zu_014 and zu_014[0]["gezahlt"] > 0, f"und einen Betrag > 0 ({zu_014[0]['gezahlt'] if zu_014 else '-'})") pruefe(zu_014 and "Berichtigte" not in os.path.basename(zu_014[0]["pdf_pfad"]), "die Buchung stammt aus der RECHNUNG, nicht aus dem Berichtigungsblatt") # 2. Zwei Rechnungen mit derselben Nummer muessen BEIDE dastehen zu_020 = [b for b in buchungen if b["rechnungsnummer"] in ("020", "2026-020")] pruefe(len(zu_020) == 2, f"beide Rechnungen mit 2026-020 stehen im Journal ({len(zu_020)})") pruefe({b["nachname"] for b in zu_020} == {"Erst", "Zweit"}, f"mit beiden Gästen ({[b['nachname'] for b in zu_020]})") # 3. Die Beherbergungssteuer der Rechnung ist vollstaendig da summe = round(sum(b["gezahlt"] * b["satz"] / 100 for b in buchungen), 2) pruefe(summe > 0, f"Steuer über alle Buchungen: {summe:.2f} € (Rechnung allein {steuer_soll:.2f} €)") journal.schliessen() if not fehler: import gc as _gc import sqlite3 as _sq for _obj in list(_gc.get_objects()): if isinstance(_obj, _sq.Connection): try: _obj.close() except Exception: # noqa: BLE001 pass shutil.rmtree(tmp, ignore_errors=True) print("---") print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}") sys.exit(1 if fehler else 0)