Steuerrechnungstool/pruef_einzelinstanz.py
TheMockTv 894bc81c46 Doppelte Rechnungsnummern zeigen, nur ein Fenster, keine Handbuchungen
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
2026-09-03 19:47:44 +02:00

149 lines
4.5 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: geht das Steuerjournal wirklich nur EINMAL auf?
Zwei Fenster lesen denselben Rechnungsordner in dieselbe journal.sqlite3. Was
das eine loescht, steht im anderen noch auf dem Schirm - und wer dort auf
"Bericht fuers Amt" drueckt, meldet einen Stand, den es nicht mehr gibt.
Geprueft wird mit ECHTEN Prozessen, nicht mit einem nachgebauten Aufruf: nur so
zeigt sich, ob die Sperre wirklich vor dem Datenordner greift und ob sie sich
beim Beenden auch wieder loest. Das Programm laeuft dabei aus einer Kopie im
Temp-Ordner - die Sperre gilt trotzdem (sie haengt am Namen, nicht am Ordner),
das echte Journal wird nicht angefasst.
Waehrend des Laufs blitzt kurz ein Fenster auf. Das gehoert dazu.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import time
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)
laeufer = []
def _reissleine():
for p in laeufer:
try:
p.kill()
except Exception: # noqa: BLE001
pass
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog, "
"auf den niemand klickt")
os._exit(2)
_wecker = threading.Timer(150, _reissleine)
_wecker.daemon = True
_wecker.start()
from einzelinstanz import pid_datei # noqa: E402
if os.path.exists(pid_datei()):
print(f" (Merkzettel: {pid_datei()})")
# ---- Kopie des Programms, damit das echte Journal unberuehrt bleibt --------
tmp = tempfile.mkdtemp(prefix="bst_instanz_")
prog = os.path.join(tmp, "programm")
os.makedirs(prog)
for name in os.listdir(HIER):
if name.endswith(".py"):
shutil.copy(os.path.join(HIER, name), os.path.join(prog, name))
def starte():
return subprocess.Popen([sys.executable, "app.py"], cwd=prog,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
def warte_auf_ende(p, sekunden):
"""Wartet, bis der Prozess von selbst geht. None = er laeuft noch."""
ende = time.time() + sekunden
while time.time() < ende:
if p.poll() is not None:
return p.returncode
time.sleep(0.2)
return None
# ---- 1. Das erste Fenster ------------------------------------------------
erst = starte()
laeufer.append(erst)
time.sleep(6) # Tk braucht einen Moment, bis das Fenster wirklich steht
pruefe(erst.poll() is None, f"das erste Fenster laeuft (Rueckgabe {erst.poll()})")
merkzettel = None
try:
with open(pid_datei(), encoding="ascii") as f:
merkzettel = int(f.read().strip())
except (OSError, ValueError):
pass
pruefe(merkzettel == erst.pid,
f"der Merkzettel nennt genau diesen Prozess ({merkzettel} / {erst.pid})")
# ---- 2. Der zweite Doppelklick -------------------------------------------
zweit = starte()
laeufer.append(zweit)
code = warte_auf_ende(zweit, 25)
if code is None:
zweit.kill()
pruefe(code == 0, f"der zweite Start beendet sich von selbst (Rueckgabe {code})")
pruefe(erst.poll() is None, "und das erste Fenster laeuft dabei weiter")
ausgabe = ""
try:
ausgabe = (zweit.stdout.read() or b"").decode("utf-8", "replace")
except Exception: # noqa: BLE001
pass
pruefe("Traceback" not in ausgabe, f"ohne Absturz im zweiten Prozess ({ausgabe[-200:]!r})")
# Die Sperre steht VOR dem Datenordner: der zweite Prozess darf gar nicht erst
# so weit kommen, dass er das Journal oeffnet.
pruefe("Journal" not in ausgabe,
f"der zweite Prozess fasst das Journal nicht an ({ausgabe[-200:]!r})")
# ---- 3. Sperre loest sich beim Beenden ------------------------------------
erst.terminate()
warte_auf_ende(erst, 15)
pruefe(erst.poll() is not None, "das erste Fenster laesst sich beenden")
time.sleep(1.5)
dritt = starte()
laeufer.append(dritt)
code = warte_auf_ende(dritt, 8)
pruefe(code is None, f"danach geht das Programm wieder auf (Rueckgabe {code})")
dritt.terminate()
warte_auf_ende(dritt, 10)
if dritt.poll() is None:
dritt.kill()
for p in laeufer:
if p.poll() is None:
p.kill()
try:
p.stdout.close()
except Exception: # noqa: BLE001
pass
if not fehler:
shutil.rmtree(tmp, ignore_errors=True)
else:
print(f" (Kopie bleibt zum Nachsehen: {tmp})")
print("---")
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
sys.exit(1 if fehler else 0)