rechnungstool/pruef_hinweis.py
TheMockTv 73042b771c Logo in der Statuszeile, Bau an einer Stelle, Startprobe fuer die EXE
Logo
  Das Programmlogo steht jetzt klein neben "powered by ravokk" - dort sitzt
  die Kennung des Werkzeugs, nicht die des Kunden. Eine eigene Kopfzeile hat
  dieses Programm nicht; oben sind die Reiter.

bauen.py
  Der Weg von der Quelle zur Setup-Datei war eine Folge von Befehlen, die
  jemand von Hand eintippen musste. Wer einen Schritt vergisst, liefert eine
  Setup-Datei mit einer alten EXE aus - und sieht es ihr nicht an. Genau das
  ist heute passiert. Jetzt ist es ein Befehl, die Nummer kommt aus
  version.py, und am Ende stehen die SHA256-Summen fuer den Release-Text.

pruef_start_exe.py
  Prueft, was kein anderer Pruefstand prueft: ob die GEBAUTE EXE ein
  sichtbares Fenster zeigt. Alle uebrigen fahren das Programm aus der Quelle,
  dort trat der Fehler nie auf. "Der Prozess laeuft" beweist dabei nichts -
  er lief ja, waehrend nichts zu sehen war.

Aufgeraeumt
  "Rechnung Campinghof.exe" ist weg; gebaute Dateien gehoeren ins Release,
  nicht ins Repo. Die .spec-Dateien dagegen schon: ohne sie kann niemand die
  EXE nachbauen, die der Updater ausliefert.

Fassung 1.7.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-06 20:34:59 +02:00

