Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer sich pruefbar - nach jeder liefen beide Pruefketten gruen. * Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks. Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm() begrenzt; am unteren Fensterrand lief sie vorher hinaus. Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse. * %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG). * pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py. Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31) und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen. * pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt == 1, und der Dateiname wird mitgeprueft. Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF. * .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen. Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen Unterschied, den es im Repository gar nicht gibt. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung waere Rauschen. * Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=), kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes". 39 Pruefstaende, 0 rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
207 lines
8.6 KiB
Python
207 lines
8.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)
|
|
|
|
|
|
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 tkinter import messagebox # noqa: E402
|
|
|
|
# Kein echtes Fenster im Pruefstand: sonst steht der Lauf vor einem Dialog, den
|
|
# niemand anklickt. Der Erprobungshinweis wird hier bejaht - geprueft wird er
|
|
# eigens in pruef_hinweis.py des Rechnungstools. Eine UNBEKANNTE Frage bricht
|
|
# ab, statt still 'ja' zu sagen: sonst uebergeht der Lauf genau die Faelle,
|
|
# wegen denen es die Pruefung gibt.
|
|
_antworten = {"Erprobungsfassung": True, "jetzt erstellen?": True,
|
|
"Fertig": False, "Hausnummer": True}
|
|
|
|
|
|
def _askyesno(titel, text="", **kw):
|
|
for schluessel, wert in _antworten.items():
|
|
if schluessel in str(titel):
|
|
return wert
|
|
raise AssertionError(f"unangemeldeter Dialog: {titel!r}")
|
|
|
|
|
|
messagebox.askyesno = _askyesno
|
|
messagebox.showinfo = lambda *a, **k: None
|
|
messagebox.showwarning = lambda *a, **k: None
|
|
|
|
from modell import Buchung, doppelte_nummern, summiere # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
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).
|
|
|
|
Die Spalten kommen aus gui_monat.SPALTEN - nicht abgezaehlt, sonst
|
|
verrutscht der Pruefstand still, sobald eine Spalte dazukommt (genau das
|
|
ist beim Einbau der Spalte "Art" passiert).
|
|
"""
|
|
from gui_monat import SPALTEN
|
|
namen = [sp[0] for sp in SPALTEN]
|
|
i_name, i_nr = namen.index("nachname"), namen.index("nummer")
|
|
baum = fenster.monate[monat].baum
|
|
aus = []
|
|
for iid in baum.get_children():
|
|
w = baum.item(iid, "values")
|
|
aus.append((w[i_name], w[i_nr], 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)
|