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
269 lines
9 KiB
Python
269 lines
9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: hell, dunkel, automatisch - und nichts geht dabei verloren.
|
|
|
|
Sein Wortlaut vom 07.09.2026:
|
|
|
|
"waere nun noch gut das mann in den einstellungen einstellen kann hell
|
|
oder dunkel oder automatik das fehlt sollte eigendlich drin sein"
|
|
|
|
GEPRUEFT WIRD DREIERLEI
|
|
|
|
1. Die Einstellung wirkt. "hell" ist hell, "dunkel" ist dunkel, "auto" folgt
|
|
Windows.
|
|
|
|
2. Beim Umschalten bleibt jede Eingabe stehen. Das ist der Punkt, an dem es
|
|
beim Bauen schiefging: die Mengen der Leistungszeilen hatte ich einzeln
|
|
gesichert, Kundenname und Rechnungsnummer nicht - die _build_-Methoden
|
|
legen ihre Variablen selbst an, beim Neubau standen also leere da. Wer
|
|
waehrend einer halb getippten Rechnung auf dunkel stellt, darf sie nicht
|
|
verlieren.
|
|
|
|
3. NACH dem Wechsel traegt kein Widget mehr eine Farbe der anderen Palette.
|
|
Das ist die eigentliche Falle beim Umfaerben: ein einzelnes vergessenes
|
|
Label leuchtet hell im dunklen Fenster, und niemand findet die Stelle
|
|
wieder. Deshalb wird der ganze Widget-Baum abgegangen und mit den Farben
|
|
der jeweils ANDEREN Palette verglichen.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import tkinter as tk
|
|
|
|
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, QUELLE)
|
|
|
|
|
|
os.chdir(QUELLE)
|
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
|
|
|
from tkinter import messagebox # noqa: E402
|
|
|
|
messagebox.askyesno = lambda *a, **k: True
|
|
for _n in ("showinfo", "showwarning", "showerror"):
|
|
setattr(messagebox, _n, lambda *a, **k: None)
|
|
|
|
import config # noqa: E402
|
|
|
|
# Auf einer Kopie arbeiten - die echte Konfiguration wird nicht angefasst.
|
|
tmp = tempfile.mkdtemp(prefix="darstellung_")
|
|
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["nummer_eingerichtet"] = True
|
|
roh.pop("darstellung", None)
|
|
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"),
|
|
ensure_ascii=False, indent=2)
|
|
|
|
import app as A # noqa: E402
|
|
import theme # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
f = A.RechnungsApp()
|
|
|
|
|
|
def pumpen(sekunden=1.0):
|
|
ende = time.time() + sekunden
|
|
while time.time() < ende:
|
|
f.update()
|
|
time.sleep(0.02)
|
|
|
|
|
|
pumpen(2.0)
|
|
|
|
# ---- Ohne Einstellung: automatisch --------------------------------------
|
|
pruefe(theme.modus(f.cfg) == "auto",
|
|
f"ohne Eintrag gilt 'auto' ({theme.modus(f.cfg)!r})")
|
|
pruefe(f._dark_mode == theme.windows_dark_mode(),
|
|
"und folgt damit dem Windows-Theme")
|
|
|
|
# ---- Eingaben, die den Wechsel ueberleben muessen ------------------------
|
|
f.k_vorname.set("Erika")
|
|
f.k_name.set("Mustermann")
|
|
f.k_strasse.set("Musterweg 12")
|
|
f.k_plzort.set("04416 Markkleeberg")
|
|
f.r_nummer.set("2026-099")
|
|
f.r_anreise.set("01.09.2026")
|
|
erste = sorted(f.anzahl_vars)[0]
|
|
f.anzahl_vars[erste].set("3")
|
|
pumpen(0.5)
|
|
|
|
|
|
def farben(dunkel):
|
|
"""Die Palette der einen oder anderen Darstellung, als Menge."""
|
|
merker = f._dark_mode
|
|
f.cfg["darstellung"] = "dunkel" if dunkel else "hell"
|
|
f._apply_theme()
|
|
satz = {getattr(f, n).lower() for n in dir(f)
|
|
if n.startswith("col_") and isinstance(getattr(f, n), str)}
|
|
f._dark_mode = merker
|
|
return satz
|
|
|
|
|
|
HELL = farben(False)
|
|
DUNKEL = farben(True)
|
|
# Was in BEIDEN Paletten vorkommt, taugt nicht als Beweis.
|
|
NUR_HELL = HELL - DUNKEL
|
|
NUR_DUNKEL = DUNKEL - HELL
|
|
|
|
|
|
def fremdfarben(verboten):
|
|
"""Alle Widgets, die eine Farbe der falschen Palette tragen."""
|
|
treffer = []
|
|
|
|
def durchgehen(w):
|
|
for kind in w.winfo_children():
|
|
for eigenschaft in ("bg", "background", "fg", "foreground"):
|
|
try:
|
|
wert = str(kind.cget(eigenschaft)).lower()
|
|
except tk.TclError:
|
|
continue
|
|
if wert in verboten:
|
|
treffer.append((str(kind), eigenschaft, wert))
|
|
durchgehen(kind)
|
|
|
|
durchgehen(f)
|
|
return treffer
|
|
|
|
|
|
# ---- Die drei Modi ------------------------------------------------------
|
|
for wunsch, soll_dunkel in (("hell", False), ("dunkel", True)):
|
|
f.cfg["darstellung"] = wunsch
|
|
config.save_config(f.cfg)
|
|
f._oberflaeche_neu()
|
|
pumpen(0.8)
|
|
|
|
pruefe(f._dark_mode == soll_dunkel,
|
|
f"'{wunsch}' zeichnet {'dunkel' if soll_dunkel else 'hell'} "
|
|
f"(Hintergrund {f.col_bg})")
|
|
|
|
# Nichts darf verlorengehen.
|
|
pruefe(f.k_name.get() == "Mustermann",
|
|
f"nach dem Wechsel steht der Kundenname noch da ({f.k_name.get()!r})")
|
|
pruefe(f.r_nummer.get() == "2026-099",
|
|
f"und die Rechnungsnummer ({f.r_nummer.get()!r})")
|
|
pruefe(f.k_plzort.get() == "04416 Markkleeberg",
|
|
f"und die Anschrift ({f.k_plzort.get()!r})")
|
|
pruefe(f.r_anreise.get() == "01.09.2026",
|
|
f"und das Anreisedatum ({f.r_anreise.get()!r})")
|
|
pruefe(f.anzahl_vars.get(erste) is not None
|
|
and f.anzahl_vars[erste].get() == "3",
|
|
"und die eingetragene Menge in der Leistungszeile")
|
|
|
|
# Kein Rest der anderen Palette.
|
|
reste = fremdfarben(NUR_DUNKEL if not soll_dunkel else NUR_HELL)
|
|
pruefe(not reste,
|
|
f"in '{wunsch}' ist keine Farbe der anderen Darstellung "
|
|
f"uebrig ({reste[:3]})")
|
|
|
|
# ---- Dialoge gehen mittig ueber dem Programmfenster auf ------------------
|
|
# Seine Ansage: "einfach so programmieren das die popup auf dem fenster mittig
|
|
# vom main fenster auf gehen". Das ist zugleich die Antwort auf die
|
|
# Monitorfrage - steht das Programm auf dem zweiten Bildschirm, geht der
|
|
# Dialog dort auf. Vorher klemmte ein max(0, x) jede negative Koordinate auf
|
|
# null und riss die Fenster auf den Hauptmonitor zurueck.
|
|
d = f.dlg_darstellung()
|
|
pumpen(1.2)
|
|
d.update_idletasks()
|
|
mitte_dialog = d.winfo_rootx() + d.winfo_width() // 2
|
|
mitte_fenster = f.winfo_rootx() + f.winfo_width() // 2
|
|
pruefe(abs(mitte_dialog - mitte_fenster) <= 30,
|
|
f"der Dialog steht mittig ueber dem Fenster "
|
|
f"({mitte_dialog} vs. {mitte_fenster})")
|
|
pruefe(d.winfo_rootx() >= f.winfo_rootx() - 40
|
|
and d.winfo_rootx() + d.winfo_width() <= f.winfo_rootx() + f.winfo_width() + 40,
|
|
"und liegt waagerecht innerhalb des Fensters")
|
|
pruefe(d.winfo_rooty() >= f.winfo_rooty() - 40,
|
|
"und nicht oberhalb davon")
|
|
d.destroy()
|
|
pumpen(0.3)
|
|
|
|
# ---- Der Dialog muss den Klick UEBERLEBEN -------------------------------
|
|
# Gefunden am 07.09.2026 von der Pruefkette: _oberflaeche_neu() loeschte alle
|
|
# Kinder des Hauptfensters - und ein Toplevel IST ein Kind. Der offene Dialog
|
|
# wurde also beim ersten Klick mitgeloescht, die Zeile danach fasste eine
|
|
# Leiche an ("invalid command name .!toplevel").
|
|
#
|
|
# Dieser Pruefstand hat es NICHT gefunden, weil er cfg["darstellung"] von Hand
|
|
# setzte. Hier wird jetzt der Radioknopf gedrueckt - derselbe Weg, den ein
|
|
# Mensch geht.
|
|
d = f.dlg_darstellung()
|
|
pumpen(1.0)
|
|
|
|
|
|
def radioknoepfe(fenster):
|
|
"""Alle Radiobuttons unterhalb eines Fensters, mit ihrer Beschriftung."""
|
|
treffer = []
|
|
|
|
def durchgehen(w):
|
|
for kind in w.winfo_children():
|
|
if kind.winfo_class() == "TRadiobutton":
|
|
try:
|
|
treffer.append((str(kind.cget("text")), kind))
|
|
except tk.TclError:
|
|
pass
|
|
durchgehen(kind)
|
|
|
|
durchgehen(fenster)
|
|
return treffer
|
|
|
|
|
|
knoepfe = radioknoepfe(d)
|
|
pruefe(len(knoepfe) == len(theme.MODI),
|
|
f"der Dialog zeigt alle {len(theme.MODI)} Moeglichkeiten "
|
|
f"({[t for t, _ in knoepfe]})")
|
|
|
|
ziel = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["dunkel"]), None)
|
|
pruefe(ziel is not None, "der Knopf 'Dunkel' ist da")
|
|
if ziel is not None:
|
|
ziel.invoke() # der echte Klick
|
|
pumpen(1.2)
|
|
pruefe(bool(d.winfo_exists()),
|
|
"der Dialog lebt nach dem Klick noch")
|
|
pruefe(f._dark_mode is True,
|
|
f"und die Darstellung ist umgestellt (Hintergrund {f.col_bg})")
|
|
# Und noch einmal, denn genau das war vorher unmoeglich:
|
|
hell = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["hell"]), None)
|
|
if hell is not None:
|
|
try:
|
|
hell.invoke()
|
|
pumpen(1.0)
|
|
pruefe(f._dark_mode is False,
|
|
"ein ZWEITER Klick im selben Dialog wirkt auch")
|
|
except tk.TclError as e:
|
|
pruefe(False, f"der zweite Klick scheiterte ({e})")
|
|
try:
|
|
d.destroy()
|
|
except tk.TclError:
|
|
pass
|
|
pumpen(0.3)
|
|
|
|
# ---- Zurueck auf automatisch --------------------------------------------
|
|
f.cfg["darstellung"] = "auto"
|
|
config.save_config(f.cfg)
|
|
f._oberflaeche_neu()
|
|
pumpen(0.8)
|
|
pruefe(f._dark_mode == theme.windows_dark_mode(),
|
|
"'auto' folgt wieder dem Windows-Theme")
|
|
|
|
# ---- Und die Einstellung ueberlebt den Neustart -------------------------
|
|
f.cfg["darstellung"] = "hell"
|
|
config.save_config(f.cfg)
|
|
f.destroy()
|
|
f = A.RechnungsApp()
|
|
pumpen(2.0)
|
|
pruefe(f._dark_mode is False,
|
|
"die Einstellung steht beim naechsten Start wieder da")
|
|
|
|
try:
|
|
f.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
if fehler:
|
|
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
|
|
sys.exit(1)
|
|
print("GRUEN | Hell, dunkel und automatisch wirken - ohne Datenverlust")
|