Storno wird wieder VERRECHNET statt weggelassen - seine Entscheidung

Heute Nacht war es andersherum gebaut (v1.6.3): Storno und aufgehobene
Rechnung zaehlten gar nicht mehr mit, die Berichtigung landete im Monat
der Rechnung. Sauber nach § 7 Abs. 5 der Satzung (angemeldet wird die im
Kalendermonat VEREINNAHMTE Steuer), aber nicht das, was er will.

Sein Wort:

    "ich wuerde das wie beim alten lassen, nicht das er der Stadt zu
     wenig gibt"
    "ob er im Sep dann weniger gemacht hat ist doch egal, weil im August
     hat er ja mehr - also gleicht es sich aus, es ist nur ein
     Time-Problem. Aber um was es mir geht: die Steuern sind sauber."

Und er hat recht: der Unterschied ist NUR der Monat, nie das Jahr, und
die Richtung stimmt - zu viel zuerst ist beim Amt nie ein Problem, zu
wenig schon.

Also zurueckgebaut:
- modell.zaehlbar() ist wieder raus, summiere() summiert alle Zeilen mit
  ihrem Vorzeichen. Die Begruendung samt Satzungs-Fundstelle steht im
  Docstring, damit es niemand "repariert" - in EINEM Monat sieht eine
  verrechnete Doppelung naemlich nach einem Fehler aus.
- Amtsbericht zeigt Rechnung und Storno wieder beide, jede in ihrem Monat
  und mit ihrem Vorzeichen. Damit erklaert sich die Meldung von selbst.
- aufgehobene() bleibt, wird aber nur noch fuer die Spalte "Art" und fuer
  die Dubletten-Meldung gebraucht, nicht mehr fuers Rechnen.

Nicht zurueckgebaut (das war eine eigene Ansage): Storno und aufgehobene
Rechnung werden NICHT mehr ausgegraut, sie stehen normal da und sind ueber
die Spalte "Art" gekennzeichnet.

pruef_storno_rechnet_nicht.py ist durch pruef_storno_verrechnet.py
ersetzt: derselbe Aufbau (Fake-Rechnungen in einer echten SQLite, auch
der Fall ueber die Monatsgrenze), aber es haelt jetzt das gewollte
Verhalten fest - August 9 Naechte/117,00, September -4/-52,00, Jahr
5/65,00. Restliche Kette (storno_journal, doppelte, berichtigung,
einzelinstanz, wache) 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 01:09:08 +02:00
parent d9bff8f2ec
commit 13fe1d97f9
6 changed files with 102 additions and 128 deletions

View file

