Storno: Version C - nur Storno, es folgt nichts

Seine Ansage: "des wegen sollte mann auch eine version c mit only storno
machen weil das gibt es auch".

Bisher gab es nur zwei Wege aus dem Storno:
  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 auf eine Rechnung, die nie
kommt, und es waere unnoetig eine Nummer reserviert; mit B muesste man
auf eine Rechnung verweisen, die es nicht gibt.

C steht als dritter Knopf im selben Schritt (A, B, C, Abbrechen) - kein
zweiter Schritt, weil es nichts einzutippen gibt.

- keine Folgenummer reserviert (die fortlaufende Nummernfolge bekaeme
  sonst eine Luecke, § 14 Abs. 4 Nr. 4 UStG)
- kein offener Vorgang, das Formular wartet auf nichts
- eigener Satz auf dem Blatt: "Die Leistung wird nicht abgerechnet - es
  folgt keine weitere Rechnung. Bereits gezahlte Betraege werden
  erstattet." Kein Wort von einer berichtigten Rechnung
- Kenndaten tragen "nur_storno": true, damit das Steuerjournal den Fall
  auseinanderhalten kann

Pruefstand pruef_nur_storno.py (neu, 15 Pruefungen) drueckt den Knopf am
echten Fenster und sieht danach Blatt, Kenndaten, Nummernzaehler und das
Steuerjournal an (Rechnung + Storno stehen drin, gemeldet wird nichts).
pruef_storno (A), pruef_doppelte_leistung (B), pruef_kette und
pruef_nummernsperre laufen unveraendert gruen.

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 00:53:23 +02:00
parent 1107a9bace
commit 01f6a127c2
3 changed files with 247 additions and 4 deletions

202
pruef_nur_storno.py Normal file
View file

@ -0,0 +1,202 @@
# -*- 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)
s = summiere(buchungen, weg)
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(s.buchungen == 0 and s.naechte == 0 and abs(s.gezahlt) < 0.005,
f"gemeldet wird NICHTS: 0 Rechnungen, 0 Naechte, 0,00 Euro "
f"(ist {s.buchungen}, {s.naechte}, {s.gezahlt})")
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)