Storno: das Journal zaehlte die Uebernachtungen der aufgehobenen Rechnung weiter mit (4 - 0 + 3 = 7 statt 3 gemeldeter Naechte). Die Betraege kuerzten sich, die Naechte nicht. Der Storno traegt jetzt die negative Zahl, und das Journal kennt Art, Bezug und Datum des Belegs (vier neue Spalten, alte Journale bauen sich beim Oeffnen um). Berichtigung: das Blatt wurde nur uebersprungen. Der berichtigte NAME kommt jetzt in der Buchung an - im Amtsbericht stand sonst weiter der falsche. Angewandt wird erst am Ende des Scans, das Blatt kann vor seiner Rechnung im Ordner liegen. Doppelte Nummern: eine stornierte Rechnung ist keine offene mehr und faellt aus der Meldung. Zugeordnet wird ueber Nummer + Datum + Empfaenger, nicht ueber die Nummer allein - bei einer doppelt vergebenen Nummer waere die kein eindeutiger Bezug, und genau den verlangt § 31 Abs. 5 UStDV. Die Texte nennen jetzt den Weg: eine der beiden im Rechnungstool stornieren und mit freier Nummer neu ausstellen (§ 14 Abs. 4 Nr. 4 UStG - jede Nummer nur einmal). Neue Spalte "Art": Storno zu ..., storniert, Neuausstellung. Aufgehobene Zeilen stehen grau - geloescht wird nichts. Gemeinsames Nummernbuch (gemeinsam.py, ordnerwahl.py - in beiden Programmen dieselbe Datei): eine SQLite in einem Ordner, den beide kennen. Das Journal traegt seine Nummern ein, auch die aus der Excel-Mappe ohne PDF - genau die kannte das Rechnungstool nicht und vergab sie ein zweites Mal. Beim ersten Start wird nach dem Ordner gefragt (auswaehlen, OK, Sicherheitsfrage, bei Nein zurueck ins Feld); wer ihn nicht hat, kommt mit "Spaeter einrichten" weiter. Kein Doppelklick zum Bearbeiten mehr (gui_dialoge.py geloescht): wer hier Betrag oder Nummer verstellt, meldet dem Amt etwas anderes, als auf dem Beleg steht. Loeschen bleibt. --test / CAMPINGHOF_TEST=1 unterdrueckt den Erststart-Dialog (Pruefstaende). Pruefstaende: pruef_storno_journal.py (neu, ueber BEIDE Programme: Storno, Berichtigung, doppelte Nummer aufloesen), pruef_doppelte, pruef_berichtigung, pruef_einzelinstanz - alle gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
151 lines
4.6 KiB
Python
151 lines
4.6 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():
|
|
# --test: kein Erststart-Dialog nach dem gemeinsamen Ordner. Die Sperre
|
|
# selbst greift unabhaengig davon - genau die wird hier geprueft.
|
|
return subprocess.Popen([sys.executable, "app.py", "--test"], 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)
|