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
149 lines
4.5 KiB
Python
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)
|