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
285 lines
12 KiB
Python
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)
|