Zwei Ansagen von ihm, beide am selben Blatt:
1. "weil die nummer hast nur auf der storno selber aber nicht am
stornierten nummer um die es geht" - die aufgehobene Rechnung stand
nur als "storniert" da. Jetzt steht dort "storniert mit 2026-901".
Damit ist das Paar in BEIDE Richtungen lesbar, und das zaehlt vor
allem dann, wenn der Storno in einem anderen Monat liegt und auf
diesem Blatt gar nicht auftaucht.
2. "mach eine neue spalte mit grund oder so was" - der Vermerk stand als
zweite Zeile unter der Rechnungsnummer. Jetzt ist es eine eigene
Spalte "Grund" zwischen Rechnung und Name, eine Zeile je Buchung.
Feste Breiten neu verteilt (20/20/34 mm + Rest fuer den Namen), damit
nichts umbricht.
Dafuer neu: modell.aufhebungen() gibt {id der Rechnung: Storno-Buchung}
statt nur der ids - die Anzeige will auch sagen, WOMIT storniert wurde,
nicht nur DASS. aufgehobene() bleibt als duenne Huelle darueber, damit
zeig_stornos und doppelte_nummern unveraendert weiterlaufen.
Fenster und Blatt zeigen wieder dieselben Worte.
pruef_storno_verrechnet prueft beide Richtungen mit, auch ueber die
Monatsgrenze ("storniert mit 2026-911" im August, der Storno selbst steht
im September). Angesehen: Tabelle als Bild gerendert, alles einzeilig,
auch mit langem Namen. doppelte und berichtigung gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bEsoFUk16DfnNA7MjY36H
184 lines
8.5 KiB
Python
184 lines
8.5 KiB
Python
"""Pruefstand: ein Storno wird VERRECHNET, nicht weggelassen.
|
|
|
|
Entscheidung des Auftraggebers vom 04.09.2026, nachdem beide Wege
|
|
durchgerechnet und die Satzung nachgelesen waren:
|
|
|
|
"ich wuerde das wie beim alten lassen, nicht das er der Stadt zu wenig
|
|
gibt, auch wenn er das so der Steuerberaterin gibt"
|
|
|
|
Heisst: die aufgehobene Rechnung bleibt mit ihrem PLUS im Monat der Rechnung
|
|
stehen, der Storno steht mit seinem MINUS im Monat des Storno-Belegs. Uebers
|
|
Jahr hebt sich das auf. Der andere Weg (beide Zeilen gar nicht zaehlen) war
|
|
zwischenzeitlich gebaut und ist bewusst wieder raus - siehe modell.summiere.
|
|
|
|
⚠️ Dieser Pruefstand haelt genau das fest, damit es niemand (auch ich nicht)
|
|
"repariert": in einem Monat sieht eine verrechnete Doppelung nach einem Fehler
|
|
aus. Sie ist keiner, sie ist gewollt.
|
|
|
|
Gerechnet wird auf einer ECHTEN SQLite ("musst in der sqlite fake rechnungen
|
|
anlegen"), nicht auf zusammengesteckten Objekten. Das echte Journal wird nicht
|
|
angefasst - jeder Lauf legt eine eigene Datei in %TEMP% an.
|
|
|
|
Aufruf: python -u pruef_storno_verrechnet.py
|
|
"""
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
from datetime import date
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HIER)
|
|
|
|
fehler = 0
|
|
|
|
|
|
def pruefe(bedingung, text):
|
|
global fehler
|
|
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 += 1
|
|
|
|
|
|
def _reissleine():
|
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = threading.Timer(180, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
# ---- eigener Datenordner, damit das echte Journal unberuehrt bleibt --------
|
|
tmp = tempfile.mkdtemp(prefix="bst_storno_")
|
|
import ablage # noqa: E402
|
|
ablage.daten_ordner = lambda: tmp
|
|
|
|
from modell import Buchung, summiere, aufgehobene # noqa: E402
|
|
import bericht_pdf # noqa: E402
|
|
import firma as firma_lib # noqa: E402
|
|
from pypdf import PdfReader # noqa: E402
|
|
import app as app_mod # noqa: E402
|
|
from gui_monat import SPALTEN # noqa: E402
|
|
|
|
pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})")
|
|
|
|
# ---- Fake-Rechnungen in die SQLite ----------------------------------------
|
|
# Fall A (August): Rechnung 4 Naechte -> Storno -> Neuausstellung 3 Naechte
|
|
# Fall B (Aug/Sep): Rechnung im August, Storno erst im September
|
|
# dazu eine saubere Rechnung, die nichts angeht
|
|
JAHR = 2026
|
|
FAKE = [
|
|
Buchung(date(JAHR, 8, 22), "900", JAHR, "Muster", 4, 52.0, quelle="manuell"),
|
|
Buchung(date(JAHR, 8, 23), "901", JAHR, "Muster", -4, -52.0, quelle="manuell",
|
|
art="storno", storno_zu="2026-900", storno_datum="22.08.2026"),
|
|
Buchung(date(JAHR, 8, 23), "902", JAHR, "Muster", 3, 39.0, quelle="manuell",
|
|
art="korrektur"),
|
|
Buchung(date(JAHR, 8, 22), "910", JAHR, "Spaet", 4, 52.0, quelle="manuell"),
|
|
Buchung(date(JAHR, 9, 2), "911", JAHR, "Spaet", -4, -52.0, quelle="manuell",
|
|
art="storno", storno_zu="2026-910", storno_datum="22.08.2026"),
|
|
Buchung(date(JAHR, 8, 5), "905", JAHR, "Sauber", 2, 26.0, quelle="manuell"),
|
|
]
|
|
|
|
fenster = app_mod.App()
|
|
fenster.withdraw() # kein Fenster ins Gesicht, gerechnet wird trotzdem
|
|
for b in FAKE:
|
|
fenster.journal.speichern(b)
|
|
fenster.jahr.set(JAHR)
|
|
fenster.aktualisieren()
|
|
fenster.update_idletasks()
|
|
|
|
alle = fenster.journal.buchungen(jahr=JAHR)
|
|
pruefe(len(alle) == 6, f"sechs Fake-Rechnungen stehen in der SQLite ({len(alle)})")
|
|
stornos = [b for b in alle if b.storno_zu]
|
|
pruefe(len(stornos) == 2 and all(b.art == "storno" for b in stornos),
|
|
"die Storno-Spalten haben den Weg durch die DB ueberstanden "
|
|
f"({[(b.voll_nummer, b.art, b.storno_zu) for b in stornos]})")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
print("--- Fall 1: Storno im selben Monat wie seine Rechnung ---")
|
|
fall_a = [b for b in alle if b.nachname == "Muster"]
|
|
s = summiere(fall_a)
|
|
pruefe(s.naechte == 3, f"3 Uebernachtungen: 4 - 4 + 3 (ist {s.naechte})")
|
|
pruefe(abs(s.gezahlt - 39.0) < 0.005, f"39,00 Euro: 52 - 52 + 39 (ist {s.gezahlt})")
|
|
pruefe(abs(s.steuer - 1.95) < 0.005, f"1,95 Euro Steuer (ist {s.steuer})")
|
|
pruefe(s.buchungen == 3,
|
|
f"gezaehlt werden alle drei Belege - Rechnung, Storno, Neuausstellung "
|
|
f"(ist {s.buchungen})")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
print("--- Fall 2: Rechnung im August, Storno erst im September ---")
|
|
aug = summiere([b for b in alle if b.monat == 8])
|
|
sep = summiere([b for b in alle if b.monat == 9])
|
|
pruefe(aug.naechte == 9 and abs(aug.gezahlt - 117.0) < 0.005,
|
|
f"der August meldet die Doppelung MIT: 9 Naechte, 117,00 Euro "
|
|
f"(ist {aug.naechte}, {aug.gezahlt})")
|
|
pruefe(sep.naechte == -4 and abs(sep.gezahlt + 52.0) < 0.005,
|
|
f"der September zieht sie ab: -4 Naechte, -52,00 Euro "
|
|
f"(ist {sep.naechte}, {sep.gezahlt})")
|
|
jahr = summiere(alle)
|
|
pruefe(jahr.naechte == 5 and abs(jahr.gezahlt - 65.0) < 0.005,
|
|
f"uebers JAHR hebt es sich auf: 5 Naechte, 65,00 Euro "
|
|
f"(ist {jahr.naechte}, {jahr.gezahlt})")
|
|
pruefe(abs(aug.steuer + sep.steuer - jahr.steuer) < 0.005,
|
|
"und die Steuer der Monate ergibt die Steuer des Jahres")
|
|
|
|
# Die Zuordnung wird weiter gebraucht - fuer die Spalte "Art" und dafuer, dass
|
|
# eine stornierte Rechnung nicht mehr als doppelte Nummer gemeldet wird.
|
|
weg = aufgehobene(alle)
|
|
pruefe({b.voll_nummer for b in alle if b.id in weg} == {"2026-900", "2026-910"},
|
|
"aufgehoben sind genau die beiden stornierten Rechnungen "
|
|
f"({sorted(b.voll_nummer for b in alle if b.id in weg)})")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
print("--- Fall 3: Amtsbericht aus der Datenbank ---")
|
|
pfad = os.path.join(tmp, "August.pdf")
|
|
bericht_pdf.render_monat(pfad, firma_lib.lade(), JAHR, 8, alle)
|
|
# Der Text wird im PDF umbrochen ("storniert mit" / "2026-901") - Leerraum
|
|
# also vereinheitlichen, sonst prueft man den Zeilenumbruch statt den Inhalt.
|
|
text = " ".join((PdfReader(pfad).pages[0].extract_text() or "").split())
|
|
pruefe("2026-900" in text and "2026-901" in text,
|
|
"Rechnung UND Storno stehen im Bericht - die Meldung erklaert sich selbst")
|
|
pruefe("-52,00" in text, "der Storno steht mit seinem Minusbetrag drin")
|
|
pruefe("117,00" in text, "und die Monatssumme ist 117,00 Euro")
|
|
# Ohne Beschriftung steht im Bericht eine Stornonummer neben einer voellig
|
|
# anderen Rechnungsnummer - sein Wort: "musst dann rein schreiben storniert".
|
|
pruefe("storniert mit 2026-901" in text,
|
|
"die aufgehobene Rechnung nennt den Storno, der sie aufhebt")
|
|
pruefe("storniert mit 2026-911" in text,
|
|
"auch dann, wenn dieser Storno in einem ANDEREN Monat liegt")
|
|
pruefe("Storno zu 2026-900" in text, "und der Storno nennt die Rechnung, die er aufhebt")
|
|
pruefe("Neuausstellung" in text, "die neue Rechnung ist als Neuausstellung beschriftet")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
print("--- Fall 4: das echte Fenster ---")
|
|
namen = [sp[0] for sp in SPALTEN]
|
|
i_nr, i_art = namen.index("nummer"), namen.index("art")
|
|
baum = fenster.monate[8].baum
|
|
reihen = [(baum.item(i, "values")[i_nr], baum.item(i, "values")[i_art],
|
|
tuple(baum.item(i, "tags"))) for i in baum.get_children()]
|
|
# 2026-900 (storniert), -901 (Storno), -902 (Neuausstellung), -905, -910 (storniert)
|
|
pruefe(len(reihen) == 5, f"der August zeigt alle fuenf Zeilen ({len(reihen)})")
|
|
pruefe(not any("aufgehoben" in t for _nr, _a, t in reihen),
|
|
f"keine Zeile ist ausgegraut ({sorted({t for _n, _a, t in reihen})})")
|
|
arten = {nr: art for nr, art, _t in reihen}
|
|
pruefe(arten.get("2026-901") == "Storno zu 2026-900",
|
|
f"der Storno ist als Storno getaggt ({arten.get('2026-901')!r})")
|
|
pruefe(arten.get("2026-900") == "storniert mit 2026-901",
|
|
f"die aufgehobene Rechnung nennt ihren Storno ({arten.get('2026-900')!r})")
|
|
pruefe(arten.get("2026-902") == "Neuausstellung",
|
|
f"und die neue als Neuausstellung ({arten.get('2026-902')!r})")
|
|
|
|
leiste = fenster.monate[8].summen.werte
|
|
pruefe(leiste["naechte"].cget("text") == "9",
|
|
f"die Monatsleiste zeigt die verrechneten 9 Naechte "
|
|
f"({leiste['naechte'].cget('text')})")
|
|
|
|
fenster.destroy()
|
|
_wecker.cancel()
|
|
|
|
print("---")
|
|
print("alles gruen" if not fehler else f"{fehler} FEHLER")
|
|
sys.exit(1 if fehler else 0)
|