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_bilder.py schrieb ein eigenes Format, pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug. * 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. * Sechs stumme "except OSError" sagen jetzt, warum sie schweigen duerfen, und einer meldet statt zu schweigen: schlaegt die Uebernahme einer alten config.json fehl, faengt das Programm ohne Firmendaten, Katalog und Zaehler neu an - und der Erfolgsfall schrieb eine Zeile, der Fehlerfall nicht. Die 24 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
167 lines
6.5 KiB
Python
167 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: das Fenster ueber die API bedienen - mit Schluessel.
|
|
|
|
Warum es das gibt (seine Ansage): *"bau dir steuer logik per api rein, dann
|
|
kannst damit die sachen triggern, brauchst meine maus nicht"* und *"mach pw
|
|
rein, also schluessel wie bei ssh key, das nur du und das beherbergungssteuer
|
|
ding rein kann, weil muss ja sprechen"*.
|
|
|
|
Geprueft wird:
|
|
|
|
1. Ohne Schluessel geht NICHTS - auch nicht "nur nachsehen".
|
|
2. Mit falschem Schluessel ebenfalls nicht.
|
|
3. Mit dem Schluessel aus dem gemeinsamen Ordner laesst sich das Fenster
|
|
fuellen, nachlesen und eine Vorschau erzeugen.
|
|
4. Was einen BELEG schreibt, geht nur im Testlauf - und schreibt dann
|
|
tatsaechlich die Rechnung.
|
|
5. ping und schau_nach bleiben schluessellos, sonst kann ein aelteres
|
|
Steuerjournal nicht mehr anklopfen.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
|
|
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, QUELLE)
|
|
|
|
tmp = tempfile.mkdtemp(prefix="api_pruef_")
|
|
ordner = os.path.join(tmp, "rechnungen")
|
|
gemeinsam_ordner = os.path.join(tmp, "install")
|
|
os.makedirs(ordner)
|
|
os.makedirs(gemeinsam_ordner)
|
|
|
|
import config # noqa: E402
|
|
shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json"))
|
|
config.BASE_DIR = tmp
|
|
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
|
|
# Der Erststart-Zustand gehoert dem Pruefstand, nicht der config des
|
|
# Entwicklerrechners: sonst laeuft er in einen anderen Zweig, sobald hier
|
|
# jemand die Nummer einmal eingerichtet hat.
|
|
roh["nummer_eingerichtet"] = False
|
|
roh["output_dir"] = ordner
|
|
roh["gemeinsam_ordner"] = gemeinsam_ordner
|
|
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
|
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
|
import api # noqa: E402
|
|
import app as A # noqa: E402
|
|
import gemeinsam # noqa: E402
|
|
from tkinter import messagebox # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
|
|
def _reissleine():
|
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = threading.Timer(120, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
|
|
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
|
# Geprueft wird er eigens in pruef_hinweis.py.
|
|
messagebox.askyesno = lambda *a, **k: True
|
|
|
|
f = A.RechnungsApp()
|
|
f.cfg["output_dir"] = ordner
|
|
f.buch = gemeinsam.Nummernbuch(gemeinsam_ordner)
|
|
f.api.starten(f.buch, ordner=gemeinsam_ordner)
|
|
f.update()
|
|
|
|
port = f.api.port
|
|
pruefe(bool(port), f"die API laeuft ({port})")
|
|
|
|
schluessel = api.schluessel_holen(gemeinsam_ordner)
|
|
pruefe(len(schluessel) == 64, f"ein Schluessel liegt im gemeinsamen Ordner ({len(schluessel)} Zeichen)")
|
|
pruefe(os.path.exists(os.path.join(gemeinsam_ordner, api.SCHLUESSEL_DATEI)),
|
|
"und zwar als Datei neben dem Nummernbuch")
|
|
pruefe(api.schluessel_holen(gemeinsam_ordner) == schluessel,
|
|
"beim zweiten Lesen kommt derselbe zurueck (kein neuer je Start)")
|
|
|
|
|
|
def rufen(**nachricht):
|
|
"""Direkt anrufen - der Pruefstand spielt hier das andere Programm."""
|
|
was = nachricht.pop("was", "aktion")
|
|
return api._reden(port, was, zeitgrenze=20.0, **nachricht)
|
|
|
|
|
|
# Das Fenster muss waehrend des Anrufs weiterlaufen: die Aktion wird in seinem
|
|
# Faden ausgefuehrt. Also nebenher anrufen und die Ereignisschleife drehen.
|
|
def anrufen_und_drehen(**nachricht):
|
|
ergebnis = {}
|
|
|
|
def anruf():
|
|
ergebnis["antwort"] = rufen(**nachricht)
|
|
|
|
t = threading.Thread(target=anruf, daemon=True)
|
|
t.start()
|
|
while t.is_alive():
|
|
f.update()
|
|
return ergebnis.get("antwort")
|
|
|
|
|
|
# ---- 1. + 2. Ohne (oder mit falschem) Schluessel: kein Zutritt -------------
|
|
pruefe(anrufen_und_drehen(name="zustand") == "kein zutritt",
|
|
"ohne Schluessel: kein Zutritt")
|
|
pruefe(anrufen_und_drehen(name="zustand", schluessel="0" * 64) == "kein zutritt",
|
|
"falscher Schluessel: kein Zutritt")
|
|
|
|
# ---- 5. Die alten zwei Woerter bleiben schluessellos -----------------------
|
|
pruefe(anrufen_und_drehen(was="ping") == api.ANTWORT_HALLO,
|
|
"ping antwortet weiterhin ohne Schluessel")
|
|
|
|
# ---- 3. Mit Schluessel: fuellen, nachlesen, Vorschau -----------------------
|
|
antwort = anrufen_und_drehen(
|
|
name="setze", schluessel=schluessel,
|
|
daten={"kunde": {"vorname": "Anna", "nachname": "Beispiel",
|
|
"strasse": "Musterweg 1", "plzort": "12345 Musterstadt"},
|
|
"anreise": "01.09.2026", "abreise": "04.09.2026",
|
|
"positionen": [{"nr": sorted(f.anzahl_vars)[0], "anzahl": 3}]})
|
|
pruefe(isinstance(antwort, dict) and antwort.get("kunde", {}).get("nachname") == "Beispiel",
|
|
f"mit Schluessel laesst sich das Formular fuellen ({str(antwort)[:60]}…)")
|
|
pruefe(antwort.get("gesamt", 0) > 0, f"und es ist gerechnet ({antwort.get('gesamt')})")
|
|
|
|
antwort = anrufen_und_drehen(name="vorschau", schluessel=schluessel)
|
|
pfad = (antwort or {}).get("pfad", "")
|
|
pruefe(bool(pfad) and os.path.exists(pfad), f"die Vorschau entsteht ohne Maus ({os.path.basename(pfad)})")
|
|
pruefe(not os.listdir(ordner), "und sie landet NICHT im Rechnungsordner")
|
|
|
|
# ---- 4. Beleg schreiben ---------------------------------------------------
|
|
antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
|
pdfs = [n for n in os.listdir(ordner) if n.lower().endswith(".pdf")]
|
|
pruefe(len(pdfs) == 1, f"im Testlauf schreibt die API die Rechnung ({pdfs})")
|
|
|
|
# Und im ECHTEN Betrieb nicht: dieselbe Aktion, ohne den Pruefschalter.
|
|
# Umgeschaltet wird die Umgebungsvariable selbst - das Programm fragt sie bei
|
|
# jeder Aktion neu ab, genau wie beim echten Start.
|
|
os.environ.pop("CAMPINGHOF_TEST", None)
|
|
antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
|
pruefe(isinstance(antwort, dict) and "nur im Testlauf" in str(antwort.get("fehler", "")),
|
|
f"im echten Betrieb verweigert sie den Beleg ({antwort})")
|
|
pruefe(len([n for n in os.listdir(ordner) if n.lower().endswith(".pdf")]) == 1,
|
|
"und es kam keine zweite Rechnung dazu")
|
|
|
|
# Unbekannter Name sagt, was es gibt - statt still nichts zu tun.
|
|
antwort = anrufen_und_drehen(name="tanz", schluessel=schluessel)
|
|
pruefe("kenne ich nicht" in str(antwort), f"unbekannte Aktion wird benannt ({str(antwort)[:50]}…)")
|
|
|
|
f.api.stoppen(f.buch)
|
|
f.destroy()
|
|
print()
|
|
if fehler:
|
|
print(f"FEHLGESCHLAGEN: {len(fehler)}")
|
|
for x in fehler:
|
|
print(" -", x)
|
|
print(f"(Ordner bleibt: {tmp})")
|
|
sys.exit(1)
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
print("ALLES GRUEN")
|