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
This commit is contained in:
parent
142dddc647
commit
ca9ac212cf
11 changed files with 835 additions and 212 deletions
168
pruef_api.py
Normal file
168
pruef_api.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# -*- 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue