# -*- coding: utf-8 -*- """Pruefstand fuer die Storno-Kette - kompletter Durchlauf in einem Wegwerf-Ordner. Geprueft wird an ECHTEN PDFs, die das Programm selbst schreibt: 1. Rechnung 2026-001 erstellen 2. sie stornieren -> Storno 2026-002 mit negativen Betraegen 3. berichtigte Rechnung 2026-003 mit geaenderter Menge 4. der Steuerrechner liest den Ordner: 001 + 002 + 003 muss 003 ergeben 5. Rueckweg: Storno anlegen und verwerfen -> PDF weg, Zaehler zurueck, Original da Die echte config.json wird NICHT angefasst - es wird mit einer Kopie im Wegwerf-Ordner gearbeitet. """ import json import os import shutil import sys import tempfile QUELLE = r"C:\claude\rechnungstool" sys.path.insert(0, QUELLE) tmp = tempfile.mkdtemp(prefix="storno_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["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 json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2) # Kein Erststart-Dialog im Pruefstand - er wuerde den Lauf haengen lassen. os.environ["CAMPINGHOF_TEST"] = "1" import app as A # noqa: E402 import bestand as BE # noqa: E402 from tkinter import messagebox # noqa: E402 # --- Reissleine --------------------------------------------------------- # Ein Pruefstand darf NIE haengen. Bleibt er in einem Dialog stehen (etwa weil # eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten # von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt. import threading as _th from pruefhelfer import fehler, pruefe def _reissleine(): import os as _os print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog, " "der nicht beantwortet wird") _os._exit(2) _wecker = _th.Timer(120, _reissleine) _wecker.daemon = True _wecker.start() # Dialoge automatisch beantworten; jede Frage wird protokolliert. antworten = {} gefragt = [] def askyesno(titel, text, **kw): gefragt.append(titel) for schlue, wert in antworten.items(): if schlue in titel: return wert # Kein stilles "Nein": ein nicht angemeldeter Dialog wuerde den Ablauf # abbrechen und der Pruefstand haelt das Ergebnis faelschlich fuer gewollt. raise AssertionError(f"unangemeldeter Dialog: {titel!r}") antworten["Fertig"] = False # "jetzt oeffnen?" - nie oeffnen im Pruefstand antworten["endgültig erstellen"] = True # die Sicherheitsfrage bejahen antworten["Hausnummer"] = True # "Musterweg 1" hat eine, greift nur im Sonderfall # Die Freigabe vor dem Beleg (§ 14 UStG) wird bewusst bejaht - geprueft wird hier # der Ablauf DANACH. Dass ein "Nein" keinen Beleg erzeugt, prueft pruef_freigabe.py. antworten["jetzt erstellen?"] = True # Der Erprobungshinweis wird hier bejaht - geprueft wird er in pruef_hinweis.py. antworten["Erprobungsfassung"] = True messagebox.askyesno = askyesno messagebox.showinfo = lambda *a, **k: None warnungen = [] messagebox.showwarning = lambda *a, **k: warnungen.append(a[0] if a else "") messagebox.showerror = lambda *a, **k: print("[fehler]", a[0] if a else "", a[1] if len(a) > 1 else "") f = A.RechnungsApp() f.update() f.cfg["output_dir"] = ordner # ---- 1. normale Rechnung ------------------------------------------------ f.k_vorname.set("Anna") f.k_name.set("Beispiel") f.k_strasse.set("Musterweg 1") f.k_plzort.set("12345 Musterstadt") f.r_anreise.set("01.08.2026") f.r_abreise.set("05.08.2026") erste = sorted(f.anzahl_vars)[0] f.anzahl_vars[erste].set("4") f.update() f.pdf_erstellen() erste_pdf = os.path.join(ordner, "Rechnung_2026-001_Beispiel.pdf") pruefe(os.path.exists(erste_pdf), "Rechnung 2026-001 geschrieben") meta1 = BE.kenndaten_lesen(erste_pdf) pruefe(bool(meta1), "Kenndaten der Rechnung lesbar") pruefe(meta1.get("version") == 3, f"Metadaten Version 3 (ist {meta1.get('version')})") pruefe(meta1.get("art") == "rechnung", "art = rechnung") pruefe(len(meta1.get("positionen") or []) == 1, "Positionen stehen in den Kenndaten") pruefe(bool(meta1.get("kunde", {}).get("plzort")), "Anschrift steht in den Kenndaten") brutto1 = float(meta1["zwischensumme"]) gesamt1 = float(meta1["gesamt"]) print(f" Rechnung 001: brutto {brutto1:.2f}, gesamt {gesamt1:.2f}") # ---- 2. stornieren ------------------------------------------------------ offen = [d for d in BE.rechnungen_im_ordner(ordner) if d.get("art") != "storno"] pruefe(len(offen) == 1, "eine stornierbare Rechnung gefunden") f._storno_anlegen(offen[0], ordner) pruefe(f.storno_stand is not None, "Storno liegt im Zwischenspeicher") storno_pdf = f.storno_stand["pfad"] if f.storno_stand else "" pruefe(os.path.exists(storno_pdf), f"Storno-PDF geschrieben ({os.path.basename(storno_pdf)})") meta2 = BE.kenndaten_lesen(storno_pdf) pruefe(meta2.get("art") == "storno", "art = storno") pruefe(meta2.get("storno_zu") == "2026-001", "storno_zu zeigt auf 2026-001") import re as _re pruefe(bool(_re.fullmatch(r"\d{2}\.\d{2}\.\d{4}", str(meta2.get("storno_datum") or ""))) and meta2.get("storno_datum") == meta1.get("datum"), f"Datum der alten Rechnung steht drin ({meta2.get('storno_datum')!r})") pruefe(float(meta2["zwischensumme"]) == -brutto1, f"Zwischensumme negativ ({meta2['zwischensumme']})") pruefe(float(meta2["gesamt"]) == -gesamt1, f"Gesamt negativ ({meta2['gesamt']})") pruefe(bool(meta2["ust_bloecke"]) and all(b["netto"] < 0 and b["ust"] < 0 for b in meta2["ust_bloecke"]), f"alle USt-Bloecke echt negativ ({meta2['ust_bloecke']})") pruefe(f.r_nummer.get() == "2026-003", f"Formular steht auf 2026-003 (ist {f.r_nummer.get()})") pruefe(str(f.nummer_entry.cget("state")) == "readonly", "Nummernfeld gesperrt") pruefe(f.k_plzort.get() == "12345 Musterstadt", "Anschrift ins Formular uebernommen") pruefe(f.anzahl_vars[erste].get() == "4.0", f"Menge uebernommen (ist {f.anzahl_vars[erste].get()})") # ---- 3. berichtigte Rechnung ------------------------------------------- antworten["Storno wirklich"] = True f.anzahl_vars[erste].set("3") # das war der Fehler: 4 statt 3 Naechte f.update() f.pdf_erstellen() dritte = os.path.join(ordner, "Rechnung_2026-003_Beispiel.pdf") pruefe(os.path.exists(dritte), "berichtigte Rechnung 2026-003 geschrieben") pruefe(f.storno_stand is None, "Zwischenspeicher nach dem Erstellen leer") pruefe(str(f.nummer_entry.cget("state")) == "normal", "Nummernfeld wieder frei") meta3 = BE.kenndaten_lesen(dritte) pruefe(meta3.get("art") == "korrektur", "art = korrektur") pruefe(meta3.get("ersetzt") == "2026-001", "ersetzt zeigt auf 2026-001") pruefe(meta3.get("storno_nummer") == "2026-002", "storno_nummer zeigt auf 2026-002") pruefe(os.path.exists(erste_pdf), "die urspruengliche Rechnung liegt unangetastet da") gesamt3 = float(meta3["gesamt"]) print(f" Rechnung 003: gesamt {gesamt3:.2f}") # ---- 4. was sieht der Steuerrechner? ------------------------------------ sys.path.insert(0, r"C:\claude\steuerrechner") import einnahmen # noqa: E402 liste, kaputt = einnahmen.scanne(ordner, {"ust_saetze": [7, 19]}) pruefe(len(liste) == 3, f"Steuerrechner liest 3 Belege (liest {len(liste)})") pruefe(not kaputt, f"kein Beleg uebersprungen ({kaputt})") pruefe(not any(e.unsicher for e in liste), "kein Beleg als unsicher markiert") summe = round(sum(e.gesamt for e in liste), 2) netto = round(sum(e.netto for e in liste), 2) ust = round(sum(e.ust for e in liste), 2) pruefe(abs(summe - gesamt3) < 0.005, f"Jahressumme = nur die berichtigte Rechnung ({summe:.2f} vs {gesamt3:.2f})") print(f" Summe {summe:.2f} | Netto {netto:.2f} | USt {ust:.2f}") # ---- 5. Rueckweg: Storno anlegen und verwerfen -------------------------- antworten["Storno wirklich"] = False offen = [d for d in BE.rechnungen_im_ordner(ordner) if d.get("art") == "korrektur"] f._storno_anlegen(offen[0], ordner) storno2 = f.storno_stand["pfad"] zaehler_mit_storno = f.cfg["rechnung_zaehler"] pruefe(os.path.exists(storno2), "zweites Storno geschrieben") f.anzahl_vars[erste].set("2") f.update() f.pdf_erstellen() # -> Frage wird mit Nein beantwortet pruefe(not os.path.exists(storno2), "verworfenes Storno-PDF ist geloescht") pruefe(f.storno_stand is None, "Zwischenspeicher geleert") pruefe(f.cfg["rechnung_zaehler"] == zaehler_mit_storno - 1, f"Zaehler zurueckgesetzt ({f.cfg['rechnung_zaehler']} statt {zaehler_mit_storno})") pruefe(os.path.exists(dritte), "die Rechnung 2026-003 steht unveraendert da") pruefe(len([n for n in os.listdir(ordner) if n.endswith('.pdf')]) == 3, "es liegen wieder genau 3 PDFs im Ordner") # ---- 6. Suche im Storno-Dialog (Nummer eintippen, nicht auswaehlen) ----- import tkinter as tk # noqa: E402 from tkinter import ttk # noqa: E402 f.dlg_storno() f.update() dialog = [w for w in f.winfo_children() if isinstance(w, tk.Toplevel)][-1] feld = None schild = None for rahmen in dialog.winfo_children(): for w in rahmen.winfo_children(): if isinstance(w, ttk.Entry): feld = w elif isinstance(w, ttk.Label) and w.cget("textvariable"): schild = w pruefe(feld is not None and schild is not None, "Eingabefeld und Meldezeile im Dialog da") def tippe(text): feld.delete(0, "end") feld.insert(0, text) feld.event_generate("") f.update() return f.getvar(schild.cget("textvariable")) meldung = tippe("2026-003") pruefe("Der Storno lautet" in meldung, f"gueltige Nummer gefunden: {meldung.splitlines()[0]}") meldung = tippe("2026-3") pruefe("2026-003" in meldung, "kurze Schreibweise 2026-3 findet 2026-003") meldung = tippe("2026-001") pruefe("storniert" in meldung and "zweites Mal" in meldung, f"schon stornierte Rechnung wird abgelehnt ({meldung})") meldung = tippe("2026-002") pruefe("selbst eine Stornorechnung" in meldung, "Storno selbst ist nicht stornierbar") meldung = tippe("2026-999") pruefe("keine Rechnung" in meldung, "unbekannte Nummer wird abgelehnt") meldung = tippe("") pruefe("Nummer eintragen" in meldung, "leeres Feld sagt was zu tun ist") dialog.destroy() # ---- 7. Pflichtfelder: ohne Anschrift wird nichts geschrieben ----------- vorher = sorted(os.listdir(ordner)) warnungen.clear() f.k_strasse.set("") f.k_plzort.set("") f.anzahl_vars[erste].set("2") f.update() f.pdf_erstellen() pruefe(warnungen and "Kundenangaben fehlen" in warnungen[0], f"fehlende Anschrift wird gemeldet ({warnungen})") pruefe(sorted(os.listdir(ordner)) == vorher, "ohne Anschrift wurde keine PDF geschrieben") warnungen.clear() f.k_vorname.set("") f.k_strasse.set("Musterweg 1") f.k_plzort.set("12345 Musterstadt") f.update() f.pdf_erstellen() pruefe(sorted(os.listdir(ordner)) == vorher, "auch ohne Vornamen wird nichts geschrieben") f.k_vorname.set("Anna") f.update() f.pdf_erstellen() pruefe(len(os.listdir(ordner)) == len(vorher) + 1, "mit allen Pflichtfeldern entsteht die PDF") # ---- 8. Was zaehlt als "richtig ausgefuellt"? --------------------------- def kunde_setzen(vor, nach, strasse, ort): f.k_vorname.set(vor) f.k_name.set(nach) f.k_strasse.set(strasse) f.k_plzort.set(ort) return f.kunde_pruefen() faelle_schlecht = [ (" ", "Meier", "Musterweg 1", "12345 Musterstadt", "Vorname nur Leerzeichen"), ("Anna", " ", "Musterweg 1", "12345 Musterstadt", "Nachname nur Leerzeichen"), ("Anna", "x", "Musterweg 1", "12345 Musterstadt", "Nachname ein Buchstabe"), ("Anna", "123", "Musterweg 1", "12345 Musterstadt", "Nachname nur Ziffern"), ("Anna", "Meier", "-", "12345 Musterstadt", "Strasse nur ein Strich"), ("Anna", "Meier", "Musterweg 1", "12345", "nur PLZ, kein Ort"), ("Anna", "Meier", "Musterweg 1", "Musterstadt", "nur Ort, keine PLZ"), ("Anna", "Meier", "Musterweg 1", " ", "Ort nur Leerzeichen"), ] for vor, nach, strasse, ort, was in faelle_schlecht: pruefe(bool(kunde_setzen(vor, nach, strasse, ort)), f"abgelehnt: {was}") faelle_gut = [ ("Anna", "Meier", "Musterweg 1", "12345 Musterstadt", "normaler Fall"), ("Li", "Ng", "Hauptstr. 2", "04416 Markkleeberg", "kurze Namen"), ("Jan", "de Vries", "Kerkstraat 12", "1234 AB Amsterdam", "Niederlande, 4 Ziffern"), ("Hans", "Huber", "Seeweg 3", "5020 Salzburg", "Oesterreich, 4 Ziffern"), ("Anna", "Camping Nord GmbH", "Am Deich 7a", "12345 Musterstadt", "Firma"), ] for vor, nach, strasse, ort, was in faelle_gut: m = kunde_setzen(vor, nach, strasse, ort) pruefe(not m, f"angenommen: {was} {m if m else ''}") # Saeubern: Rand- und Mehrfachleerzeichen verschwinden aus den Feldern kunde_setzen(" Anna ", " von Meier ", " Musterweg 1 ", " 12345 Musterstadt ") pruefe(f.k_vorname.get() == "Anna", f"Vorname getrimmt ({f.k_vorname.get()!r})") pruefe(f.k_name.get() == "von Meier", f"Doppelleerzeichen weg ({f.k_name.get()!r})") pruefe(f.k_plzort.get() == "12345 Musterstadt", f"Ort gesaeubert ({f.k_plzort.get()!r})") # ---- 9. Strasse ohne Hausnummer: fragen, nicht sperren ------------------ kunde_setzen("Anna", "Beispiel", "Am Seeufer", "12345 Musterstadt") pruefe(not f.kunde_pruefen(), "Anschrift ohne Hausnummer ist kein Mangel") vorher = sorted(os.listdir(ordner)) gefragt.clear() antworten["Hausnummer"] = False # Nutzer sagt: nein, doch nicht f.anzahl_vars[erste].set("2") f.update() f.pdf_erstellen() pruefe(any("Hausnummer" in t for t in gefragt), f"nach der Hausnummer wird gefragt ({gefragt})") pruefe(sorted(os.listdir(ordner)) == vorher, "bei Nein wird nichts geschrieben") antworten["Hausnummer"] = True # Nutzer sagt: ja, so ist es richtig f.pdf_erstellen() pruefe(len(os.listdir(ordner)) == len(vorher) + 1, "bei Ja entsteht die PDF trotzdem") # ---- 10. Altrechnung im Muster "alles in der Vornamen-Zeile" ----------- import berechnung as B # noqa: E402 import pdf_renderer as P # noqa: E402 def alt_rechnung(name, dateiname, kunde_zeilen): """Schreibt eine Rechnung wie die alte Fassung: ohne kunde/positionen in den Kenndaten.""" rr = B.berechne(f.cfg["katalog"], {erste: {"anzahl": 3, "personen": 0}}, f.cfg) # Nummer aus dem Dateinamen, damit Kenndaten und Datei zusammenpassen nummer = dateiname.split("_")[1].replace(".pdf", "") kopf = {"rechnungsnummer": nummer, "datum": "12.08.2026", "anreise": "", "abreise": "", "naechte": "", "kunde_zeilen": kunde_zeilen, "nachname": "", "vorname": name} ziel = os.path.join(ordner, dateiname) P.render_rechnung(ziel, f.cfg, rr, kopf) return ziel # a) mit Anschrift im Text, Name komplett in der Vornamen-Zeile alt = alt_rechnung("Andres Schäfer", "Rechnung_2026-900.pdf", ["Herr", "Andres Schäfer", "c/o Stellplatz 12", "Bornaer Chaussee 36", "04416 Markkleeberg"]) meta_alt = BE.kenndaten_lesen(alt) pruefe(not meta_alt.get("kunde"), "Altrechnung hat keine Kundendaten in den Kenndaten") kunde, woher = BE.kundendaten_ermitteln(meta_alt) pruefe(kunde["vorname"] == "Andres", f"Vorname aufgeteilt ({kunde['vorname']!r})") pruefe(kunde["nachname"] == "Schäfer", f"Nachname aufgeteilt ({kunde['nachname']!r})") pruefe(kunde.get("anrede") == "Herr", f"Anrede aus dem Text ({kunde.get('anrede')!r})") pruefe(kunde.get("strasse") == "Bornaer Chaussee 36", f"Straße aus dem Text ({kunde.get('strasse')!r})") pruefe(kunde.get("plzort") == "04416 Markkleeberg", f"PLZ/Ort aus dem Text ({kunde.get('plzort')!r})") pruefe(kunde.get("zusatz") == "c/o Stellplatz 12", f"Zusatz aus dem Text ({kunde.get('zusatz')!r})") # b) sein echter Fall: NUR der Name, keine Anschrift alt2 = alt_rechnung("Andres Schäfer", "Rechnung_2026-901.pdf", ["", "Andres Schäfer", "", "", ""]) kunde2, _ = BE.kundendaten_ermitteln(BE.kenndaten_lesen(alt2)) pruefe(kunde2["nachname"] == "Schäfer", "auch ohne Anschrift wird der Name aufgeteilt") pruefe(not kunde2.get("plzort"), "keine Anschrift erfunden") # c) Formular fuellen: was fehlt, wird benannt meldung = f._formular_aus_meta(BE.kenndaten_lesen(alt2)) pruefe(f.k_vorname.get() == "Andres" and f.k_name.get() == "Schäfer", "Formular bekommt Vor- und Nachnamen getrennt") pruefe("Straße" in meldung and "PLZ" in meldung and "Nachname vom Vornamen" in meldung, f"fehlende Anschrift wird benannt ({meldung})") meldung = f._formular_aus_meta(BE.kenndaten_lesen(alt)) pruefe(f.k_plzort.get() == "04416 Markkleeberg", "Anschrift landet im Formular") pruefe("aus dem Rechnungstext gelesen" in meldung, f"Herkunft wird gesagt ({meldung})") for name in ("Rechnung_2026-900.pdf", "Rechnung_2026-901.pdf"): os.remove(os.path.join(ordner, name)) # ---- 11. Berichtigung: gleicher Dialog, zweiter Knopf ------------------- alt3 = alt_rechnung("Andres Schäfer", "Rechnung_2026-910.pdf", ["", "Andres Schäfer", "", "", ""]) meta3 = BE.kenndaten_lesen(alt3) vorher = sorted(os.listdir(ordner)) zaehler_vor_berichtigung = f.cfg["rechnung_zaehler"] f._berichtigung_starten(meta3, ordner) pruefe(f.berichtigung_stand is not None, "Flag fuer die Berichtigung gesetzt") pruefe(f.r_nummer.get() == "2026-910", f"Rechnung behaelt ihre Nummer ({f.r_nummer.get()})") pruefe(str(f.nummer_entry.cget("state")) == "readonly", "Nummernfeld gesperrt") pruefe(f.k_vorname.get() == "Andres" and f.k_name.get() == "Schäfer", "Name aufgeteilt im Formular") # ohne Anschrift wird auch hier nichts geschrieben warnungen.clear() f.anzahl_vars[erste].set("3") f.update() f.pdf_erstellen() pruefe(warnungen and "Kundenangaben" in warnungen[0], "Pflichtpruefung gilt auch hier") pruefe(sorted(os.listdir(ordner)) == vorher, "nichts geschrieben") # Anschrift ergaenzen und erstellen f.k_strasse.set("Bornaer Chaussee 36") f.k_plzort.set("04416 Markkleeberg") antworten["Berichtigte Rechnung erstellen"] = True f.update() f.pdf_erstellen() import datetime as _dt ziel = os.path.join(ordner, f"Berichtigte Rechnung_{_dt.date.today():%Y-%m-%d}_2026-910.pdf") pruefe(os.path.exists(ziel), f"Blatt geschrieben ({os.path.basename(ziel)})") pruefe(f.berichtigung_stand is None, "Flag nach dem Erstellen wieder weg") pruefe(str(f.nummer_entry.cget("state")) == "normal", "Nummernfeld wieder frei") pruefe(os.path.exists(alt3), "die berichtigte Rechnung liegt unangetastet da") pruefe(f.cfg["rechnung_zaehler"] == zaehler_vor_berichtigung, f"Zaehler unveraendert - die Berichtigung verbraucht keine Nummer " f"({f.cfg['rechnung_zaehler']} vs {zaehler_vor_berichtigung})") meta_b = BE.kenndaten_lesen(ziel) pruefe(meta_b.get("art") == "berichtigung", "art = berichtigung") pruefe(meta_b.get("berichtigt_zu") == "2026-910", "Bezug auf die Rechnung") pruefe(float(meta_b.get("gesamt") or 0) == 0.0, f"Kenndaten tragen 0 - keine zweite Einnahme ({meta_b.get('gesamt')})") from pypdf import PdfReader as _PR # noqa: E402 text = _PR(ziel).pages[0].extract_text() pruefe("Berichtigte Rechnung" in text, "Ueberschrift steht auf dem Blatt") pruefe("2026-910" in text, "Rechnungsnummer steht drauf") pruefe("31 Abs. 5 UStDV" in text and "zus" in text and "tzliche Leistung" in text, "der Schutzsatz steht drauf (keine zusaetzliche Leistung, § 31 Abs. 5)") pruefe("04416 Markkleeberg" in text, "berichtigte Anschrift steht drauf") liste3, kaputt3 = einnahmen.scanne(ordner, {"ust_saetze": [7, 19]}) pruefe(not any("Berichtigte" in os.path.basename(e.pdf_pfad) for e in liste3), "Steuerrechner zaehlt das Blatt nicht als Einnahme") pruefe(not any("Berichtigte" in os.path.basename(p) for p, _ in kaputt3), f"und meldet es nicht als Fehler ({kaputt3})") for name in ("Rechnung_2026-910.pdf", os.path.basename(ziel)): os.remove(os.path.join(ordner, name)) f.destroy() # Wegwerf-Ordner nur bei Fehlern behalten - sonst laeuft das Temp-Verzeichnis # voll. Ein Pruefstand, der 400 Ordner hinterlaesst, ist selbst ein Problem. if not fehler: import shutil as _sh _sh.rmtree(tmp, ignore_errors=True) print("---") print(f"Ordner: {ordner}") print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}") sys.exit(1 if fehler else 0)