NAME Das Programm heisst jetzt Steuerrechnungstool - wie sein Repository (https://git.pcore.de/TheMockTv/Steuerrechnungstool). Umgestellt: Fenstertitel, Kopfzeile, "laeuft bereits"-Meldung, Startdatei, Spec-Datei (EXE-Name), Logzeile. NICHT umbenannt, mit Absicht: * "Beherbergungssteuer" als NAME DER STEUER - das ist ein Rechtsbegriff und steht weiter so in Berichten, Anmeldung und Oberflaeche. * LOCALAPPDATA\Beherbergungssteuer in ablage.py - dort liegt der Zeiger auf den Datenordner. Wer den umbenennt, kappt bestehenden Installationen den Weg zu ihren Daten. * Der lokale Ordner C:\claude\beherbergungssteuer - steht in Pruefstaenden und Spec-Dateien fest, eigener Schritt. HINWEIS hinweis.py aus dem Rechnungstool uebernommen (liegt in beiden Programmen gleich). Der Hinweis kommt beim Start, Ablehnen beendet das Programm ohne etwas zu vermerken, die Kenntnisnahme wird mit Benutzer, Zeit und Fassung in der Einstellungstabelle des Journals gespeichert. Neu: Menue Hilfe -> Info mit dem Text und der Signaturzeile. PRUEFSTAENDE Vier Prueflaeufe hatten gar keinen Dialogabfang - bisher fragte das Programm beim Start nichts. Jetzt bleiben sie sonst vor einem echten Fenster stehen. pruef_doppelte und pruef_storno_verrechnet haben einen bekommen, der bei UNBEKANNTEN Fragen abbricht statt still "ja" zu sagen. pruef_storno_journal und pruef_wache melden Hinweis und Freigabe ausdruecklich an. pruef_wache treibt ausserdem die Ereignisschleife an (update), sonst kaeme after_idle nie dran. pruef_wache: die Erwartung "im gemeinsamen Ordner liegt genau EINE Datei" ist auf die richtige Menge gebracht - Nummernbuch UND Schluessel der Mini-API, der seit dem 05.09.2026 dorthin gehoert. Alles darueber hinaus faellt weiter auf. ⚠️ pruef_wache bleibt ROT an einer Stelle, die NICHT von diesen Arbeiten kommt (vor dem Umbau gemessen, war schon rot): die Statuszeile soll nach einem Push "Rechnungstool meldet" zeigen, wird aber vom Ergebnis des Einlesens ueberschrieben. Dokumentiert in AUFGABE_BEWEISKETTE_PRUEFBAR.md. 5 von 6 Pruefstaenden gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
216 lines
8.8 KiB
Python
216 lines
8.8 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 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
|
|
|
|
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)
|