Steuerrechnungstool/pruef_wache.py
TheMockTv 96671027c8 Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle
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
2026-09-07 18:39:03 +02:00

285 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: das Rechnungstool meldet, das Steuerjournal liest nach.
Sein Fall: beide Programme stehen offen. Er schreibt im Rechnungstool eine
Rechnung - dann soll sie hier auftauchen, ohne dass jemand "Rechnungen
einlesen" drueckt. Seine Ansage zum Weg:
"machs einfacher das rechnungstoll schaut ob der steuer ding da ist und
meldet dem hier neue rechnung wurde erstellt"
Der Weg, den er vorgegeben hat: das laufende Journal traegt sich mit seiner PID
in der GEMEINSAMEN SQLITE ein ("der briefkasten ist die sqlite"), das
Rechnungstool fragt WINDOWS, ob dieser Prozess laeuft ("nicht auf fenster
schauen"), und legt dann eine Zeile in den Briefkasten - dieselbe Datei. Das
Journal sieht nur dort nach. Geprueft wird mit BEIDEN echten Programmen.
"""
import json
import os
import shutil
import sys
import tempfile
import threading
import time
JOURNAL = r"C:\claude\beherbergungssteuer"
TOOL = r"C:\claude\rechnungstool"
def _reissleine():
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
os._exit(2)
_wecker = threading.Timer(150, _reissleine)
_wecker.daemon = True
_wecker.start()
os.environ["CAMPINGHOF_TEST"] = "1"
tmp = tempfile.mkdtemp(prefix="bst_melden_")
gemeinsam_ordner = os.path.join(tmp, "install")
rechnungen = os.path.join(tmp, "rechnungen")
daten = os.path.join(tmp, "daten")
for d in (gemeinsam_ordner, rechnungen, daten):
os.makedirs(d)
# ---- Das Steuerjournal ----------------------------------------------------
sys.path.insert(0, JOURNAL)
import ablage # noqa: E402
ablage.daten_ordner = lambda: daten
import app as JA # noqa: E402
import gemeinsam # noqa: E402
del sys.modules["app"]
sys.path.remove(JOURNAL)
from tkinter import messagebox # noqa: E402
from pruefhelfer import fehler, pruefe
messagebox.showinfo = lambda *a, **k: None
messagebox.showwarning = lambda *a, **k: None
# Der Abfang MUSS vor dem ersten Fenster stehen: sonst geht ein echter Dialog
# auf und der Lauf wartet auf einen Klick, den niemand macht. Genau das ist am
# 06.09.2026 passiert - der Hinweis kam, bevor der Abfang gesetzt war.
# Erprobungshinweis und Freigabe vor dem Beleg werden hier bejaht - geprueft
# werden sie eigens in pruef_hinweis.py und pruef_freigabe.py des Rechnungstools.
antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True,
"Storno wirklich": True, "Storno verwerfen": True}
antworten["Erprobungsfassung"] = True
antworten["jetzt erstellen?"] = True
def askyesno(titel, text, **kw):
for schlue, wert in antworten.items():
if schlue in titel:
return wert
raise AssertionError(f"unangemeldeter Dialog: {titel!r}")
messagebox.askyesno = askyesno
journal = JA.App()
# Der Erprobungshinweis haengt an after(0) - ohne Ereignisschleife kaeme er nie
# dran, und damit auch der verzoegerte Start nicht. update() treibt die Schleife
# einmal an, genau wie es mainloop() taete. Abgekuerzt wird nichts.
journal.update()
journal.withdraw()
journal.pdf_ordner = rechnungen
journal.journal.setze("pdf_ordner", rechnungen)
journal.journal.setze("gemeinsam_ordner", gemeinsam_ordner)
journal._buch_oeffnen()
journal.jahr.set(2026)
journal.update()
# ---- 1. Anmeldung mit der PID --------------------------------------------
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"vor der Anmeldung gilt das Journal als geschlossen")
journal._anmelden()
pruefe(gemeinsam.journal_laeuft(gemeinsam_ordner),
"nach der Anmeldung sieht man an der PID, dass es laeuft")
# Die Absicht dieser Pruefung: im gemeinsamen Ordner liegt NICHTS Fremdes -
# keine Herzschlagdateien, keine Zwischenstaende. Erlaubt sind genau zwei:
# das Nummernbuch und der Schluessel der Mini-API, der seit dem Umbau vom
# 05.09.2026 dorthin gehoert (beide Programme muessen ihn lesen koennen).
# Alles darueber hinaus faellt weiterhin auf.
ERLAUBT = {gemeinsam.DATEINAME, "api_schluessel.txt"}
_liegt = set(os.listdir(gemeinsam_ordner))
pruefe(_liegt <= ERLAUBT,
f"im gemeinsamen Ordner liegt nichts Fremdes ({sorted(_liegt)})")
# ---- 1b. Die Mini-API: Port im Telefonbuch, und sie meldet sich -----------
import api # noqa: E402
eintrag = gemeinsam.Nummernbuch(gemeinsam_ordner).wer_laeuft("journal")
pruefe(bool(eintrag and eintrag.get("port")),
f"das Journal hat Port und PID im Telefonbuch ({eintrag})")
pruefe(journal.api.port > 0, f"sein kleiner Server laeuft ({journal.api.port})")
pruefe(api.erreichbar(gemeinsam.Nummernbuch(gemeinsam_ordner), "journal"),
"auf ein ping antwortet es: hallo, bin noch da")
pruefe(gemeinsam.prozess_laeuft(os.getpid(), os.path.basename(sys.executable)),
"die PID-Pruefung erkennt den eigenen Prozess")
pruefe(not gemeinsam.prozess_laeuft(999999, ""),
"eine Nummer, die es nicht gibt, laeuft nicht")
pruefe(not gemeinsam.prozess_laeuft(os.getpid(), "gibtsnicht.exe"),
"und ein fremder EXE-Name zaehlt nicht (PIDs werden wiederverwendet)")
# ---- Das Rechnungstool ----------------------------------------------------
sys.path.insert(0, TOOL)
import config # noqa: E402
shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json"))
config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
roh["output_dir"] = rechnungen
roh["gemeinsam_ordner"] = gemeinsam_ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
import app as A # noqa: E402
tool = A.RechnungsApp()
tool.update() # dasselbe fuer das Rechnungstool
tool.cfg["output_dir"] = rechnungen
tool.cfg["gemeinsam_ordner"] = gemeinsam_ordner
tool._buch_oeffnen()
# ---- 2. Das Rechnungstool sieht: das Journal ist da -----------------------
pruefe(gemeinsam.journal_laeuft(gemeinsam_ordner),
"das Rechnungstool sieht an der PID, dass das Journal offen ist")
# ---- 3. Rechnung schreiben -> Nachricht in den Briefkasten ----------------
journal._wache_jetzt() # Stand merken, Briefkasten ist noch leer
tool.k_vorname.set("Anna")
tool.k_name.set("Meldung")
tool.k_strasse.set("Musterweg 1")
tool.k_plzort.set("12345 Musterstadt")
tool.r_anreise.set("01.08.2026")
tool.r_abreise.set("04.08.2026")
tool.anzahl_vars[sorted(tool.anzahl_vars)[0]].set("3")
tool.update()
tool.pdf_erstellen()
texte, _ = gemeinsam.Nummernbuch(gemeinsam_ordner).nachrichten_ab(0)
pruefe(any("Rechnung 2026-001 erstellt" in t for t in texte),
f"die Meldung steht in der SQLite ({texte})")
# ---- 4. Der PUSH kommt an: das Journal liest ohne Wache nach -------------
# Nicht _wache_jetzt() aufrufen - genau das soll der Push von selbst ausloesen.
for _ in range(40):
if not journal._push_kasten.empty():
break
time.sleep(0.05)
pruefe(not journal._push_kasten.empty(),
"der Push des Rechnungstools ist im Fenster angekommen")
journal._push_abholen()
journal.update()
# Auf die Meldung WARTEN statt einmal hinzusehen. Das Einlesen laeuft
# nebenher und schreibt danach sein eigenes Ergebnis in dieselbe Zeile - wer
# genau dazwischen misst, sieht "Nummernbuch: 1 Nummern eingetragen" und
# meldet einen Fehler, den es nicht gibt. Genau das ist am 07.09.2026 einmal
# in einer Prueflauf-Runde passiert und beim naechsten Lauf nicht mehr.
for _ in range(60):
if "Rechnungstool meldet" in journal.status.cget("text"):
break
journal.update()
time.sleep(0.05)
buchungen = journal.journal.buchungen(jahr=2026)
pruefe(len(buchungen) == 1 and buchungen[0].nachname == "Meldung",
f"die Rechnung steht im Journal ({[(b.voll_nummer, b.nachname) for b in buchungen]})")
pruefe("Rechnungstool meldet" in journal.status.cget("text"),
f"und es steht in der Statuszeile ({journal.status.cget('text')!r})")
# Gebucht wird nach RECHNUNGSDATUM (heute), nicht nach Anreise.
import datetime as _dt
monat = _dt.date.today().month
zeilen = journal.monate[monat].baum.get_children()
pruefe(len(zeilen) == 1, f"das Monatsblatt zeigt sie ({len(zeilen)} im Monat {monat})")
# ---- 5. Ohne neue Meldung passiert nichts --------------------------------
pruefe(journal._wache_jetzt() is False, "ohne neue Meldung wird nicht eingelesen")
# ---- 5b. Gegenseitig: auch das Rechnungstool ist erreichbar --------------
pruefe(tool.api.port > 0, f"das Rechnungstool hat auch einen kleinen Server ({tool.api.port})")
pruefe(api.erreichbar(gemeinsam.Nummernbuch(gemeinsam_ordner), "rechnungstool"),
"und meldet sich ebenfalls mit: hallo, bin noch da")
pruefe(api.anklopfen(gemeinsam.Nummernbuch(gemeinsam_ordner), "rechnungstool"),
"beim Rechnungstool laesst sich anklopfen (die beiden sehen sich gegenseitig)")
# ---- 6. Auch der Storno meldet sich --------------------------------------
import bestand as BE # noqa: E402
offen = [m for m in BE.rechnungen_im_ordner(rechnungen) if m.get("art") != "storno"]
tool._storno_anlegen(offen[0], rechnungen)
texte, _ = gemeinsam.Nummernbuch(gemeinsam_ordner).nachrichten_ab(0)
pruefe(any("Storno" in t and "2026-001" in t for t in texte),
f"der Storno meldet sich ebenfalls ({texte})")
pruefe(journal._wache_jetzt() is True, "und das Journal liest ihn ein")
storno = [b for b in journal.journal.buchungen(jahr=2026) if b.storno_zu]
pruefe(len(storno) == 1 and storno[0].storno_zu == "2026-001",
f"der Storno steht mit seinem Bezug im Journal ({[(b.voll_nummer, b.storno_zu) for b in storno]})")
# ---- 6b. Ist die API weg, findet die Meldung trotzdem ihren Weg ---------
journal.api.stoppen() # nur den Server zumachen, Eintrag bleibt stehen
pruefe(not api.erreichbar(gemeinsam.Nummernbuch(gemeinsam_ordner), "journal"),
"ohne Server antwortet niemand mehr auf das ping")
vorher_id = gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht()
tool._journal_benachrichtigen("Probe ohne API")
pruefe(gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht() > vorher_id,
"die Meldung steht trotzdem in der SQLite (Rueckfallebene)")
pruefe(journal._wache_jetzt() is True, "und die Wache findet sie beim naechsten Nachsehen")
# ---- 7. Ist das Journal zu, wird nicht gemeldet --------------------------
journal._abmelden()
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"nach dem Abmelden gilt das Journal als geschlossen")
vorher = gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht()
tool.reset_formular()
tool.k_vorname.set("Bernd")
tool.k_name.set("Ohne")
tool.k_strasse.set("Musterweg 2")
tool.k_plzort.set("12345 Musterstadt")
tool.r_anreise.set("10.08.2026")
tool.r_abreise.set("12.08.2026")
tool.anzahl_vars[sorted(tool.anzahl_vars)[0]].set("2")
tool.update()
tool.pdf_erstellen()
pruefe(gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht() == vorher,
"dann kommt keine Meldung dazu")
pruefe(len([d for d in os.listdir(rechnungen) if "Ohne" in d]) == 1,
"die Rechnung wird trotzdem geschrieben")
# ---- 8. Eine Karteileiche zaehlt nicht und wird weggeraeumt -------------
buch = gemeinsam.Nummernbuch(gemeinsam_ordner)
buch.anmelden("journal", 999999, "Beherbergungssteuer.exe")
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"eine tote PID (abgestuerzt) gilt nicht als laufend")
pruefe(not buch.laeuft("journal"), "und die Karteileiche ist gleich weggeraeumt")
# ---- 9. Die Wache darf nie sterben --------------------------------------
echt = gemeinsam.datei_stand
gemeinsam.datei_stand = lambda o: (_ for _ in ()).throw(OSError("Laufwerk weg"))
try:
journal._wache()
gestorben = False
except Exception: # noqa: BLE001
gestorben = True
gemeinsam.datei_stand = echt
pruefe(not gestorben, "ein Fehler im Durchgang wirft die Wache nicht um")
# ---- 10. Ohne gemeinsamen Ordner bleibt alles ruhig ---------------------
journal.buch = None
pruefe(journal._wache_jetzt() is False, "ohne gemeinsames Buch tut die Wache nichts")
tool.destroy()
journal.journal.schliessen()
journal.destroy()
if not fehler:
shutil.rmtree(tmp, ignore_errors=True)
else:
print(f" (Wegwerf-Ordner bleibt: {tmp})")
print("---")
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
sys.exit(1 if fehler else 0)