@ -1,189 +0,0 @@
"""Pruefstand: ein Storno wird NICHT mehr mitgerechnet.
Ansage des Auftraggebers (04.09.2026):
"wenn es als storno getagt ist darf der rechner das nicht mehr rechnen,
weil es minus ist - die rechnung gibt es nicht mehr."
Vorher hoben sich Rechnung (+) und Storno (-) in der Summe gegenseitig auf.
Naechte und Steuer kamen dabei richtig heraus, aber:
* die Zahl "Rechnungen" war je Stornofall um ZWEI zu hoch, und
* lag der Storno in einem anderen MONAT als seine Rechnung, stand in jedem
der beiden Monate die Haelfte der Verrechnung allein da (August meldete
eine Uebernachtung zu viel, September eine zu wenig).
Der zweite Fall ist der, den echte Daten nicht hergeben, solange alles im
selben Monat passiert - deshalb steht er hier.
Gerechnet wird auf einer ECHTEN SQLite (Ansage: "musst in der sqlite fake
rechnungen anlegen"), nicht auf zusammengesteckten Objekten: nur so ist belegt,
dass die Storno-Spalten (art, storno_zu, storno_datum) den Weg durch die
Datenbank ueberstehen und dass die ids stimmen, an denen der Bezug haengt.
Das echte Journal wird nicht angefasst - jeder Lauf legt eine eigene Datei
in %TEMP% an.
Aufruf: python -u pruef_storno_rechnet_nicht.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()
# Ab hier wird NUR noch gelesen, was wirklich in der Datenbank steht.
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]})")
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 1: Storno im selben Monat ---")
fall_a = [b for b in alle if b.nachname == "Muster"]
s = summiere(fall_a, weg)
pruefe(s.buchungen == 1, f"gezaehlt wird EINE Rechnung, nicht drei (ist {s.buchungen})")
pruefe(s.naechte == 3, f"3 Uebernachtungen (ist {s.naechte})")
pruefe(abs(s.gezahlt - 39.0) < 0.005, f"39,00 Euro Entgelt (ist {s.gezahlt})")
pruefe(abs(s.steuer - 1.95) < 0.005, f"1,95 Euro Steuer (ist {s.steuer})")
pruefe(len(fall_a) == 3, "geloescht wird nichts - alle drei Zeilen bleiben in der DB")
# --------------------------------------------------------------------------
print("--- Fall 2: Rechnung im August, Storno im September ---")
aug = summiere([b for b in alle if b.monat == 8], weg)
sep = summiere([b for b in alle if b.monat == 9], weg)
pruefe(aug.buchungen == 2 and aug.naechte == 5,
"August: 2 Rechnungen (Neuausstellung + Sauber), 5 Naechte "
f"(ist {aug.buchungen}, {aug.naechte})")
pruefe(sep.buchungen == 0 and sep.naechte == 0,
"September meldet nichts - der Storno ist keine Buchung "
f"(ist {sep.buchungen}, {sep.naechte})")
pruefe(abs(aug.steuer - 3.25) < 0.005 and sep.steuer == 0,
f"und die Steuer steht im richtigen Monat ({aug.steuer} / {sep.steuer})")
# Gegenprobe: ein Storno, der auf eine fremde Nummer zeigt, hebt nichts auf.
fenster.journal.speichern(
Buchung(date(JAHR, 9, 3), "912", JAHR, "Fremd", -3, -39.0, quelle="manuell",
art="storno", storno_zu="2026-999", storno_datum="01.01.2026"))
mit_fremd = fenster.journal.buchungen(jahr=JAHR)
weg2 = aufgehobene(mit_fremd)
pruefe({b.voll_nummer for b in mit_fremd if b.id in weg2} == {"2026-900", "2026-910"},
"ein Storno hebt nur SEINE Rechnung auf, nicht irgendeine")
pruefe(summiere([b for b in mit_fremd if b.monat == 9], weg2).buchungen == 0,
"und ein Storno ins Leere zaehlt trotzdem nicht mit")
# --------------------------------------------------------------------------
print("--- Fall 3: Amtsbericht aus der Datenbank ---")
pfad = os.path.join(tmp, "August.pdf")
bericht_pdf.render_monat(pfad, firma_lib.lade(), JAHR, 8, mit_fremd)
text = (PdfReader(pfad).pages[0].extract_text() or "").replace("\n", " ")
pruefe("2026-900" not in text, "die aufgehobene Rechnung steht nicht im Bericht")
pruefe("2026-901" not in text, "der Storno steht nicht im Bericht")
pruefe("2026-910" not in text, "auch die im September stornierte Rechnung nicht")
pruefe("2026-902" in text and "2026-905" in text, "die gueltigen Rechnungen stehen drin")
pruefe("-52" not in text, "und keine Minusbetraege mehr auf dem Blatt fuers Amt")
# --------------------------------------------------------------------------
print("--- Fall 4: das echte Fenster ---")
fenster.aktualisieren()
fenster.update_idletasks()
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 weiter 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",
f"die aufgehobene Rechnung ist als storniert getaggt ({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["rechnungen"].cget("text") == "2",
f"die Monatsleiste zeigt 2 Rechnungen ({leiste['rechnungen'].cget('text')})")
pruefe(leiste["naechte"].cget("text") == "5",
f"und 5 Uebernachtungen ({leiste['naechte'].cget('text')})")
pruefe(fenster.jahr_summe.werte["rechnungen"].cget("text") == "2",
f"die Jahresleiste ebenso ({fenster.jahr_summe.werte['rechnungen'].cget('text')})")
fenster.destroy()
_wecker.cancel()
print("---")
print("alles gruen" if not fehler else f"{fehler} FEHLER")
sys.exit(1 if fehler else 0)