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
401 lines
16 KiB
Python
401 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Pruefstand fuer die ZWEI Regeln, um die es dem Programm eigentlich geht:
|
||
|
||
1. Eine Rechnungsnummer wird nie zweimal vergeben.
|
||
2. Eine ausgestellte Rechnung wird nie veraendert und nie geloescht.
|
||
|
||
Warum es diesen Pruefstand gibt: pruef_storno.py lief 100x gruen und pruefte
|
||
trotzdem keine der beiden Regeln - er ging immer nur den gluecklichen Weg mit
|
||
sauber hochgezaehltem Zaehler. Ein Mutationstest hat gezeigt, dass man BEIDE
|
||
Sperren gegen die doppelte Nummer ausbauen kann, ohne dass er rot wird.
|
||
|
||
Darum hier:
|
||
- Der Bestand wird ueber PRUEFSUMMEN verglichen, nicht ueber os.path.exists.
|
||
Eine Datei, die noch da ist, kann trotzdem veraendert worden sein.
|
||
- Die Nummer wird von HAND ins Feld getippt, so wie es der Nutzer tut.
|
||
- Unbekannte Dialoge lassen den Lauf scheitern, statt still "Nein" zu sagen.
|
||
"""
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
|
||
QUELLE = r"C:\claude\rechnungstool"
|
||
sys.path.insert(0, QUELLE)
|
||
|
||
tmp = tempfile.mkdtemp(prefix="kern_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)
|
||
|
||
# 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: jede Frage muss angemeldet sein --------------------------------
|
||
antworten = {}
|
||
gefragt = []
|
||
meldungen = []
|
||
|
||
|
||
def askyesno(titel, text="", **kw):
|
||
gefragt.append(titel)
|
||
for schluessel, wert in antworten.items():
|
||
if schluessel in titel:
|
||
return wert
|
||
# Kein stilles "Nein": sonst haelt der Pruefstand einen abgebrochenen
|
||
# Ablauf faelschlich fuer ein Ergebnis.
|
||
raise AssertionError(f"unangemeldeter Dialog: {titel!r} — {text[:80]!r}")
|
||
|
||
|
||
# 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: meldungen.append(("info", a[0] if a else ""))
|
||
messagebox.showwarning = lambda *a, **k: meldungen.append(("warnung", a[0] if a else ""))
|
||
messagebox.showerror = lambda *a, **k: meldungen.append(("fehler", a[0] if a else ""))
|
||
|
||
|
||
def bestand_lesen():
|
||
"""Pruefsumme je PDF im Ordner - erkennt auch stille Aenderungen am Inhalt."""
|
||
stand = {}
|
||
for name in sorted(os.listdir(ordner)):
|
||
if name.lower().endswith(".pdf"):
|
||
with open(os.path.join(ordner, name), "rb") as f:
|
||
stand[name] = hashlib.sha256(f.read()).hexdigest()
|
||
return stand
|
||
|
||
|
||
def unveraendert(vorher, ausser=()):
|
||
"""Alle Dateien aus <vorher> sind byteweise unveraendert (bis auf <ausser>)."""
|
||
jetzt = bestand_lesen()
|
||
for name, summe in vorher.items():
|
||
if name in ausser:
|
||
continue
|
||
if jetzt.get(name) != summe:
|
||
return False, (f"{name} " + ("ist weg" if name not in jetzt else "wurde verändert"))
|
||
return True, ""
|
||
|
||
|
||
def kunde_setzen(vor, nach, tage="2"):
|
||
f.k_vorname.set(vor)
|
||
f.k_name.set(nach)
|
||
f.k_strasse.set("Musterweg 1")
|
||
f.k_plzort.set("12345 Musterstadt")
|
||
f.anzahl_vars[erste].set(tage)
|
||
f.update()
|
||
|
||
|
||
f = A.RechnungsApp()
|
||
f.update()
|
||
f.cfg["output_dir"] = ordner
|
||
erste = sorted(f.anzahl_vars)[0]
|
||
antworten["Fertig"] = False # "jetzt oeffnen?" immer nein
|
||
antworten["endgültig erstellen"] = True # die Sicherheitsfrage bejahen
|
||
|
||
# ---- Die Sicherheitsfrage muss auch wirklich abbrechen ----------------------
|
||
# "Sind alle Angaben richtig?" ist die letzte Gelegenheit vor dem Beleg. Sagt
|
||
# der Nutzer Nein, darf NICHTS entstehen - sonst waere die Frage eine Attrappe.
|
||
# Aus der Sicherheitsfrage ist das Auswahlfenster geworden (Rechnung oder
|
||
# Proforma). "Abbrechen" heisst dort: gar nichts schreiben.
|
||
f._testwahl = None
|
||
kunde_setzen("Test", "Abbruch", "1")
|
||
f.pdf_erstellen()
|
||
pruefe(not [n for n in os.listdir(ordner) if n.lower().endswith(".pdf")],
|
||
"bei „Nein“ auf die Sicherheitsfrage entsteht keine PDF")
|
||
f._testwahl = "rechnung"
|
||
|
||
# ---- Ausgangslage: zwei echte Rechnungen ------------------------------------
|
||
kunde_setzen("Anna", "Beispiel", "4")
|
||
f.pdf_erstellen()
|
||
kunde_setzen("Bernd", "Zweit", "3")
|
||
f.pdf_erstellen()
|
||
pruefe(sorted(bestand_lesen()) == ["Rechnung_2026-001_Beispiel.pdf",
|
||
"Rechnung_2026-002_Zweit.pdf"],
|
||
f"zwei Rechnungen angelegt ({sorted(bestand_lesen())})")
|
||
|
||
# =============================================================================
|
||
# REGEL 1: eine Nummer nie zweimal - auch nicht, wenn sie von Hand kommt
|
||
# =============================================================================
|
||
print("--- Regel 1: Nummer von Hand auf eine vergebene setzen ---")
|
||
vorher = bestand_lesen()
|
||
gefragt.clear()
|
||
antworten["Rechnungsnummer schon vergeben"] = True # "naechste freie nehmen?"
|
||
kunde_setzen("Carla", "Dritt", "1")
|
||
f.r_nummer.set("2026-001") # genau das tut der Nutzer von Hand
|
||
f.update()
|
||
f.pdf_erstellen()
|
||
|
||
pruefe(any("schon vergeben" in t for t in gefragt),
|
||
f"Programm meldet die vergebene Nummer ({gefragt})")
|
||
ok, warum = unveraendert(vorher)
|
||
pruefe(ok, f"die beiden alten Rechnungen sind byteweise unverändert ({warum})")
|
||
pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-003_Dritt.pdf")),
|
||
"die neue Rechnung bekam die nächste FREIE Nummer 2026-003")
|
||
pruefe(f.r_nummer.get() != "2026-001", f"Feld zeigt nicht mehr die alte Nummer ({f.r_nummer.get()})")
|
||
|
||
print("--- Regel 1: Nutzer lehnt die Ausweichnummer ab ---")
|
||
vorher = bestand_lesen()
|
||
antworten["Rechnungsnummer schon vergeben"] = False
|
||
kunde_setzen("Doris", "Vier", "1")
|
||
f.r_nummer.set("2026-002")
|
||
f.update()
|
||
f.pdf_erstellen()
|
||
ok, warum = unveraendert(vorher)
|
||
pruefe(ok and bestand_lesen() == vorher,
|
||
f"bei Nein wird gar nichts geschrieben und nichts verändert ({warum})")
|
||
|
||
print("--- Regel 1: Zähler steht HINTER dem Ordner (config aus dem Backup) ---")
|
||
f.cfg["rechnung_zaehler"] = 0 # so sieht eine zurückgespielte config aus
|
||
f.berichtigung_stand = None
|
||
f.storno_stand = None
|
||
f._neue_nummer_vorschlagen()
|
||
pruefe(f.r_nummer.get() == "2026-004",
|
||
f"Vorschlag überspringt die belegten Nummern ({f.r_nummer.get()})")
|
||
|
||
print("--- Regel 1: der Zähler darf nur vorwärts ---")
|
||
# Zweite Verteidigungslinie hinter dem Ordner-Abgleich: wer von Hand eine
|
||
# niedrigere freie Nummer nimmt, darf den Zähler nicht zurückdrehen - sonst
|
||
# schlägt das Programm die dazwischenliegenden Nummern ein zweites Mal vor.
|
||
kunde_setzen("Emil", "Hoch", "1")
|
||
f.r_nummer.set("2026-009")
|
||
f.update()
|
||
f.pdf_erstellen()
|
||
pruefe(f.cfg["rechnung_zaehler"] == 9, f"Zähler steht auf 9 ({f.cfg['rechnung_zaehler']})")
|
||
kunde_setzen("Frida", "Tief", "1")
|
||
f.r_nummer.set("2026-007")
|
||
f.update()
|
||
f.pdf_erstellen()
|
||
pruefe(os.path.exists(os.path.join(ordner, "Rechnung_2026-007_Tief.pdf")),
|
||
"die niedrigere freie Nummer 2026-007 wurde geschrieben")
|
||
pruefe(f.cfg["rechnung_zaehler"] == 9,
|
||
f"Zähler bleibt auf 9, fällt NICHT auf 7 zurück ({f.cfg['rechnung_zaehler']})")
|
||
f._neue_nummer_vorschlagen()
|
||
pruefe(f.r_nummer.get() == "2026-010",
|
||
f"nächster Vorschlag ist 2026-010, keine schon vergebene ({f.r_nummer.get()})")
|
||
|
||
print("--- Regel 1: Jahreswechsel - beide Quellen müssen dasselbe sagen ---")
|
||
f.cfg["rechnung_jahr"] = 2025
|
||
f.cfg["rechnung_zaehler"] = 87
|
||
f._neue_nummer_vorschlagen()
|
||
vom_formular = f.r_nummer.get()
|
||
vom_storno = BE.naechste_freie_nummer(ordner, f.cfg)
|
||
pruefe(vom_formular == vom_storno,
|
||
f"Formular und Storno-Weg schlagen dieselbe Nummer vor ({vom_formular} / {vom_storno})")
|
||
pruefe(vom_formular.startswith("2026-"),
|
||
f"neues Jahr fängt im aktuellen Jahr an ({vom_formular})")
|
||
f.cfg["rechnung_jahr"], f.cfg["rechnung_zaehler"] = 2026, 3
|
||
config.save_config(f.cfg)
|
||
|
||
# =============================================================================
|
||
# REGEL 2: ausgestellte Rechnung nie anfassen
|
||
# =============================================================================
|
||
print("--- Regel 2: Storno über den Dialogknopf, nicht über die interne Methode ---")
|
||
vorher = bestand_lesen()
|
||
gefragt.clear()
|
||
f.dlg_storno()
|
||
f.update()
|
||
import tkinter as tk # noqa: E402
|
||
from tkinter import ttk # noqa: E402
|
||
dlg = [w for w in f.winfo_children() if isinstance(w, tk.Toplevel)][-1]
|
||
feld = knopf = None
|
||
for rahmen in dlg.winfo_children():
|
||
for w in rahmen.winfo_children():
|
||
if isinstance(w, ttk.Entry):
|
||
feld = w
|
||
elif isinstance(w, ttk.Frame):
|
||
for b in w.winfo_children():
|
||
if isinstance(b, ttk.Button) and "Storno erstellen" in str(b.cget("text")):
|
||
knopf = b
|
||
pruefe(feld is not None and knopf is not None, "Storno-Dialog hat Feld und Knopf")
|
||
feld.delete(0, "end")
|
||
feld.insert(0, "2026-001")
|
||
knopf.invoke() # der ECHTE Weg, nicht _storno_anlegen()
|
||
f.update()
|
||
|
||
pruefe(f.storno_stand is not None and f.storno_stand["zu"] == "2026-001",
|
||
f"der Knopf hat wirklich ein Storno gestartet ({f.storno_stand})")
|
||
storno_datei = os.path.basename(f.storno_stand["pfad"]) if f.storno_stand else ""
|
||
ok, warum = unveraendert(vorher)
|
||
pruefe(ok, f"alle bisherigen Rechnungen unverändert ({warum})")
|
||
|
||
print("--- Regel 2: was steht auf dem Storno-Blatt? ---")
|
||
from pypdf import PdfReader # noqa: E402
|
||
text = PdfReader(f.storno_stand["pfad"]).pages[0].extract_text()
|
||
pruefe("Stornorechnung" in text, "Überschrift „Stornorechnung“ steht drauf")
|
||
pruefe("2026-001" in text, "Bezug auf die alte Nummer steht drauf")
|
||
# pypdf legt jeden Tabellenwert in eine eigene Zeile - die Betraege stehen also
|
||
# NACH ihrer Beschriftung, nicht daneben.
|
||
zeilen = text.splitlines()
|
||
|
||
|
||
def betraege_nach(label, wieviele=6):
|
||
"""Die Zahlenwerte, die auf die Zeile mit <label> folgen."""
|
||
for i, z in enumerate(zeilen):
|
||
if label in z:
|
||
return [w.strip() for w in zeilen[i + 1:i + 1 + wieviele]
|
||
if "," in w and any(c.isdigit() for c in w)]
|
||
return []
|
||
|
||
|
||
pos = betraege_nach("Storno der Rechnung")
|
||
pruefe(bool(pos), f"Positionszeile hat Beträge ({pos})")
|
||
pruefe(all(w.startswith("-") for w in pos),
|
||
f"Positionszeile ist NEGATIV, nicht nur der Summenblock ({pos})")
|
||
gesamt = betraege_nach("Gesamtbetrag", 1)
|
||
pruefe(gesamt and gesamt[0].startswith("-"), f"Gesamtbetrag negativ ({gesamt})")
|
||
netto = betraege_nach("Nettobetrag", 1)
|
||
pruefe(netto and netto[0].startswith("-"), f"Nettobetrag negativ ({netto})")
|
||
|
||
print("--- Regel 2: berichtigte Rechnung schreiben, alter Bestand bleibt ---")
|
||
vorher = bestand_lesen()
|
||
antworten["Storno wirklich"] = True
|
||
kunde_setzen("Anna", "Beispiel", "3")
|
||
f.pdf_erstellen()
|
||
ok, warum = unveraendert(vorher)
|
||
pruefe(ok, f"Storno und alle Rechnungen unverändert ({warum})")
|
||
pruefe(len(bestand_lesen()) == len(vorher) + 1, "genau eine Datei kam dazu")
|
||
|
||
# =============================================================================
|
||
# Berichtigung: zweimal am selben Tag darf nicht stillschweigend überschreiben
|
||
# =============================================================================
|
||
print("--- Berichtigung zweimal am selben Tag ---")
|
||
antworten["Berichtigte Rechnung erstellen"] = True
|
||
meta = next(x for x in BE.rechnungen_im_ordner(ordner)
|
||
if x.get("rechnungsnummer") == "2026-002")
|
||
f._berichtigung_starten(meta, ordner)
|
||
f.k_strasse.set("Neue Straße 5")
|
||
f.update()
|
||
f.pdf_erstellen()
|
||
erste_fassung = bestand_lesen()
|
||
ziel = [n for n in erste_fassung if n.startswith("Berichtigte Rechnung")]
|
||
pruefe(len(ziel) == 1, f"Berichtigungsblatt geschrieben ({ziel})")
|
||
|
||
antworten["Berichtigung von heute ersetzen"] = False # NEIN, nicht ersetzen
|
||
f._berichtigung_starten(meta, ordner)
|
||
f.k_strasse.set("Ganz andere Straße 9")
|
||
f.update()
|
||
gefragt.clear()
|
||
f.pdf_erstellen()
|
||
pruefe(any("ersetzen" in t for t in gefragt), f"vor dem Ersetzen wird gefragt ({gefragt})")
|
||
ok, warum = unveraendert(erste_fassung)
|
||
pruefe(ok, f"bei Nein bleibt die erste Fassung unangetastet ({warum})")
|
||
|
||
# =============================================================================
|
||
# Der Altbestand: dieselbe Nummer liegt MEHRFACH im Ordner (Windows-Kopie).
|
||
# Dann darf der Storno-Dialog nicht raten, welcher Beleg gemeint ist.
|
||
# =============================================================================
|
||
print("--- zwei Belege unter einer Nummer ---")
|
||
# Die abgelehnte Berichtigung liegt noch im Zwischenspeicher - so wie der Nutzer
|
||
# es dann macht: zuruecksetzen.
|
||
f.reset_formular()
|
||
pruefe(f.berichtigung_stand is None, "Zurücksetzen räumt die offene Berichtigung weg")
|
||
import shutil as _sh # noqa: E402
|
||
quelle = os.path.join(ordner, "Rechnung_2026-002_Zweit.pdf")
|
||
kopie = os.path.join(ordner, "Rechnung_2026-002_Zweit (1).pdf")
|
||
_sh.copy(quelle, kopie)
|
||
|
||
pruefe(len(BE.vorhandene_rechnungen(ordner, "2026-002")) == 2,
|
||
"die Windows-Kopie zählt bei der Nummernvergabe mit")
|
||
|
||
f.dlg_storno()
|
||
f.update()
|
||
dlg = [w for w in f.winfo_children() if isinstance(w, tk.Toplevel)][-1]
|
||
feld = liste = knopf = None
|
||
for rahmen in dlg.winfo_children():
|
||
for w in rahmen.winfo_children():
|
||
if isinstance(w, ttk.Entry):
|
||
feld = w
|
||
elif isinstance(w, tk.Listbox):
|
||
liste = w
|
||
elif isinstance(w, ttk.Frame):
|
||
for b in w.winfo_children():
|
||
if isinstance(b, ttk.Button) and "Storno erstellen" in str(b.cget("text")):
|
||
knopf = b
|
||
elif isinstance(w, ttk.Label) and w.cget("textvariable"):
|
||
meldezeile = w
|
||
pruefe(liste is not None, "der Dialog hat eine Auswahlliste")
|
||
|
||
feld.delete(0, "end")
|
||
feld.insert(0, "2026-002")
|
||
feld.event_generate("<KeyRelease>")
|
||
f.update()
|
||
meldung = f.getvar(meldezeile.cget("textvariable"))
|
||
pruefe("2×" in meldung or "2x" in meldung, f"die Doppelvergabe wird gemeldet ({meldung.splitlines()[0]})")
|
||
pruefe(liste.winfo_ismapped() and liste.size() == 2,
|
||
f"beide Belege stehen zur Auswahl ({liste.size()})")
|
||
|
||
vorher = bestand_lesen()
|
||
knopf.invoke() # ohne Auswahl darf NICHTS passieren
|
||
f.update()
|
||
pruefe(f.storno_stand is None, "ohne Auswahl wird kein Storno angelegt")
|
||
pruefe(bestand_lesen() == vorher, "und nichts geschrieben")
|
||
|
||
# bewusst den ZWEITEN Eintrag wählen - welche Datei das ist, sagt die Liste
|
||
zweiter = liste.get(1)
|
||
datei_zwei = zweiter.split()[-1]
|
||
liste.selection_set(1)
|
||
liste.event_generate("<<ListboxSelect>>")
|
||
f.update()
|
||
gewaehlt = f.getvar(meldezeile.cget("textvariable"))
|
||
pruefe(datei_zwei in gewaehlt,
|
||
f"die Meldung nennt genau die gewählte Datei ({datei_zwei})")
|
||
pruefe(liste.get(0).split()[-1] not in gewaehlt,
|
||
"und NICHT die andere Datei mit derselben Nummer")
|
||
knopf.invoke()
|
||
f.update()
|
||
pruefe(f.storno_stand is not None and f.storno_stand["zu"] == "2026-002",
|
||
f"nach der Auswahl läuft das Storno ({f.storno_stand})")
|
||
f._storno_verwerfen("Pruefstand raeumt auf")
|
||
os.remove(kopie)
|
||
|
||
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)
|