Das zweite Blatt "fuer das Amt" trug DIESELBE Rechnungsnummer, aber einen anderen Gesamtbetrag. Mehrere Blaetter zum selben Umsatz mit verschiedenen Endbetraegen sehen aus, als werde ueber verschiedene Umsaetze abgerechnet - bei einer Pruefung ein Problem. Ersatzlos entfallen; was die Stadt braucht, rechnet das Steuerjournal aus den Kenndaten der PDF. Mit dem Blatt fielen _AmtMarke, gesamtrechnung_betont und der doppelte Renderlauf weg. Die API kann das Fenster jetzt BEDIENEN - mit Schluessel wie bei einem SSH-Key: 32 Byte Zufall in api_schluessel.txt im gemeinsamen Ordner, den nur das Steuerjournal und der Betreiber lesen. Ohne gueltigen Schluessel kein Zutritt (compare_digest). ping und schau_nach bleiben schluessellos, sonst kann ein aelteres Journal nicht mehr anklopfen. Was einen BELEG schreibt, geht nur im Testlauf - eine Fernbedienung, die echte Rechnungen ausstellt, hat in einer Buchhaltung nichts zu suchen. api.py liegt wie immer in beiden Programmen. Der Erststart fragte nach "der zuletzt vergebenen Nummer" - das holt niemanden ab, der bei 0 anfaengt. Jetzt zwei Wege: bei 0 anfangen ODER die eigene Zaehlung weiterfuehren. Unter das schon Vergebene kommt keiner der beiden. pruef_nummernsperre bediente den Dialog nur halb: nach einer abgelehnten Zahl bleibt er offen (so soll er), niemand drueckte "Spaeter" - das Programm wartete in wait_window() ewig. Das war schon vor diesem Umbau so. Neu: pruef_api.py. Alle 14 Pruefstaende gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
168 lines
6.2 KiB
Python
168 lines
6.2 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
|
|
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
|
|
|
|
|
|
def _reissleine():
|
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = threading.Timer(120, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
fehler = []
|
|
|
|
|
|
def pruefe(bedingung, text):
|
|
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
|
k = sys.stdout.encoding or "utf-8"
|
|
print(zeile.encode(k, "replace").decode(k))
|
|
if not bedingung:
|
|
fehler.append(text)
|
|
|
|
|
|
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
|
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, aber ohne Testlauf-Flag.
|
|
A.TESTLAUF = False
|
|
antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
|
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")
|
|
A.TESTLAUF = True
|
|
|
|
# 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")
|