rechnungstool/pruef_api.py
TheMockTv ca9ac212cf Amt-Blatt raus, Bedienung ueber die API, Erststart fragt nach zwei Wegen
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
2026-09-05 18:24:59 +02:00

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")