rechnungstool/pruef_nur_storno.py
TheMockTv 37b5c60266 Storno-Erklaerung steht jetzt auch auf dem Amtsblatt
Sein Fund: "in der pdf wird der satz nicht angezeigt, da ist dann ein
sprung in den rechnungs nummern".

Die Schlusstexte waren komplett auf die Kundenseite beschraenkt ("auf dem
Amt-Blatt unnoetig"). Fuer Dank- und Zahlungstexte stimmt das. Fuer die
STORNO-Erklaerung nicht: auf Seite 2 stand dann eine Stornonummer
(2026-456) neben einer voellig anderen Rechnungsnummer (2026-342), und
das Blatt, das die Stadt bekommt, sagte nirgends warum.

Jetzt traegt auch das Amtsblatt den Satz - "hebt die Rechnung X vom
Y vollstaendig auf, ist bereits mit Z abgerechnet" bzw. bei Version C
"die Leistung wird nicht abgerechnet". Die Beherbergungssteuer-Zeile
bleibt dort weiterhin aussen vor, die gehoert da nicht hin.

pruef_nur_storno an die Verrechnung angepasst (das Steuerjournal laesst
Storno und aufgehobene Rechnung wieder mitzaehlen, sie heben sich
gegenseitig auf): unterm Strich 0 Naechte und 0,00 Euro, gezaehlt werden
aber beide Belege. A und B unveraendert gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
2026-09-04 01:19:50 +02:00

209 lines
7.5 KiB
Python

# -*- 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)
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kodierung = sys.stdout.encoding or "utf-8"
print(zeile.encode(kodierung, "replace").decode(kodierung))
if not bedingung:
fehler.append(text)
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
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)