193 lines
6.3 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: der Hinweis auf die Erprobungsfassung und die Kenntnisnahme.
Sein Wortlaut vom 06.09.2026:
"wenn er nein drueckt oder ablehnt, schliesst sich die software und geht
beim naechsten start wieder mit dem selben auf, bis er auf ja drueckt"
"und das alles auch noch mal in die info mit signatur: user hat dies
gelesen und zur kenntnis genommen"
Geprueft wird die ganze Strecke, weil jeder Teil fuer sich wertlos waere:
1. Der Hinweis kommt beim Start und nennt Haftung, Vorschau und die
Fehleradresse.
2. NEIN -> das Fenster ist zu, und in der Konfiguration steht NICHTS.
Ein "Nein", das trotzdem vermerkt wird, waere eine Luege.
3. Neuer Start -> es wird WIEDER gefragt.
4. JA -> Signatur mit Benutzer, Zeit und Fassung; danach wird nicht mehr
gefragt.
5. Andere Fassung des Textes -> es wird erneut gefragt. Eine Zustimmung gilt
fuer das, was dagestanden hat, nicht fuer alles Kuenftige.
"""
import json
import os
import time
import shutil
import sys
import tempfile
import threading
QUELLE = r"C:\claude\rechnungstool"
sys.path.insert(0, QUELLE)
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kod = sys.stdout.encoding or "utf-8"
print(zeile.encode(kod, "replace").decode(kod))
if not bedingung:
fehler.append(text)
def _reissleine():
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
os._exit(2)
_wecker = threading.Timer(120, _reissleine)
_wecker.daemon = True
_wecker.start()
tmp = tempfile.mkdtemp(prefix="hinweis_pruef_")
ordner = os.path.join(tmp, "rechnungen")
os.makedirs(ordner)
import config # noqa: E402
shutil.copy(os.path.join(QUELLE, "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["output_dir"] = ordner
roh["nummer_eingerichtet"] = True
for weg in ("hinweis_fassung", "hinweis_zeit", "hinweis_benutzer"):
roh.pop(weg, None)
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
os.environ["CAMPINGHOF_TEST"] = "1"
import app as A # noqa: E402
import hinweis as H # noqa: E402
from tkinter import messagebox # noqa: E402
antworten = {"Fertig": False, "Hausnummer": True}
gesehen = []
def askyesno(titel, text, **kw):
gesehen.append((str(titel), str(text)))
for schlue, wert in antworten.items():
if schlue in titel:
return wert
raise AssertionError(f"unangemeldeter Dialog: {titel!r}")
messagebox.askyesno = askyesno
messagebox.showinfo = lambda *a, **k: None
messagebox.showwarning = lambda *a, **k: None
messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "")
def pumpen(fenster, bedingung, sekunden=6):
"""Ereignisschleife drehen, bis die Bedingung stimmt.
Der Hinweis wartet, bis das Fenster gezeichnet ist, und meldet sich per
after(). Ein einzelnes update() erreicht das nicht - hier wird die Schleife
gedreht wie mainloop() es taete. Abgekuerzt wird nichts.
"""
ende = time.time() + sekunden
while time.time() < ende:
try:
fenster.update()
except Exception: # noqa: BLE001 - Fenster kann zwischendurch weg sein
return bedingung()
if bedingung():
return True
time.sleep(0.02)
return bedingung()
def gelesen():
return json.load(open(config.CONFIG_PATH, encoding="utf-8"))
# ---- 1./2. Ablehnen: Fenster zu, nichts vermerkt -----------------------
antworten["Erprobungsfassung"] = False
f = A.RechnungsApp()
pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen))
treffer = [t for t in gesehen if "Erprobungsfassung" in t[0]]
pruefe(bool(treffer), "der Hinweis kommt beim Start")
if treffer:
inhalt = treffer[0][1]
pruefe("ERPROBUNGSFASSUNG" in inhalt.upper(), "er nennt die Erprobungsfassung")
pruefe("Haftung" in inhalt, "er nennt die Haftung")
pruefe("Vorschau" in inhalt, "er verweist auf die Vorschau")
pruefe("git.pcore.de/TheMockTv/rechnungstool" in inhalt,
"er nennt die Adresse fuer Fehlermeldungen")
def fenster_zu(fenster):
"""True, wenn es das Fenster nicht mehr gibt.
Nach destroy() antwortet winfo_exists() nicht mehr mit 0, sondern wirft -
die ganze Anwendung ist weg. Beides heisst hier dasselbe.
"""
import tkinter
try:
return not fenster.winfo_exists()
except tkinter.TclError:
return True
pruefe(fenster_zu(f), "nach Nein ist das Fenster zu")
cfg = gelesen()
pruefe(not cfg.get("hinweis_zeit"), "nach Nein steht nichts in der Konfiguration")
# ---- 3. Neuer Start: es wird wieder gefragt ----------------------------
gesehen.clear()
antworten["Erprobungsfassung"] = True
f = A.RechnungsApp()
pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen))
pruefe(any("Erprobungsfassung" in t[0] for t in gesehen),
"beim naechsten Start wird wieder gefragt")
# ---- 4. Zustimmen: Signatur steht ---------------------------------------
cfg = gelesen()
pruefe(cfg.get("hinweis_fassung") == H.FASSUNG,
f"die Fassung ist vermerkt ({cfg.get('hinweis_fassung')!r})")
pruefe(bool(cfg.get("hinweis_zeit")), "der Zeitpunkt ist vermerkt")
pruefe(bool(cfg.get("hinweis_benutzer")), "der Benutzer ist vermerkt")
sig = H.signatur(f.cfg)
pruefe("gelesen und zur kenntnis genommen" in sig.lower(),
f"die Signaturzeile ist lesbar ({sig!r})")
# ---- 5. Kein zweites Mal fragen -----------------------------------------
gesehen.clear()
f.destroy()
f = A.RechnungsApp()
pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen), sekunden=2)
pruefe(not any("Erprobungsfassung" in t[0] for t in gesehen),
"nach dem Ja wird nicht noch einmal gefragt")
# ---- 6. Andere Fassung -> wieder fragen ---------------------------------
gesehen.clear()
f.destroy()
H.FASSUNG = "9999-12-31" # so, als waere der Text geaendert worden
f = A.RechnungsApp()
pumpen(f, lambda: any("Erprobungsfassung" in t[0] for t in gesehen))
pruefe(any("Erprobungsfassung" in t[0] for t in gesehen),
"bei geaenderter Fassung wird erneut gefragt")
_wecker.cancel()
try:
f.destroy()
except Exception: # noqa: BLE001
pass
shutil.rmtree(tmp, ignore_errors=True)
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Hinweis, Ablehnung und Signatur wirken")