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
209 lines
7.5 KiB
Python
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)
|