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:
parent
d9bff8f2ec
commit
13fe1d97f9
6 changed files with 102 additions and 128 deletions
174
pruef_storno_verrechnet.py
Normal file
174
pruef_storno_verrechnet.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""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)
|
||||
text = (PdfReader(pfad).pages[0].extract_text() or "").replace("\n", " ")
|
||||
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, f"und die Monatssumme ist 117,00 Euro")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
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",
|
||||
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["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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue