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:
parent
1107a9bace
commit
01f6a127c2
3 changed files with 247 additions and 4 deletions
38
korrektur.py
38
korrektur.py
|
|
@ -341,13 +341,21 @@ class KorrekturMixin:
|
|||
|
||||
A - es folgt eine neue Rechnung (der bisherige Weg)
|
||||
B - die Leistung ist schon abgerechnet -> Nummer der BLEIBENDEN
|
||||
C - nur Storno: es folgt nichts und es gibt keine andere Rechnung
|
||||
|
||||
Sein Fall: mit dem alten Programm wurde bei einem Fehler einfach eine
|
||||
NEUE Rechnung geschrieben. Jetzt liegt dieselbe Leistung mehrfach im
|
||||
Ordner, und die Ueberzaehligen muessen auf die Rechnung verweisen, die
|
||||
BLEIBT - sonst wartet der Gast auf eine Rechnung, die nie kommt.
|
||||
|
||||
Rueckgabe: ("neu", None) | ("vorhanden", "2026-338") | None (Abbruch).
|
||||
C kam am 04.09.2026 dazu (*"des wegen sollte man auch eine version c mit
|
||||
only storno machen weil das gibt es auch"*): die Leistung wird gar nicht
|
||||
abgerechnet - Buchung geplatzt, Rechnung komplett irrtuemlich. Ohne C
|
||||
muesste man A waehlen und die versprochene neue Rechnung bliebe ewig
|
||||
offen, oder B und muesste auf eine Rechnung verweisen, die es nicht gibt.
|
||||
|
||||
Rueckgabe: ("neu", None) | ("vorhanden", "2026-338") | ("nur", None)
|
||||
| None (Abbruch).
|
||||
"""
|
||||
# Im Testlauf klickt niemand: dann gilt der uebliche Weg. Der Pruefstand,
|
||||
# der GENAU DIESES Fenster prueft, setzt _storno_dialog_zeigen.
|
||||
|
|
@ -384,7 +392,13 @@ class KorrekturMixin:
|
|||
font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
||||
ttk.Label(schritt1, wraplength=620, justify="left", text=(
|
||||
"Für Doppelte: derselbe Aufenthalt steht mehrfach im Ordner. Im nächsten Schritt "
|
||||
"trägst du die Nummer der Rechnung ein, die bleibt.")).pack(anchor="w", pady=(2, 0))
|
||||
"trägst du die Nummer der Rechnung ein, die bleibt.")).pack(anchor="w", pady=(2, 10))
|
||||
ttk.Label(schritt1, text="C – nur Storno",
|
||||
font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
||||
ttk.Label(schritt1, wraplength=620, justify="left", text=(
|
||||
"Die Leistung wird gar nicht abgerechnet: Buchung geplatzt oder die Rechnung "
|
||||
"war komplett irrtümlich. Es folgt keine neue Rechnung und es gibt auch keine "
|
||||
"andere, auf die verwiesen wird.")).pack(anchor="w", pady=(2, 0))
|
||||
|
||||
# ---- Schritt 2: die Nummer (erst nach B) --------------------------
|
||||
schritt2 = ttk.Frame(rahmen)
|
||||
|
|
@ -446,6 +460,10 @@ class KorrekturMixin:
|
|||
wahl["art"] = "neu"
|
||||
d.destroy()
|
||||
|
||||
def nimm_nur():
|
||||
wahl["art"] = "nur"
|
||||
d.destroy()
|
||||
|
||||
def uebernehmen():
|
||||
nr = suchen()
|
||||
if not nr:
|
||||
|
|
@ -484,6 +502,8 @@ class KorrekturMixin:
|
|||
command=nimm_neu).pack(side="left")
|
||||
ttk.Button(knoepfe, text="B: schon abgerechnet", width=22,
|
||||
command=zu_schritt2).pack(side="left", padx=8)
|
||||
ttk.Button(knoepfe, text="C: nur Storno", width=16,
|
||||
command=nimm_nur).pack(side="left")
|
||||
ttk.Button(knoepfe, text="Abbrechen", width=14,
|
||||
command=d.destroy).pack(side="right")
|
||||
|
||||
|
|
@ -497,7 +517,9 @@ class KorrekturMixin:
|
|||
self.wait_window(d)
|
||||
if wahl["art"] == "vorhanden":
|
||||
return "vorhanden", wahl["nummer"]
|
||||
return ("neu", None) if wahl["art"] == "neu" else None
|
||||
if wahl["art"] in ("neu", "nur"):
|
||||
return wahl["art"], None
|
||||
return None
|
||||
|
||||
def _storno_anlegen(self, meta, out_dir):
|
||||
"""Schreibt die Storno-PDF und füllt das Formular für die berichtigte Rechnung."""
|
||||
|
|
@ -528,6 +550,11 @@ class KorrekturMixin:
|
|||
# Es kommt keine neue Rechnung - das Storno verweist auf die,
|
||||
# die bleibt. Es wird also auch keine Nummer dafuer reserviert.
|
||||
folge_nummer = vorhandene
|
||||
elif art_folge == "nur":
|
||||
# Nur Storno: es folgt nichts, es gibt auch nichts, worauf zu
|
||||
# verweisen waere. Keine Nummer reservieren - sonst reisst die
|
||||
# fortlaufende Nummernfolge eine Luecke (Paragraph 14 Abs. 4 Nr. 4 UStG).
|
||||
folge_nummer = ""
|
||||
else:
|
||||
folge_nummer = naechste_freie_nummer_nach(out_dir, self.cfg, nummer,
|
||||
self._belegte_nummern())
|
||||
|
|
@ -579,6 +606,9 @@ class KorrekturMixin:
|
|||
# Sagt dem Blatt, ob die Nummer eine BLEIBENDE Rechnung meint
|
||||
# ("bereits abgerechnet mit") oder eine, die noch kommt.
|
||||
"ersatz_vorhanden": art_folge == "vorhanden",
|
||||
# Sagt dem Blatt: es kommt NICHTS nach. Ohne das stuende dort der
|
||||
# Satz von der "berichtigten Rechnung", die es nie geben wird.
|
||||
"nur_storno": art_folge == "nur",
|
||||
"storno_datum": str(meta.get("datum") or ""),
|
||||
"kunde": kunde,
|
||||
}
|
||||
|
|
@ -596,7 +626,7 @@ class KorrekturMixin:
|
|||
|
||||
# Nur wenn eine neue Rechnung folgt, bleibt ein Vorgang offen. Bei
|
||||
# "schon abgerechnet" ist mit dem Storno alles getan.
|
||||
self.storno_stand = None if art_folge == "vorhanden" else {
|
||||
self.storno_stand = None if art_folge in ("vorhanden", "nur") else {
|
||||
"pfad": pfad, "nummer": nummer, "zu": alt_nummer,
|
||||
"vorgang": str(meta.get("vorgang") or alt_nummer),
|
||||
"zu_datum": str(meta.get("datum") or ""),
|
||||
|
|
|
|||
|
|
@ -494,6 +494,16 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
|
|||
"verrechnet.", st["dank"]))
|
||||
story.append(KeepTogether(block))
|
||||
return
|
||||
if kopf.get("nur_storno"):
|
||||
# Nur Storno (04.09.2026): die Leistung wird gar nicht abgerechnet.
|
||||
# Hier darf NICHT von einer berichtigten Rechnung die Rede sein -
|
||||
# es kommt keine, und der Gast wartet sonst darauf.
|
||||
block.append(Paragraph(
|
||||
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
|
||||
"Die Leistung wird nicht abgerechnet – es folgt keine weitere "
|
||||
"Rechnung. Bereits gezahlte Beträge werden erstattet.", st["dank"]))
|
||||
story.append(KeepTogether(block))
|
||||
return
|
||||
kette = (f" Die berichtigte Rechnung trägt die Nummer {folgt} "
|
||||
f"(also {kopf.get('storno_zu','')} → {kopf.get('rechnungsnummer','')} "
|
||||
f"→ {folgt}).") if folgt else ""
|
||||
|
|
@ -631,6 +641,7 @@ def _kenndaten(rechnung, kopf):
|
|||
"storno_zu": kopf.get("storno_zu", ""),
|
||||
"korrektur_nummer": kopf.get("korrektur_nummer", ""),
|
||||
"ersatz_vorhanden": bool(kopf.get("ersatz_vorhanden")),
|
||||
"nur_storno": bool(kopf.get("nur_storno")),
|
||||
"storno_datum": kopf.get("storno_datum", ""),
|
||||
"ersetzt": kopf.get("ersetzt", ""),
|
||||
"storno_nummer": kopf.get("storno_nummer", ""),
|
||||
|
|
|
|||
202
pruef_nur_storno.py
Normal file
202
pruef_nur_storno.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue