Doppelte Rechnungsnummern werden ANGEZEIGT statt still mitgeschleppt: rote Zeile mit Warnzeichen, markierter Monatsreiter, rote Zeile unter der Jahressumme und eine Liste aller Belege mit Dateinamen. Gerechnet wird ueber das ganze Jahr - liegt die zweite Rechnung im naechsten Monat, sieht kein Monat fuer sich eine Dublette, das Amt bekommt sie trotzdem doppelt. Die Betraege bleiben in allen Summen und im Amtsbericht; gemeldet wird der Fund, korrigiert wird nichts von allein. Vorgeschichte: bis v1.2 war die Rechnungsnummer der Schluessel der Tabelle - die zweite Rechnung ueberschrieb die erste still und fehlte im Amtsbericht. v1.3 hat den Schluessel auf die PDF-Datei umgestellt, seitdem bleiben beide Zeilen stehen. Erst jetzt sieht man den Fall auch. Einzelinstanz aus dem Steuerrechner uebernommen (nur Mutex- und PID-Name geaendert): zwei Fenster lesen denselben Rechnungsordner in dieselbe journal.sqlite3 - was das eine loescht, steht im anderen noch da, und wer dort "PDF fuers Amt" drueckt, meldet einen Stand, den es nicht mehr gibt. Die Sperre greift vor ablage.daten_ordner() und nur beim echten Start. "Neue Buchung" ist raus: jede Zeile gehoert zu einer Rechnung aus dem Rechnungstool. Wer eine Buchung braucht, schreibt die Rechnung. Bestehende Zeilen lassen sich weiter bearbeiten. Pruefstaende: pruef_doppelte.py (27 Pruefungen am echten Fenster), pruef_einzelinstanz.py (echte Prozesse). Beide gruen, dazu Abnahme mit den Augen an Hauptfenster, Liste und Bearbeiten-Dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
186 lines
7.6 KiB
Python
186 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: zeigt das Steuerjournal doppelt vergebene Rechnungsnummern an?
|
|
|
|
Seit dem 03.09.2026 ist der Schluessel der Tabelle die PDF-Datei und nicht mehr
|
|
die Rechnungsnummer (siehe db._alte_nummern_sperre_loesen). Damit bleiben zwei
|
|
Rechnungen mit derselben Nummer beide stehen - was sie stillschweigend tun
|
|
wuerden, waere die Uebernachtung doppelt in der Meldung ans Amt. Genau deshalb
|
|
muss die Oberflaeche den Fund ZEIGEN.
|
|
|
|
Die Falle, die ein Test je Monat nicht faengt: dieselbe Nummer in ZWEI Monaten.
|
|
Kein Monat sieht fuer sich eine Dublette, das Jahr aber schon - deshalb wird
|
|
ueber das ganze Jahr gerechnet.
|
|
|
|
Geprueft wird am echten Fenster (App + MonatsTab), nicht nur am Rechenkern:
|
|
"rot faerben" ist eine Behauptung ueber die Anzeige.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
from datetime import date
|
|
|
|
HIER = r"C:\claude\beherbergungssteuer"
|
|
sys.path.insert(0, HIER)
|
|
|
|
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)")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = threading.Timer(120, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
# ---- eigener Datenordner, damit das echte Journal unberuehrt bleibt --------
|
|
tmp = tempfile.mkdtemp(prefix="bst_doppelt_")
|
|
import ablage # noqa: E402
|
|
|
|
ablage.daten_ordner = lambda: tmp
|
|
|
|
import app as app_mod # noqa: E402 (liest ablage.daten_ordner beim Import)
|
|
from modell import Buchung, doppelte_nummern, summiere # noqa: E402
|
|
|
|
pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})")
|
|
|
|
# ---- Buchungen: der Fall, den das Amt nicht sehen darf ---------------------
|
|
JAHR = 2026
|
|
BUCHUNGEN = [
|
|
# zwei verschiedene Gaeste unter EINER Nummer, im selben Monat
|
|
Buchung(date(JAHR, 8, 1), "020", JAHR, "Erst", 2, 100.0, quelle="manuell"),
|
|
Buchung(date(JAHR, 8, 5), "020", JAHR, "Zweit", 4, 200.0, quelle="manuell"),
|
|
# dieselbe Nummer, aber ueber die Monatsgrenze hinweg - die eigentliche Falle
|
|
Buchung(date(JAHR, 8, 20), "030", JAHR, "August", 1, 50.0, quelle="manuell"),
|
|
Buchung(date(JAHR, 9, 2), "030", JAHR, "September", 3, 150.0, quelle="manuell"),
|
|
# und eine saubere Rechnung, die nicht angefasst werden darf
|
|
Buchung(date(JAHR, 8, 9), "021", JAHR, "Sauber", 2, 80.0, quelle="manuell"),
|
|
]
|
|
|
|
fenster = app_mod.App()
|
|
fenster.withdraw() # kein Fenster ins Gesicht, gerechnet wird trotzdem
|
|
for b in BUCHUNGEN:
|
|
fenster.journal.speichern(b)
|
|
fenster.jahr.set(JAHR)
|
|
fenster.aktualisieren()
|
|
fenster.update_idletasks()
|
|
|
|
alle = fenster.journal.buchungen(jahr=JAHR)
|
|
pruefe(len(alle) == len(BUCHUNGEN), f"alle {len(BUCHUNGEN)} Buchungen stehen im Journal ({len(alle)})")
|
|
|
|
# ---- 1. Rechenkern ---------------------------------------------------------
|
|
d = doppelte_nummern(alle)
|
|
pruefe(set(d) == {"2026-020", "2026-030"}, f"genau die zwei Dubletten gefunden ({sorted(d)})")
|
|
pruefe(all(len(v) == 2 for v in d.values()), "je zwei Belege pro Nummer")
|
|
pruefe("2026-021" not in d, "die saubere Rechnung wird nicht gemeldet")
|
|
pruefe(not doppelte_nummern([b for b in alle if b.rechnungsnummer == "021"]),
|
|
"ohne Dubletten bleibt die Meldung leer")
|
|
|
|
# ---- 2. Die Betraege bleiben vollstaendig ---------------------------------
|
|
s = summiere(alle)
|
|
pruefe(abs(s.gezahlt - 580.0) < 0.005, f"Jahressumme unveraendert 580,00 ({s.gezahlt})")
|
|
pruefe(abs(s.steuer - 29.0) < 0.005, f"Steuer unveraendert 29,00 ({s.steuer})")
|
|
|
|
|
|
def zeilen(monat):
|
|
"""Was im Monatsblatt WIRKLICH steht: (Nachname, Nummerspalte, Tags)."""
|
|
baum = fenster.monate[monat].baum
|
|
aus = []
|
|
for iid in baum.get_children():
|
|
w = baum.item(iid, "values")
|
|
aus.append((w[3], w[2], tuple(baum.item(iid, "tags"))))
|
|
return aus
|
|
|
|
|
|
# ---- 3. Anzeige: rot + Warnzeichen ----------------------------------------
|
|
aug = zeilen(8)
|
|
pruefe(len(aug) == 4, f"August zeigt vier Zeilen ({len(aug)})")
|
|
rot_aug = {name for name, _nr, tags in aug if "doppelt" in tags}
|
|
pruefe(rot_aug == {"Erst", "Zweit", "August"}, f"im August sind die richtigen rot ({sorted(rot_aug)})")
|
|
pruefe(all(nr.startswith("\u26a0") for name, nr, tags in aug if "doppelt" in tags),
|
|
"die roten Zeilen tragen das Warnzeichen vor der Nummer")
|
|
sauber = [z for z in aug if z[0] == "Sauber"][0]
|
|
pruefe("doppelt" not in sauber[2] and not sauber[1].startswith("\u26a0"),
|
|
f"die saubere Rechnung bleibt schwarz und ohne Zeichen ({sauber})")
|
|
|
|
# ---- 4. Die Falle: dieselbe Nummer in zwei Monaten ------------------------
|
|
sep = zeilen(9)
|
|
pruefe(len(sep) == 1 and "doppelt" in sep[0][2],
|
|
f"September markiert seine Haelfte der Dublette ({sep})")
|
|
titel = [fenster.blaetter.tab(m - 1, "text") for m in range(1, 13)]
|
|
pruefe(titel[7].startswith("\u26a0") and titel[8].startswith("\u26a0"),
|
|
f"beide Reiter sind markiert ({titel[7]!r}, {titel[8]!r})")
|
|
pruefe(not any(t.startswith("\u26a0") for i, t in enumerate(titel) if i not in (7, 8)),
|
|
f"kein anderer Reiter ist markiert ({[t for t in titel if t.startswith(chr(0x26a0))]})")
|
|
|
|
# ---- 5. Die Warnleiste --------------------------------------------------
|
|
pruefe(bool(fenster.doppelt_leiste.winfo_manager()), "die Warnleiste ist eingeblendet")
|
|
text = fenster.warnung.cget("text")
|
|
pruefe("2026-020" in text and "2026-030" in text, f"sie nennt beide Nummern ({text[:70]}…)")
|
|
pruefe("Erst" in text and "Zweit" in text, "und die betroffenen Gaeste")
|
|
|
|
# ---- 6. Die Suche darf die Dublette nicht verstecken ---------------------
|
|
fenster.suche.set("Zweit")
|
|
fenster.update_idletasks()
|
|
gefiltert = zeilen(8)
|
|
pruefe(len(gefiltert) == 1 and "doppelt" in gefiltert[0][2],
|
|
f"auch im Suchtreffer bleibt die Zeile rot ({gefiltert})")
|
|
fenster.suche.set("")
|
|
fenster.update_idletasks()
|
|
|
|
# ---- 7. Das Fenster mit der Liste ---------------------------------------
|
|
vorher = set(fenster.winfo_children())
|
|
fenster.doppelte_zeigen()
|
|
fenster.update_idletasks()
|
|
neu = [w for w in fenster.winfo_children() if w not in vorher]
|
|
pruefe(len(neu) == 1, f"die Liste geht auf ({len(neu)} Fenster)")
|
|
if neu:
|
|
inhalt = ""
|
|
def sammeln(w):
|
|
global inhalt
|
|
import tkinter as tk
|
|
if isinstance(w, tk.Text):
|
|
inhalt += w.get("1.0", "end")
|
|
for k in w.winfo_children():
|
|
sammeln(k)
|
|
sammeln(neu[0])
|
|
pruefe("2026-020" in inhalt and "2026-030" in inhalt, "sie listet beide Nummern")
|
|
pruefe(all(n in inhalt for n in ("Erst", "Zweit", "August", "September")),
|
|
"mit allen vier Belegen")
|
|
pruefe("Sauber" not in inhalt, "und nicht die saubere Rechnung")
|
|
neu[0].destroy()
|
|
|
|
# ---- 8. Dublette aufloesen -> die Warnung muss verschwinden -------------
|
|
weg = [b for b in alle if b.nachname == "Zweit"][0]
|
|
fenster.journal.loeschen(weg.id)
|
|
weg2 = [b for b in alle if b.nachname == "September"][0]
|
|
fenster.journal.loeschen(weg2.id)
|
|
fenster.aktualisieren()
|
|
fenster.update_idletasks()
|
|
pruefe(not fenster._doppelte, f"keine Dubletten mehr ({sorted(fenster._doppelte)})")
|
|
pruefe(not fenster.doppelt_leiste.winfo_manager(), "die Warnleiste ist wieder weg")
|
|
pruefe(not any("doppelt" in t for _n, _nr, t in zeilen(8)), "keine rote Zeile mehr im August")
|
|
titel = [fenster.blaetter.tab(m - 1, "text") for m in range(1, 13)]
|
|
pruefe(not any(t.startswith("\u26a0") for t in titel), "und kein markierter Reiter mehr")
|
|
|
|
fenster.journal.schliessen()
|
|
fenster.destroy()
|
|
|
|
if not fehler:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
else:
|
|
print(f" (Testordner bleibt zum Nachsehen: {tmp})")
|
|
print("---")
|
|
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
|
|
sys.exit(1 if fehler else 0)
|