Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle

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
This commit is contained in:
TheMockTv 2026-09-07 18:39:03 +02:00
parent 5babfc3e2e
commit 2b09d15cd0
36 changed files with 563 additions and 364 deletions

109
app.py
View file

@ -476,34 +476,15 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
WAS DABEI ERHALTEN BLEIBT
-------------------------
Alles Eingetippte. Ein Themenwechsel darf keine halb getippte Rechnung
wegwerfen.
wegwerfen. Um die Tk-Variablen kuemmert sich variablen_sichern() im
ThemeMixin - dort steht auch, warum ALLE gesichert werden und nicht
eine gepflegte Auswahl.
Dass die Werte an StringVars haengen, reicht dafuer NICHT: die
_build_-Methoden legen ihre Vars selbst an (`self.k_name =
tk.StringVar()`), beim Neubau stehen also neue, leere Vars da. Beim
ersten Versuch waren Kundenname und Rechnungsnummer weg, waehrend die
Mengen blieben - die hatte ich einzeln gesichert.
Deshalb werden hier ALLE Variablen des Fensters gesichert, nicht eine
Auswahl: durchgegangen wird, was das Objekt an Tk-Variablen traegt.
Wer spaeter ein Feld hinzufuegt, muss nichts eintragen - es ist
automatisch dabei. Eine Liste zum Pflegen waere genau die Sorte, die
man beim naechsten Feld vergisst.
Hier stehen nur die Dinge, die es sonst nirgends gibt: die Mengen und
Personenzahlen der Leistungszeilen (sie haengen an Woerterbuechern,
nicht am Fenster) und das Blatt, das gerade vorn war.
"""
gesichert = {}
for name in dir(self):
if name.startswith("__"):
continue
try:
wert = getattr(self, name)
except Exception: # noqa: BLE001 - Eigenschaften koennen werfen
continue
if isinstance(wert, (tk.StringVar, tk.IntVar, tk.DoubleVar,
tk.BooleanVar)):
try:
gesichert[name] = wert.get()
except tk.TclError:
pass
gesichert = self.variablen_sichern()
mengen = {nr: v.get() for nr, v in self.anzahl_vars.items()}
personen = {nr: v.get() for nr, v in self.personen_vars.items()}
blatt = None
@ -512,19 +493,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
except Exception: # noqa: BLE001 - dann eben das erste Blatt
pass
# Fremde Fenster stehen lassen! Ein tk.Toplevel IST ein Kind des
# Hauptfensters - der offene Darstellungsdialog wurde hier bisher
# mitgeloescht, und die Zeile danach fasste eine Leiche an
# ("invalid command name .!toplevel"). Gefunden am 07.09.2026 von der
# Pruefkette, mit dem echten Bedienweg gemessen: Klick auf "Dunkel"
# -> Farbe wechselt, Fenster ist weg, Traceback im Log.
#
# Der Pruefstand hat es nicht gefunden, weil er cfg["darstellung"] von
# Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser
# dem Weg, den ein Mensch geht.
for kind in self.winfo_children():
if not isinstance(kind, tk.Toplevel):
kind.destroy()
self.kinder_loeschen()
self.flaeche = None
self._text_zellen = []
@ -536,14 +505,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
self._build_positionen()
self.flaeche.anordnen()
for name, wert in gesichert.items():
neu = getattr(self, name, None)
if isinstance(neu, (tk.StringVar, tk.IntVar, tk.DoubleVar,
tk.BooleanVar)):
try:
neu.set(wert)
except tk.TclError:
pass
self.variablen_zuruecksetzen(gesichert)
for nr, wert in mengen.items():
if nr in self.anzahl_vars:
self.anzahl_vars[nr].set(wert)
@ -558,46 +520,17 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
self._buch_anzeigen()
self.recalc()
def dlg_darstellung(self):
"""Hell, dunkel oder wie Windows - und sofort sichtbar."""
d = tk.Toplevel(self)
d.title("Darstellung")
d.transient(self)
d.resizable(False, False)
self._style_dialog(d)
rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text="Darstellung", font=(theme.SCHRIFT_NAME, 11, "bold"),
foreground=self.col_accent).pack(anchor="w")
ttk.Label(rahmen, justify="left", wraplength=340, foreground=self.col_muted,
text="Gilt nur für das Programmfenster. Die Farbe der Rechnung "
"steht unter Einstellungen → Firmendaten – das Blatt "
"gehört dem Kunden, nicht dem Programm.").pack(
anchor="w", pady=(6, 12))
# Der Dialog selbst steht im ThemeMixin (theme.py) - er sah in beiden
# Programmen gleich aus. Verschieden ist nur der Hinweis darin und die
# Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen.
DARSTELLUNG_HINWEIS = (
"Gilt nur für das Programmfenster. Die Farbe der Rechnung steht unter "
"Einstellungen → Firmendaten – das Blatt gehört dem Kunden, nicht dem "
"Programm.")
gewaehlt = tk.StringVar(value=theme.modus(self.cfg))
def uebernehmen():
neu = gewaehlt.get()
if neu == theme.modus(self.cfg):
return
self.cfg["darstellung"] = neu
config.save_config(self.cfg)
self._oberflaeche_neu()
# Der Dialog gehoert noch zur alten Darstellung - er wird
# mitgefaerbt, sonst steht ein helles Fenster vor einem dunklen.
self._style_dialog(d)
for wert in theme.MODI:
ttk.Radiobutton(rahmen, text=theme.MODUS_NAMEN[wert], value=wert,
variable=gewaehlt, command=uebernehmen).pack(
anchor="w", pady=3)
ttk.Separator(rahmen, orient="horizontal").pack(fill="x", pady=(12, 10))
ttk.Button(rahmen, text="Schließen", width=14,
command=d.destroy).pack(anchor="e")
theme.mittig(d, self)
return d
def _cfg_speichern(self):
"""Hier ist die Konfiguration eine config.json und will geschrieben sein."""
config.save_config(self.cfg)
def _umbruch_nachziehen(self, runde=0, vorige_breite=None):
"""Den Textumbruch der Leistungsspalte auf ihre echte Breite setzen.
@ -856,7 +789,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
def _fenstersymbol(self):
"""Das Programmsymbol auf das Fenster legen."""
return wasserzeichen.symbol_setzen(self, dunkel=getattr(self, "_dark_mode", False))
return wasserzeichen.symbol_setzen(self)
def _hinweis_wenn_sichtbar(self, versuche=0):
"""Wartet, bis das Fenster gezeichnet ist, und fragt dann.
@ -1108,7 +1041,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
d.protocol("WM_DELETE_WINDOW", d.destroy)
d.bind("<Escape>", lambda _e: d.destroy())
d.update_idletasks()
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
theme.mittig(d, self)
d.grab_set()
self.wait_window(d)
@ -1458,7 +1390,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
d.protocol("WM_DELETE_WINDOW", d.destroy)
d.bind("<Escape>", lambda _e: d.destroy())
d.update_idletasks()
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
theme.mittig(d, self)
d.grab_set()
self.wait_window(d)