Eigene Meldungsfenster, aktiver Reiter hervorgehoben, Pruefschalter als Funktion
Sein Fund: "der ohne gemeinsamen ordner popup ist noch hell" und "er hebt den aktiven tab nicht hervor, der inaktive ist der hervorgehobene". - Meldungen und Ja/Nein-Fragen kommen jetzt aus eigenen Fenstern im Theme (MeldungMixin). messagebox sind Fenster von WINDOWS - sie bleiben hell, egal was das Programm einstellt. Dieselbe Erfahrung steckt schon in der eigenen Menueleiste. 49 Aufrufe umgestellt; das Ordner-Fenster meldet ueber das Hauptfenster mit. ⚠️ Im Pruflauf laeuft beides weiter ueber messagebox - dort haengen die Pruefstaende ihre Antworten ein. Sonst warten sie auf einen Klick. - Reiter: der OFFENE ist jetzt der hellere (Kachelfarbe), die ruhenden liegen auf dem dunklen Grund. Vorher war es andersherum. - config.TESTLAUF ist eine FUNKTION geworden: Pruefstaende importieren config oft, bevor sie CAMPINGHOF_TEST setzen - der einmal berechnete Wert stand dann den ganzen Lauf auf False, und das Programm oeffnete echte Dialoge, auf die niemand klickt. Genau daran hing pruef_api. - Der Erststart-Dialog hat einen eigenen Schalter (_erststart_zeigen), statt am ganzen Pruefschalter zu haengen: ein Pruefstand kann IHN zeigen, ohne dass alle anderen Meldungen wieder echte Fenster aufmachen. - Pruefstand-Fenster stehen nicht mehr in der Taskleiste (-toolwindow): sie liegen neben dem Bildschirm, und ein Eintrag, den man anklickt, ohne dass etwas erscheint, ist schlimmer als gar keiner. - pruef_nummernsperre drueckt jetzt den Radioknopf "Meine Zaehlung weiterfuehren", statt nur ins Zahlenfeld zu schreiben - das Feld ist ohne diesen Klick gesperrt, wie beim Benutzer auch. 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
5ebb02b267
commit
2e3a191f19
8 changed files with 229 additions and 74 deletions
91
app.py
91
app.py
|
|
@ -40,7 +40,7 @@ from bestand import (hoechster_zaehler_im_ordner, naechste_freie_nummer,
|
||||||
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn,
|
from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn,
|
||||||
laufende_pid_lesen)
|
laufende_pid_lesen)
|
||||||
import theme
|
import theme
|
||||||
from theme import ThemeMixin
|
from theme import MeldungMixin, ThemeMixin
|
||||||
from korrektur import KorrekturMixin
|
from korrektur import KorrekturMixin
|
||||||
from einstellungen import EinstellungenMixin
|
from einstellungen import EinstellungenMixin
|
||||||
|
|
||||||
|
|
@ -49,10 +49,12 @@ from einstellungen import EinstellungenMixin
|
||||||
# modaler Dialog wuerde sie haengen lassen - und in der EXE kommt man an den
|
# modaler Dialog wuerde sie haengen lassen - und in der EXE kommt man an den
|
||||||
# Schalter ohnehin nicht heran (Doppelklick uebergibt keine Argumente).
|
# Schalter ohnehin nicht heran (Doppelklick uebergibt keine Argumente).
|
||||||
# python app.py --test oder CAMPINGHOF_TEST=1
|
# python app.py --test oder CAMPINGHOF_TEST=1
|
||||||
TESTLAUF = ("--test" in sys.argv) or os.environ.get("CAMPINGHOF_TEST") == "1"
|
# Der Pruefschalter steht in config (dort als Funktion, siehe Kommentar). Hier
|
||||||
|
# nur noch der Startwert fuer alles, was ihn schon beim Laden braucht.
|
||||||
|
TESTLAUF = config.testlauf()
|
||||||
|
|
||||||
|
|
||||||
class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cfg = config.load_config()
|
self.cfg = config.load_config()
|
||||||
|
|
@ -61,13 +63,20 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
hoehe = max(560, min(840, self.winfo_screenheight() - 90))
|
hoehe = max(560, min(840, self.winfo_screenheight() - 90))
|
||||||
self.geometry(f"1060x{hoehe}")
|
self.geometry(f"1060x{hoehe}")
|
||||||
self.minsize(900, 540)
|
self.minsize(900, 540)
|
||||||
if TESTLAUF:
|
if config.testlauf():
|
||||||
# Ein Pruefstand bedient das Programm, waehrend nebenan jemand
|
# Ein Pruefstand bedient das Programm, waehrend nebenan jemand
|
||||||
# arbeitet. Das Fenster geht deshalb NEBEN dem Bildschirm auf (auf
|
# arbeitet. Das Fenster geht deshalb NEBEN dem Bildschirm auf (auf
|
||||||
# dem zweiten Monitor, wenn einer da ist) - sichtbar, aber nicht im
|
# dem zweiten Monitor, wenn einer da ist) - sichtbar, aber nicht im
|
||||||
# Weg. Unsichtbar machen waere falsch: dann sieht niemand mehr, was
|
# Weg. Unsichtbar machen waere falsch: dann sieht niemand mehr, was
|
||||||
# der Pruefstand eigentlich tut.
|
# der Pruefstand eigentlich tut.
|
||||||
self.geometry(f"+{self.winfo_screenwidth() + 20}+40")
|
self.geometry(f"+{self.winfo_screenwidth() + 20}+40")
|
||||||
|
try:
|
||||||
|
# Kein Eintrag in der Taskleiste: das Fenster liegt neben
|
||||||
|
# dem Bildschirm, und ein Eintrag, den man anklickt, ohne
|
||||||
|
# dass etwas erscheint, ist schlimmer als gar keiner.
|
||||||
|
self.attributes("-toolwindow", True)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|
||||||
self.anzahl_vars = {} # nr -> StringVar
|
self.anzahl_vars = {} # nr -> StringVar
|
||||||
self.personen_vars = {} # nr -> StringVar
|
self.personen_vars = {} # nr -> StringVar
|
||||||
|
|
@ -593,7 +602,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
wird wieder gefragt.
|
wird wieder gefragt.
|
||||||
"""
|
"""
|
||||||
self._buch_oeffnen()
|
self._buch_oeffnen()
|
||||||
if self.buch is None and not TESTLAUF:
|
if self.buch is None and not config.testlauf():
|
||||||
self.dlg_gemeinsam(erststart=True)
|
self.dlg_gemeinsam(erststart=True)
|
||||||
self._buch_anzeigen()
|
self._buch_anzeigen()
|
||||||
|
|
||||||
|
|
@ -602,7 +611,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
self, "Rechnungstool", str(self.cfg.get("gemeinsam_ordner", "") or ""))
|
self, "Rechnungstool", str(self.cfg.get("gemeinsam_ordner", "") or ""))
|
||||||
if not ordner:
|
if not ordner:
|
||||||
if erststart:
|
if erststart:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Ohne gemeinsamen Ordner",
|
"Ohne gemeinsamen Ordner",
|
||||||
"Es wurde kein gemeinsamer Ordner gewählt.\n\n"
|
"Es wurde kein gemeinsamer Ordner gewählt.\n\n"
|
||||||
"Das Rechnungstool kennt jetzt nur die Nummern seiner eigenen "
|
"Das Rechnungstool kennt jetzt nur die Nummern seiner eigenen "
|
||||||
|
|
@ -642,7 +651,13 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
einmal nennen koennen, sonst faengt das Programm bei 001 an und vergibt
|
einmal nennen koennen, sonst faengt das Programm bei 001 an und vergibt
|
||||||
Nummern ein zweites Mal. Danach ist der Zaehler zu.
|
Nummern ein zweites Mal. Danach ist der Zaehler zu.
|
||||||
"""
|
"""
|
||||||
if TESTLAUF or self.cfg.get("nummer_eingerichtet"):
|
# Im Pruflauf bleibt dieser Dialog zu - er wartet auf einen Klick, den dort
|
||||||
|
# niemand macht. Ein Pruefstand, der GENAU IHN pruefen will, setzt
|
||||||
|
# `_erststart_zeigen = True` (dasselbe Muster wie `_dialog_zeigen` beim
|
||||||
|
# Angebot). So bleiben alle anderen Meldungen weiterhin automatisch
|
||||||
|
# beantwortet - vorher musste dafuer der ganze Pruefschalter fallen, und
|
||||||
|
# dann hing der Lauf in der naechsten Meldung.
|
||||||
|
if (config.testlauf() and not getattr(self, "_erststart_zeigen", False)) or self.cfg.get("nummer_eingerichtet"):
|
||||||
return
|
return
|
||||||
jahr = int(self.cfg.get("rechnung_jahr", 0) or datetime.date.today().year)
|
jahr = int(self.cfg.get("rechnung_jahr", 0) or datetime.date.today().year)
|
||||||
gefunden = self.hoechster_stand(jahr)
|
gefunden = self.hoechster_stand(jahr)
|
||||||
|
|
@ -720,7 +735,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
if weg.get() == "weiter" else
|
if weg.get() == "weiter" else
|
||||||
f"Bei 0 anfangen geht nicht mehr: {jahr}-{gefunden:03d} ist schon vergeben.")
|
f"Bei 0 anfangen geht nicht mehr: {jahr}-{gefunden:03d} ist schon vergeben.")
|
||||||
return
|
return
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Sind Sie sicher?",
|
"Sind Sie sicher?",
|
||||||
f"Die nächste Rechnung bekommt die Nummer {jahr}-{zahl + 1:03d}.\n\n"
|
f"Die nächste Rechnung bekommt die Nummer {jahr}-{zahl + 1:03d}.\n\n"
|
||||||
"Das lässt sich danach nicht mehr ändern.\n\n"
|
"Das lässt sich danach nicht mehr ändern.\n\n"
|
||||||
|
|
@ -733,7 +748,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
try:
|
try:
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
except OSError as e: # noqa: BLE001
|
except OSError as e: # noqa: BLE001
|
||||||
messagebox.showwarning("Nicht gespeichert",
|
self.melden_warnung("Nicht gespeichert",
|
||||||
f"Der Stand ließ sich nicht speichern:\n{e}", parent=d)
|
f"Der Stand ließ sich nicht speichern:\n{e}", parent=d)
|
||||||
return
|
return
|
||||||
fertig["ok"] = True
|
fertig["ok"] = True
|
||||||
|
|
@ -832,7 +847,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
quelle="rechnungstool", herkunft="pdf", pdf_pfad=pdf_pfad)
|
quelle="rechnungstool", herkunft="pdf", pdf_pfad=pdf_pfad)
|
||||||
except Exception as e: # noqa: BLE001 - die PDF ist schon geschrieben
|
except Exception as e: # noqa: BLE001 - die PDF ist schon geschrieben
|
||||||
print("[nummernbuch] Eintragen fehlgeschlagen:", e)
|
print("[nummernbuch] Eintragen fehlgeschlagen:", e)
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Nummernbuch nicht erreichbar",
|
"Nummernbuch nicht erreichbar",
|
||||||
"Die Rechnung ist geschrieben, konnte aber nicht ins gemeinsame "
|
"Die Rechnung ist geschrieben, konnte aber nicht ins gemeinsame "
|
||||||
f"Nummernbuch eingetragen werden:\n{e}\n\n"
|
f"Nummernbuch eingetragen werden:\n{e}\n\n"
|
||||||
|
|
@ -1048,7 +1063,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# Im Testlauf gibt es niemanden, der klickt: dann gilt der uebliche Weg
|
# Im Testlauf gibt es niemanden, der klickt: dann gilt der uebliche Weg
|
||||||
# (Rechnung). Der Pruefstand, der GENAU DIESES Fenster pruefen will,
|
# (Rechnung). Der Pruefstand, der GENAU DIESES Fenster pruefen will,
|
||||||
# setzt _dialog_zeigen und drueckt die Knoepfe wirklich.
|
# setzt _dialog_zeigen und drueckt die Knoepfe wirklich.
|
||||||
if TESTLAUF and not getattr(self, "_dialog_zeigen", False):
|
if config.testlauf() and not getattr(self, "_dialog_zeigen", False):
|
||||||
# _testwahl: was der Pruefstand im Fenster geklickt haette.
|
# _testwahl: was der Pruefstand im Fenster geklickt haette.
|
||||||
# "rechnung" (Vorgabe), "angebot" oder None fuer Abbrechen.
|
# "rechnung" (Vorgabe), "angebot" oder None fuer Abbrechen.
|
||||||
return getattr(self, "_testwahl", "rechnung")
|
return getattr(self, "_testwahl", "rechnung")
|
||||||
|
|
@ -1174,7 +1189,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
}
|
}
|
||||||
if name not in tabelle:
|
if name not in tabelle:
|
||||||
return {"fehler": "kenne ich nicht: " + str(name), "kann": sorted(tabelle)}
|
return {"fehler": "kenne ich nicht: " + str(name), "kann": sorted(tabelle)}
|
||||||
if name in schreibt_beleg and not TESTLAUF:
|
if name in schreibt_beleg and not config.testlauf():
|
||||||
return {"fehler": "schreibt einen Beleg - geht nur im Testlauf"}
|
return {"fehler": "schreibt einen Beleg - geht nur im Testlauf"}
|
||||||
return tabelle[name](daten or {})
|
return tabelle[name](daten or {})
|
||||||
|
|
||||||
|
|
@ -1270,7 +1285,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
montage = self.ist_montage()
|
montage = self.ist_montage()
|
||||||
r = self.recalc()
|
r = self.recalc()
|
||||||
if not r.zeilen:
|
if not r.zeilen:
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Noch nichts einzutragen",
|
"Noch nichts einzutragen",
|
||||||
"Bitte die Anzahl der Monate eintragen." if montage else
|
"Bitte die Anzahl der Monate eintragen." if montage else
|
||||||
"Trage erst bei einer Leistung eine Anzahl ein - dann zeigt die "
|
"Trage erst bei einer Leistung eine Anzahl ein - dann zeigt die "
|
||||||
|
|
@ -1288,7 +1303,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
self._oeffne_datei(pfad)
|
self._oeffne_datei(pfad)
|
||||||
return pfad
|
return pfad
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
messagebox.showerror("Vorschau nicht möglich",
|
self.melden_fehler("Vorschau nicht möglich",
|
||||||
f"Die Vorschau konnte nicht geöffnet werden:\n{e}")
|
f"Die Vorschau konnte nicht geöffnet werden:\n{e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
@ -1296,7 +1311,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
montage = self.ist_montage()
|
montage = self.ist_montage()
|
||||||
r = self.recalc()
|
r = self.recalc()
|
||||||
if not r.zeilen:
|
if not r.zeilen:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Keine Positionen",
|
"Keine Positionen",
|
||||||
"Bitte die Anzahl der Monate eintragen." if montage else
|
"Bitte die Anzahl der Monate eintragen." if montage else
|
||||||
"Es wurde keine Leistung mit Anzahl > 0 eingetragen.")
|
"Es wurde keine Leistung mit Anzahl > 0 eingetragen.")
|
||||||
|
|
@ -1304,7 +1319,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
if montage and not (self.r_anreise.get().strip() and self.r_abreise.get().strip()):
|
if montage and not (self.r_anreise.get().strip() and self.r_abreise.get().strip()):
|
||||||
# Ohne Zeitraum steht auf dem Blatt nicht, wofuer gezahlt wird - und
|
# Ohne Zeitraum steht auf dem Blatt nicht, wofuer gezahlt wird - und
|
||||||
# das Steuerjournal haette keine Naechte fuer die Meldung ans Amt.
|
# das Steuerjournal haette keine Naechte fuer die Meldung ans Amt.
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Zeitraum fehlt",
|
"Zeitraum fehlt",
|
||||||
"Bitte oben rechts den Zeitraum eintragen (von / bis).\n\n"
|
"Bitte oben rechts den Zeitraum eintragen (von / bis).\n\n"
|
||||||
"Er steht auf der Rechnung und ist die Grundlage für die "
|
"Er steht auf der Rechnung und ist die Grundlage für die "
|
||||||
|
|
@ -1312,14 +1327,14 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
return
|
return
|
||||||
nummer = self.r_nummer.get().strip()
|
nummer = self.r_nummer.get().strip()
|
||||||
if not nummer:
|
if not nummer:
|
||||||
messagebox.showwarning("Rechnungsnummer fehlt", "Bitte eine Rechnungsnummer eingeben.")
|
self.melden_warnung("Rechnungsnummer fehlt", "Bitte eine Rechnungsnummer eingeben.")
|
||||||
return
|
return
|
||||||
# Den 31.02. gibt es auch dann nicht, wenn Tag und Monat einzeln moeglich
|
# Den 31.02. gibt es auch dann nicht, wenn Tag und Monat einzeln moeglich
|
||||||
# sind. Ein falsches Datum auf einer ausgestellten Rechnung laesst sich nur
|
# sind. Ein falsches Datum auf einer ausgestellten Rechnung laesst sich nur
|
||||||
# noch ueber eine Berichtigung geradeziehen - also hier abfangen.
|
# noch ueber eine Berichtigung geradeziehen - also hier abfangen.
|
||||||
kaputt = self._datumsfelder_pruefen()
|
kaputt = self._datumsfelder_pruefen()
|
||||||
if kaputt:
|
if kaputt:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Datum nicht lesbar",
|
"Datum nicht lesbar",
|
||||||
"So kann die Rechnung nicht geschrieben werden:" + chr(10) + chr(10) +
|
"So kann die Rechnung nicht geschrieben werden:" + chr(10) + chr(10) +
|
||||||
"- " + (chr(10) + "- ").join(kaputt) + chr(10) + chr(10) +
|
"- " + (chr(10) + "- ").join(kaputt) + chr(10) + chr(10) +
|
||||||
|
|
@ -1329,7 +1344,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# (§ 14 Abs. 4 Nr. 1 UStG) - ohne sie wird nichts geschrieben.
|
# (§ 14 Abs. 4 Nr. 1 UStG) - ohne sie wird nichts geschrieben.
|
||||||
maengel = self.kunde_pruefen()
|
maengel = self.kunde_pruefen()
|
||||||
if maengel:
|
if maengel:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Kundenangaben fehlen",
|
"Kundenangaben fehlen",
|
||||||
"So kann die Rechnung nicht geschrieben werden:\n\n- " + "\n- ".join(maengel) +
|
"So kann die Rechnung nicht geschrieben werden:\n\n- " + "\n- ".join(maengel) +
|
||||||
"\n\nName und vollständige Anschrift des Kunden gehören auf jede "
|
"\n\nName und vollständige Anschrift des Kunden gehören auf jede "
|
||||||
|
|
@ -1338,7 +1353,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# Hausnummer nur nachfragen, nicht blockieren - es gibt Anschriften ohne
|
# Hausnummer nur nachfragen, nicht blockieren - es gibt Anschriften ohne
|
||||||
# (Postfach, Haus am Feldweg), und eine falsche Sperre waere schlimmer.
|
# (Postfach, Haus am Feldweg), und eine falsche Sperre waere schlimmer.
|
||||||
if not ziffern(self.k_strasse.get()):
|
if not ziffern(self.k_strasse.get()):
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Hausnummer fehlt",
|
"Hausnummer fehlt",
|
||||||
f"In „{self.k_strasse.get()}“ steht keine Hausnummer.\n\n"
|
f"In „{self.k_strasse.get()}“ steht keine Hausnummer.\n\n"
|
||||||
"Trotzdem so auf die Rechnung schreiben?"):
|
"Trotzdem so auf die Rechnung schreiben?"):
|
||||||
|
|
@ -1368,7 +1383,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# geloescht und es bleibt allein die urspruengliche Rechnung stehen.
|
# geloescht und es bleibt allein die urspruengliche Rechnung stehen.
|
||||||
stand = self.storno_stand
|
stand = self.storno_stand
|
||||||
if stand:
|
if stand:
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Storno wirklich durchführen?",
|
"Storno wirklich durchführen?",
|
||||||
f"Storno {stand['nummer']} hebt die Rechnung {stand['zu']} auf, "
|
f"Storno {stand['nummer']} hebt die Rechnung {stand['zu']} auf, "
|
||||||
f"und {nummer} wird die berichtigte Rechnung.\n\n"
|
f"und {nummer} wird die berichtigte Rechnung.\n\n"
|
||||||
|
|
@ -1378,7 +1393,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
f"Nein = alles verwerfen, es bleibt allein Rechnung {stand['zu']}",
|
f"Nein = alles verwerfen, es bleibt allein Rechnung {stand['zu']}",
|
||||||
icon="warning", default="yes"):
|
icon="warning", default="yes"):
|
||||||
self._storno_verwerfen("vom Benutzer verworfen")
|
self._storno_verwerfen("vom Benutzer verworfen")
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Storno verworfen",
|
"Storno verworfen",
|
||||||
f"Der Storno wurde gelöscht. Es bleibt allein die Rechnung {stand['zu']}.")
|
f"Der Storno wurde gelöscht. Es bleibt allein die Rechnung {stand['zu']}.")
|
||||||
return
|
return
|
||||||
|
|
@ -1406,7 +1421,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
kopf["vorgang"] = ber.get("vorgang") or ber["zu"]
|
kopf["vorgang"] = ber.get("vorgang") or ber["zu"]
|
||||||
kopf["berichtigt_zu"] = ber["zu"]
|
kopf["berichtigt_zu"] = ber["zu"]
|
||||||
kopf["berichtigt_am"] = datetime.date.today().strftime("%d.%m.%Y")
|
kopf["berichtigt_am"] = datetime.date.today().strftime("%d.%m.%Y")
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Berichtigte Rechnung erstellen?",
|
"Berichtigte Rechnung erstellen?",
|
||||||
f"Zu Rechnung {ber['zu']} vom {ber['datum']} wird ein berichtigtes "
|
f"Zu Rechnung {ber['zu']} vom {ber['datum']} wird ein berichtigtes "
|
||||||
"Blatt geschrieben.\n\n"
|
"Blatt geschrieben.\n\n"
|
||||||
|
|
@ -1421,7 +1436,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
pfad = os.path.join(
|
pfad = os.path.join(
|
||||||
out_dir, f"Berichtigte Rechnung_{datetime.date.today():%Y-%m-%d}"
|
out_dir, f"Berichtigte Rechnung_{datetime.date.today():%Y-%m-%d}"
|
||||||
f"_{safe_filename(ber['zu'])}.pdf")
|
f"_{safe_filename(ber['zu'])}.pdf")
|
||||||
if os.path.exists(pfad) and not messagebox.askyesno(
|
if os.path.exists(pfad) and not self.frage(
|
||||||
"Berichtigung von heute ersetzen?",
|
"Berichtigung von heute ersetzen?",
|
||||||
f"Zu Rechnung {ber['zu']} gibt es von heute schon eine Berichtigung:\n"
|
f"Zu Rechnung {ber['zu']} gibt es von heute schon eine Berichtigung:\n"
|
||||||
f"{os.path.basename(pfad)}\n\nDurch die neue ersetzen?"):
|
f"{os.path.basename(pfad)}\n\nDurch die neue ersetzen?"):
|
||||||
|
|
@ -1429,7 +1444,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
try:
|
try:
|
||||||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||||||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||||
messagebox.showerror("Fehler beim Erstellen",
|
self.melden_fehler("Fehler beim Erstellen",
|
||||||
f"Berichtigung konnte nicht erstellt werden:\n{e}")
|
f"Berichtigung konnte nicht erstellt werden:\n{e}")
|
||||||
print("[berichtigung] FEHLER:", e)
|
print("[berichtigung] FEHLER:", e)
|
||||||
return
|
return
|
||||||
|
|
@ -1447,7 +1462,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
print("[nummernbuch] Berichtigung nicht eingetragen:", e)
|
print("[nummernbuch] Berichtigung nicht eingetragen:", e)
|
||||||
self.berichtigung_stand = None
|
self.berichtigung_stand = None
|
||||||
self._storno_anzeigen()
|
self._storno_anzeigen()
|
||||||
if messagebox.askyesno("Fertig",
|
if self.frage("Fertig",
|
||||||
f"Berichtigte Rechnung zu {ber['zu']}:\n{pfad}\n\n"
|
f"Berichtigte Rechnung zu {ber['zu']}:\n{pfad}\n\n"
|
||||||
"Jetzt öffnen (zum Drucken)?"):
|
"Jetzt öffnen (zum Drucken)?"):
|
||||||
self._oeffne_datei(pfad)
|
self._oeffne_datei(pfad)
|
||||||
|
|
@ -1468,12 +1483,12 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
liste = "- steht im gemeinsamen Nummernbuch (Steuerjournal)"
|
liste = "- steht im gemeinsamen Nummernbuch (Steuerjournal)"
|
||||||
frei = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
frei = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
||||||
if not frei:
|
if not frei:
|
||||||
messagebox.showerror(
|
self.melden_fehler(
|
||||||
"Rechnungsnummer schon vergeben",
|
"Rechnungsnummer schon vergeben",
|
||||||
f"Die Rechnungsnummer {nummer} ist schon vergeben:\n\n{liste}\n\n"
|
f"Die Rechnungsnummer {nummer} ist schon vergeben:\n\n{liste}\n\n"
|
||||||
"Es wurde keine freie Nummer gefunden - bitte die Nummer von Hand setzen.")
|
"Es wurde keine freie Nummer gefunden - bitte die Nummer von Hand setzen.")
|
||||||
return
|
return
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Rechnungsnummer schon vergeben",
|
"Rechnungsnummer schon vergeben",
|
||||||
f"Die Rechnungsnummer {nummer} ist schon vergeben:\n\n{liste}\n\n"
|
f"Die Rechnungsnummer {nummer} ist schon vergeben:\n\n{liste}\n\n"
|
||||||
"Die vorhandene Rechnung bleibt bestehen - sie darf weder überschrieben "
|
"Die vorhandene Rechnung bleibt bestehen - sie darf weder überschrieben "
|
||||||
|
|
@ -1493,7 +1508,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
try:
|
try:
|
||||||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||||||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||||
messagebox.showerror(
|
self.melden_fehler(
|
||||||
"Fehler beim Erstellen",
|
"Fehler beim Erstellen",
|
||||||
f"Angebot konnte nicht erstellt werden:\n{e}")
|
f"Angebot konnte nicht erstellt werden:\n{e}")
|
||||||
print("[angebot] FEHLER:", e)
|
print("[angebot] FEHLER:", e)
|
||||||
|
|
@ -1509,7 +1524,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
except OSError as e: # noqa: BLE001
|
except OSError as e: # noqa: BLE001
|
||||||
print("[angebot] Zaehler nicht gespeichert:", e)
|
print("[angebot] Zaehler nicht gespeichert:", e)
|
||||||
print(f"[angebot] erstellt -> {pfad}")
|
print(f"[angebot] erstellt -> {pfad}")
|
||||||
if messagebox.askyesno(
|
if self.frage(
|
||||||
"Fertig", f"Angebot erstellt:\n{pfad}\n\n"
|
"Fertig", f"Angebot erstellt:\n{pfad}\n\n"
|
||||||
"Jetzt öffnen (zum Drucken)?"):
|
"Jetzt öffnen (zum Drucken)?"):
|
||||||
self._oeffne_datei(pfad)
|
self._oeffne_datei(pfad)
|
||||||
|
|
@ -1529,7 +1544,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# Ein PDF: Seite 1 = Kunden-Rechnung, Seite 2 = Beherbergungssteuer fuers Amt
|
# Ein PDF: Seite 1 = Kunden-Rechnung, Seite 2 = Beherbergungssteuer fuers Amt
|
||||||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||||||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||||
messagebox.showerror("Fehler beim Erstellen", f"PDF konnte nicht erstellt werden:\n{e}")
|
self.melden_fehler("Fehler beim Erstellen", f"PDF konnte nicht erstellt werden:\n{e}")
|
||||||
print("[pdf] FEHLER:", e)
|
print("[pdf] FEHLER:", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1553,7 +1568,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
hinweis = (f"\n\nDazu gehört die Stornorechnung {stand['nummer']} "
|
hinweis = (f"\n\nDazu gehört die Stornorechnung {stand['nummer']} "
|
||||||
f"({os.path.basename(storno_pfad)}).\n"
|
f"({os.path.basename(storno_pfad)}).\n"
|
||||||
"Beide Blätter gehören zum Gast.") + hinweis
|
"Beide Blätter gehören zum Gast.") + hinweis
|
||||||
if messagebox.askyesno("Fertig", f"Rechnung erstellt:\n{pfad}{hinweis}\n\nJetzt öffnen (zum Drucken)?"):
|
if self.frage("Fertig", f"Rechnung erstellt:\n{pfad}{hinweis}\n\nJetzt öffnen (zum Drucken)?"):
|
||||||
self._oeffne_datei(pfad)
|
self._oeffne_datei(pfad)
|
||||||
if storno_pfad:
|
if storno_pfad:
|
||||||
self._oeffne_datei(storno_pfad)
|
self._oeffne_datei(storno_pfad)
|
||||||
|
|
@ -1578,7 +1593,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
except OSError as e: # noqa: BLE001 - die PDF ist schon geschrieben
|
except OSError as e: # noqa: BLE001 - die PDF ist schon geschrieben
|
||||||
# Ohne diese Meldung merkt niemand, dass der Zaehler stehengeblieben
|
# Ohne diese Meldung merkt niemand, dass der Zaehler stehengeblieben
|
||||||
# ist: die Ausgabe geht in eine Konsole, die es in der EXE nicht gibt.
|
# ist: die Ausgabe geht in eine Konsole, die es in der EXE nicht gibt.
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Rechnungsnummer nicht gespeichert",
|
"Rechnungsnummer nicht gespeichert",
|
||||||
f"Die Rechnung ist geschrieben, der Zählerstand aber nicht:\n{e}\n\n"
|
f"Die Rechnung ist geschrieben, der Zählerstand aber nicht:\n{e}\n\n"
|
||||||
f"Bitte im Menü unter „Rechnungsnummer…“ den Zähler auf {z} setzen, "
|
f"Bitte im Menü unter „Rechnungsnummer…“ den Zähler auf {z} setzen, "
|
||||||
|
|
@ -1588,7 +1603,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
def _beenden(self):
|
def _beenden(self):
|
||||||
"""Schliessen - mit Rückfrage, wenn ein Storno noch offen ist."""
|
"""Schliessen - mit Rückfrage, wenn ein Storno noch offen ist."""
|
||||||
if self.storno_stand:
|
if self.storno_stand:
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Storno ist noch offen",
|
"Storno ist noch offen",
|
||||||
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
||||||
f"{self.storno_stand['zu']} liegt im Ordner, die berichtigte "
|
f"{self.storno_stand['zu']} liegt im Ordner, die berichtigte "
|
||||||
|
|
@ -1606,7 +1621,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
# Ein laufender Vorgang darf nicht stumm liegenbleiben: sonst gehören die
|
# Ein laufender Vorgang darf nicht stumm liegenbleiben: sonst gehören die
|
||||||
# neu eingetippten Daten plötzlich zum Storno der fremden Rechnung.
|
# neu eingetippten Daten plötzlich zum Storno der fremden Rechnung.
|
||||||
if self.storno_stand:
|
if self.storno_stand:
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Storno verwerfen?",
|
"Storno verwerfen?",
|
||||||
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
||||||
f"{self.storno_stand['zu']} liegt noch bereit.\n\n"
|
f"{self.storno_stand['zu']} liegt noch bereit.\n\n"
|
||||||
|
|
@ -1643,7 +1658,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
Fenster offen, die er selbst wegklicken darf. Gemeldet wird es trotzdem,
|
Fenster offen, die er selbst wegklicken darf. Gemeldet wird es trotzdem,
|
||||||
damit im Protokoll steht, was zu sehen gewesen waere.
|
damit im Protokoll steht, was zu sehen gewesen waere.
|
||||||
"""
|
"""
|
||||||
if TESTLAUF:
|
if config.testlauf():
|
||||||
print(f"[oeffnen] im Pruflauf nicht geoeffnet: {pfad}")
|
print(f"[oeffnen] im Pruflauf nicht geoeffnet: {pfad}")
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
|
|
@ -1654,7 +1669,7 @@ class RechnungsApp(ThemeMixin, KorrekturMixin, EinstellungenMixin, tk.Tk):
|
||||||
else:
|
else:
|
||||||
os.system(f'xdg-open "{pfad}"')
|
os.system(f'xdg-open "{pfad}"')
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
messagebox.showinfo("Pfad", f"{pfad}\n\n(Konnte nicht automatisch geöffnet werden: {e})")
|
self.melden_info("Pfad", f"{pfad}\n\n(Konnte nicht automatisch geöffnet werden: {e})")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -1665,7 +1680,7 @@ if __name__ == "__main__":
|
||||||
if not fenster_nach_vorn(laufende_pid_lesen()):
|
if not fenster_nach_vorn(laufende_pid_lesen()):
|
||||||
_r = tk.Tk()
|
_r = tk.Tk()
|
||||||
_r.withdraw()
|
_r.withdraw()
|
||||||
messagebox.showerror(
|
self.melden_fehler(
|
||||||
"Rechnungstool läuft bereits",
|
"Rechnungstool läuft bereits",
|
||||||
"""Das Rechnungstool ist schon geöffnet, das Fenster ließ sich
|
"""Das Rechnungstool ist schon geöffnet, das Fenster ließ sich
|
||||||
aber nicht nach vorn holen - bitte über die Taskleiste zurückwechseln.
|
aber nicht nach vorn holen - bitte über die Taskleiste zurückwechseln.
|
||||||
|
|
|
||||||
15
config.py
15
config.py
|
|
@ -72,11 +72,18 @@ BASE_DIR = _daten_ordner()
|
||||||
CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
|
CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
|
||||||
|
|
||||||
# Laeuft gerade ein Pruefstand? Dann baut das Programm alles wie sonst, zeigt es
|
# Laeuft gerade ein Pruefstand? Dann baut das Programm alles wie sonst, zeigt es
|
||||||
# aber nicht: kein Fenster auf dem Bildschirm, kein Dialog, der Maus und
|
# aber nicht: kein Fenster mitten auf dem Bildschirm, kein Dialog, der Maus und
|
||||||
# Tastatur an sich reisst, waehrend der Mensch daneben arbeitet.
|
# Tastatur an sich reisst, waehrend der Mensch daneben arbeitet.
|
||||||
# Das Flag steht HIER, damit app.py, theme.py und die Pruefstaende dasselbe
|
#
|
||||||
# lesen - zwei Stellen wuerden auseinanderlaufen.
|
# ⚠️ Das ist eine FUNKTION, keine Konstante. Ein Pruefstand importiert config
|
||||||
TESTLAUF = ("--test" in sys.argv) or os.environ.get("CAMPINGHOF_TEST") == "1"
|
# haeufig, bevor er CAMPINGHOF_TEST setzt - ein einmal berechneter Wert stuende
|
||||||
|
# dann fuer den ganzen Lauf auf False, und das Programm oeffnete echte Dialoge,
|
||||||
|
# auf die niemand klickt. Genau daran hing pruef_api eine Runde lang.
|
||||||
|
def testlauf():
|
||||||
|
return ("--test" in sys.argv) or os.environ.get("CAMPINGHOF_TEST") == "1"
|
||||||
|
|
||||||
|
|
||||||
|
TESTLAUF = testlauf() # Startwert; wer spaeter fragt, nimmt testlauf()
|
||||||
|
|
||||||
# Leistungskatalog 1:1 aus der Excel-Vorlage (Rechnung Campinghof Bartl).
|
# Leistungskatalog 1:1 aus der Excel-Vorlage (Rechnung Campinghof Bartl).
|
||||||
# satz = Umsatzsteuersatz in Prozent (7 = Beherbergung, 19 = Zusatzleistung)
|
# satz = Umsatzsteuersatz in Prozent (7 = Beherbergung, 19 = Zusatzleistung)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class EinstellungenMixin:
|
||||||
if (self.cfg.get("output_dir") or "").strip():
|
if (self.cfg.get("output_dir") or "").strip():
|
||||||
return
|
return
|
||||||
vorschlag = config.default_output_dir()
|
vorschlag = config.default_output_dir()
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Speicherort der Rechnungen",
|
"Speicherort der Rechnungen",
|
||||||
"Bitte wähle einen Ordner, in dem die fertigen Rechnungen (PDF) gespeichert werden.\n\n"
|
"Bitte wähle einen Ordner, in dem die fertigen Rechnungen (PDF) gespeichert werden.\n\n"
|
||||||
f"Vorschlag: {vorschlag}\n\n(Lässt sich später unter Einstellungen ändern.)")
|
f"Vorschlag: {vorschlag}\n\n(Lässt sich später unter Einstellungen ändern.)")
|
||||||
|
|
@ -41,7 +41,7 @@ class EinstellungenMixin:
|
||||||
self.cfg["output_dir"] = d
|
self.cfg["output_dir"] = d
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
config.ensure_output_dir(self.cfg)
|
config.ensure_output_dir(self.cfg)
|
||||||
messagebox.showinfo("Speicherort", f"Rechnungen werden gespeichert in:\n{d}")
|
self.melden_info("Speicherort", f"Rechnungen werden gespeichert in:\n{d}")
|
||||||
|
|
||||||
def _info(self):
|
def _info(self):
|
||||||
d = tk.Toplevel(self)
|
d = tk.Toplevel(self)
|
||||||
|
|
@ -121,7 +121,7 @@ class EinstellungenMixin:
|
||||||
"bezeichnung": row["bez"].get().strip() or "USt",
|
"bezeichnung": row["bez"].get().strip() or "USt",
|
||||||
"extra_blatt": bool(row["extra"].get())})
|
"extra_blatt": bool(row["extra"].get())})
|
||||||
if not neu:
|
if not neu:
|
||||||
messagebox.showwarning("Leer", "Mindestens ein Steuersatz ist nötig.", parent=d)
|
self.melden_warnung("Leer", "Mindestens ein Steuersatz ist nötig.", parent=d)
|
||||||
return
|
return
|
||||||
self.cfg["steuersaetze"] = neu
|
self.cfg["steuersaetze"] = neu
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
|
|
@ -177,11 +177,11 @@ class EinstellungenMixin:
|
||||||
name = v_leistung.get().strip()
|
name = v_leistung.get().strip()
|
||||||
preis = parse_num(v_preis.get())
|
preis = parse_num(v_preis.get())
|
||||||
if not name:
|
if not name:
|
||||||
messagebox.showwarning("Bezeichnung fehlt",
|
self.melden_warnung("Bezeichnung fehlt",
|
||||||
"Bitte eine Bezeichnung eintragen.", parent=d)
|
"Bitte eine Bezeichnung eintragen.", parent=d)
|
||||||
return
|
return
|
||||||
if preis <= 0:
|
if preis <= 0:
|
||||||
messagebox.showwarning("Preis fehlt",
|
self.melden_warnung("Preis fehlt",
|
||||||
"Bitte einen Monatspreis groesser als 0 eintragen.",
|
"Bitte einen Monatspreis groesser als 0 eintragen.",
|
||||||
parent=d)
|
parent=d)
|
||||||
return
|
return
|
||||||
|
|
@ -289,7 +289,7 @@ class EinstellungenMixin:
|
||||||
"pro_person": bool(row["pro_person"].get()),
|
"pro_person": bool(row["pro_person"].get()),
|
||||||
})
|
})
|
||||||
if not neu:
|
if not neu:
|
||||||
messagebox.showwarning("Leer", "Es muss mindestens eine Leistung übrig bleiben.", parent=d)
|
self.melden_warnung("Leer", "Es muss mindestens eine Leistung übrig bleiben.", parent=d)
|
||||||
return
|
return
|
||||||
self.cfg["katalog"] = neu
|
self.cfg["katalog"] = neu
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
|
|
@ -464,7 +464,7 @@ class EinstellungenMixin:
|
||||||
jahr = self.cfg.get("rechnung_jahr")
|
jahr = self.cfg.get("rechnung_jahr")
|
||||||
zaehler = int(self.cfg.get("rechnung_zaehler", 0) or 0)
|
zaehler = int(self.cfg.get("rechnung_zaehler", 0) or 0)
|
||||||
hoch = self.hoechster_stand(jahr)
|
hoch = self.hoechster_stand(jahr)
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Rechnungsnummer",
|
"Rechnungsnummer",
|
||||||
f"Zuletzt vergeben: {jahr}-{max(zaehler, hoch):03d}\n"
|
f"Zuletzt vergeben: {jahr}-{max(zaehler, hoch):03d}\n"
|
||||||
f"Die nächste Rechnung bekommt {jahr}-{max(zaehler, hoch) + 1:03d}.\n\n"
|
f"Die nächste Rechnung bekommt {jahr}-{max(zaehler, hoch) + 1:03d}.\n\n"
|
||||||
|
|
@ -511,7 +511,7 @@ class EinstellungenMixin:
|
||||||
# (§ 14 Abs. 4 Nr. 4 UStG).
|
# (§ 14 Abs. 4 Nr. 4 UStG).
|
||||||
hoch = self.hoechster_stand(jahr)
|
hoch = self.hoechster_stand(jahr)
|
||||||
if jahr == int(self.cfg.get("rechnung_jahr", jahr) or jahr) and zaehler < hoch:
|
if jahr == int(self.cfg.get("rechnung_jahr", jahr) or jahr) and zaehler < hoch:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Nummer schon vergeben",
|
"Nummer schon vergeben",
|
||||||
f"Für {jahr} ist bereits bis {jahr}-{hoch:03d} vergeben.\n\n"
|
f"Für {jahr} ist bereits bis {jahr}-{hoch:03d} vergeben.\n\n"
|
||||||
f"Der Zähler kann nicht auf {zaehler} zurückgesetzt werden – sonst "
|
f"Der Zähler kann nicht auf {zaehler} zurückgesetzt werden – sonst "
|
||||||
|
|
|
||||||
24
korrektur.py
24
korrektur.py
|
|
@ -69,7 +69,7 @@ class KorrekturMixin:
|
||||||
self.r_datum.set(str(meta.get("datum") or self.r_datum.get()))
|
self.r_datum.set(str(meta.get("datum") or self.r_datum.get()))
|
||||||
self._storno_anzeigen()
|
self._storno_anzeigen()
|
||||||
hinweis = f"\n\nZum Kunden: {fehlend}." if fehlend else ""
|
hinweis = f"\n\nZum Kunden: {fehlend}." if fehlend else ""
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Rechnung geladen",
|
"Rechnung geladen",
|
||||||
f"Rechnung {nummer} vom {meta.get('datum','')} ist im Formular.\n\n"
|
f"Rechnung {nummer} vom {meta.get('datum','')} ist im Formular.\n\n"
|
||||||
"Jetzt die Kundenangaben richtigstellen und \"PDF erstellen\" drücken – "
|
"Jetzt die Kundenangaben richtigstellen und \"PDF erstellen\" drücken – "
|
||||||
|
|
@ -93,7 +93,7 @@ class KorrekturMixin:
|
||||||
# Abbrechen, NICHT weiterlaufen: sonst liegt das Storno noch im Ordner,
|
# Abbrechen, NICHT weiterlaufen: sonst liegt das Storno noch im Ordner,
|
||||||
# der Zähler ist zurückgedreht und die Rechnung gilt als storniert,
|
# der Zähler ist zurückgedreht und die Rechnung gilt als storniert,
|
||||||
# obwohl der Nutzer sie behalten wollte.
|
# obwohl der Nutzer sie behalten wollte.
|
||||||
messagebox.showerror(
|
self.melden_fehler(
|
||||||
"Storno konnte nicht gelöscht werden",
|
"Storno konnte nicht gelöscht werden",
|
||||||
f"Die Datei\n{stand['pfad']}\nließ sich nicht löschen:\n{e}\n\n"
|
f"Die Datei\n{stand['pfad']}\nließ sich nicht löschen:\n{e}\n\n"
|
||||||
"Ist sie noch in einem PDF-Betrachter offen? Bitte dort schließen "
|
"Ist sie noch in einem PDF-Betrachter offen? Bitte dort schließen "
|
||||||
|
|
@ -104,7 +104,7 @@ class KorrekturMixin:
|
||||||
try:
|
try:
|
||||||
config.save_config(self.cfg)
|
config.save_config(self.cfg)
|
||||||
except OSError as e: # noqa: BLE001
|
except OSError as e: # noqa: BLE001
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Zähler nicht gespeichert",
|
"Zähler nicht gespeichert",
|
||||||
f"Die Einstellungen ließen sich nicht schreiben:\n{e}\n\n"
|
f"Die Einstellungen ließen sich nicht schreiben:\n{e}\n\n"
|
||||||
"Bitte im Menü unter „Rechnungsnummer…“ prüfen.")
|
"Bitte im Menü unter „Rechnungsnummer…“ prüfen.")
|
||||||
|
|
@ -137,14 +137,14 @@ class KorrekturMixin:
|
||||||
"""
|
"""
|
||||||
storno = modus == "storno"
|
storno = modus == "storno"
|
||||||
if self.storno_stand:
|
if self.storno_stand:
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Storno liegt schon bereit",
|
"Storno liegt schon bereit",
|
||||||
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
f"Der Storno {self.storno_stand['nummer']} zu Rechnung "
|
||||||
f"{self.storno_stand['zu']} ist noch offen.\n\n"
|
f"{self.storno_stand['zu']} ist noch offen.\n\n"
|
||||||
"Erst die berichtigte Rechnung erstellen oder den Storno dort verwerfen.")
|
"Erst die berichtigte Rechnung erstellen oder den Storno dort verwerfen.")
|
||||||
return
|
return
|
||||||
if self.berichtigung_stand:
|
if self.berichtigung_stand:
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Berichtigung läuft schon",
|
"Berichtigung läuft schon",
|
||||||
f"Die Rechnung {self.berichtigung_stand['zu']} liegt gerade zum Berichtigen "
|
f"Die Rechnung {self.berichtigung_stand['zu']} liegt gerade zum Berichtigen "
|
||||||
"im Formular.\n\nErst „PDF erstellen“ drücken oder das Formular zurücksetzen.")
|
"im Formular.\n\nErst „PDF erstellen“ drücken oder das Formular zurücksetzen.")
|
||||||
|
|
@ -162,7 +162,7 @@ class KorrekturMixin:
|
||||||
if d.get("art") not in ("storno", "berichtigung")
|
if d.get("art") not in ("storno", "berichtigung")
|
||||||
and nummer_normalisieren(d.get("rechnungsnummer")) not in stornierte]
|
and nummer_normalisieren(d.get("rechnungsnummer")) not in stornierte]
|
||||||
if not offen:
|
if not offen:
|
||||||
messagebox.showwarning(
|
self.melden_warnung(
|
||||||
"Nichts zu korrigieren",
|
"Nichts zu korrigieren",
|
||||||
f"Im Ordner\n{out_dir}\nliegt keine Rechnung, die dafür in Frage kommt.")
|
f"Im Ordner\n{out_dir}\nliegt keine Rechnung, die dafür in Frage kommt.")
|
||||||
return
|
return
|
||||||
|
|
@ -472,7 +472,7 @@ class KorrekturMixin:
|
||||||
stand.configure(text="Bitte die Nummer der bleibenden Rechnung eintragen.",
|
stand.configure(text="Bitte die Nummer der bleibenden Rechnung eintragen.",
|
||||||
fg="#a00000")
|
fg="#a00000")
|
||||||
return
|
return
|
||||||
if gefunden["leicht"] and not messagebox.askyesno(
|
if gefunden["leicht"] and not self.frage(
|
||||||
"Weicht ab – trotzdem?",
|
"Weicht ab – trotzdem?",
|
||||||
f"Rechnung {nr} weicht ab: " + "; ".join(gefunden["leicht"]) +
|
f"Rechnung {nr} weicht ab: " + "; ".join(gefunden["leicht"]) +
|
||||||
". Das Storno würde trotzdem darauf verweisen. Ist das dieselbe "
|
". Das Storno würde trotzdem darauf verweisen. Ist das dieselbe "
|
||||||
|
|
@ -537,7 +537,7 @@ class KorrekturMixin:
|
||||||
|
|
||||||
nummer = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
nummer = naechste_freie_nummer(out_dir, self.cfg, self._belegte_nummern())
|
||||||
if not nummer:
|
if not nummer:
|
||||||
messagebox.showerror("Keine freie Nummer",
|
self.melden_fehler("Keine freie Nummer",
|
||||||
"Es wurde keine freie Rechnungsnummer gefunden.")
|
"Es wurde keine freie Rechnungsnummer gefunden.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -572,7 +572,7 @@ class KorrekturMixin:
|
||||||
# kann das Programm nichts erfinden; dann muss der Nutzer entscheiden.
|
# kann das Programm nichts erfinden; dann muss der Nutzer entscheiden.
|
||||||
maengel = kunde_maengel(kunde)
|
maengel = kunde_maengel(kunde)
|
||||||
if maengel:
|
if maengel:
|
||||||
if not messagebox.askyesno(
|
if not self.frage(
|
||||||
"Angaben zum Kunden unvollständig",
|
"Angaben zum Kunden unvollständig",
|
||||||
f"In Rechnung {alt_nummer} steht nicht alles:\n\n- "
|
f"In Rechnung {alt_nummer} steht nicht alles:\n\n- "
|
||||||
+ "\n- ".join(maengel) +
|
+ "\n- ".join(maengel) +
|
||||||
|
|
@ -620,7 +620,7 @@ class KorrekturMixin:
|
||||||
try:
|
try:
|
||||||
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
pdf_renderer.render_rechnung(pfad, self.cfg, r, kopf)
|
||||||
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
except Exception as e: # noqa: BLE001 - dem Nutzer den Fehler zeigen
|
||||||
messagebox.showerror("Fehler beim Erstellen",
|
self.melden_fehler("Fehler beim Erstellen",
|
||||||
f"Storno-PDF konnte nicht erstellt werden:\n{e}")
|
f"Storno-PDF konnte nicht erstellt werden:\n{e}")
|
||||||
print("[storno] FEHLER:", e)
|
print("[storno] FEHLER:", e)
|
||||||
return
|
return
|
||||||
|
|
@ -646,7 +646,7 @@ class KorrekturMixin:
|
||||||
if art_folge == "vorhanden":
|
if art_folge == "vorhanden":
|
||||||
self._storno_anzeigen()
|
self._storno_anzeigen()
|
||||||
self._neue_nummer_vorschlagen()
|
self._neue_nummer_vorschlagen()
|
||||||
if messagebox.askyesno(
|
if self.frage(
|
||||||
"Storno erstellt",
|
"Storno erstellt",
|
||||||
f"Storno {nummer} hebt Rechnung {alt_nummer} auf.\n\n"
|
f"Storno {nummer} hebt Rechnung {alt_nummer} auf.\n\n"
|
||||||
f"Die Leistung bleibt mit Rechnung {folge_nummer} abgerechnet – es "
|
f"Die Leistung bleibt mit Rechnung {folge_nummer} abgerechnet – es "
|
||||||
|
|
@ -663,7 +663,7 @@ class KorrekturMixin:
|
||||||
if fehlend:
|
if fehlend:
|
||||||
hinweis = (f"\n\nZum Kunden: {fehlend}.\nBitte oben prüfen und ergänzen – "
|
hinweis = (f"\n\nZum Kunden: {fehlend}.\nBitte oben prüfen und ergänzen – "
|
||||||
"ohne vollständige Anschrift wird die Rechnung nicht geschrieben.")
|
"ohne vollständige Anschrift wird die Rechnung nicht geschrieben.")
|
||||||
messagebox.showinfo(
|
self.melden_info(
|
||||||
"Storno erstellt",
|
"Storno erstellt",
|
||||||
f"Stornorechnung {nummer} zu Rechnung {alt_nummer} liegt hier:\n{pfad}\n\n"
|
f"Stornorechnung {nummer} zu Rechnung {alt_nummer} liegt hier:\n{pfad}\n\n"
|
||||||
f"Jetzt die berichtigte Rechnung {self.r_nummer.get()} anpassen und "
|
f"Jetzt die berichtigte Rechnung {self.r_nummer.get()} anpassen und "
|
||||||
|
|
|
||||||
|
|
@ -165,21 +165,33 @@ class OrdnerWahl(tk.Toplevel):
|
||||||
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
|
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
|
||||||
"anderen Programm denselben Ordner.", fg=self._muted)
|
"anderen Programm denselben Ordner.", fg=self._muted)
|
||||||
|
|
||||||
|
def _melden(self, titel, text):
|
||||||
|
"""Meldung im Theme des Hauptfensters - sonst wie bisher."""
|
||||||
|
eltern = self.master
|
||||||
|
if hasattr(eltern, "melden_warnung"):
|
||||||
|
return eltern.melden_warnung(titel, text)
|
||||||
|
return messagebox.showwarning(titel, text, parent=self)
|
||||||
|
|
||||||
|
def _frage(self, titel, text):
|
||||||
|
eltern = self.master
|
||||||
|
if hasattr(eltern, "frage"):
|
||||||
|
return eltern.frage(titel, text)
|
||||||
|
return messagebox.askyesno(titel, text, parent=self)
|
||||||
|
|
||||||
|
|
||||||
def uebernehmen(self):
|
def uebernehmen(self):
|
||||||
ordner = self.pfad.get().strip()
|
ordner = self.pfad.get().strip()
|
||||||
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
taugt, grund = gemeinsam.ordner_taugt(ordner)
|
||||||
if not taugt:
|
if not taugt:
|
||||||
messagebox.showwarning("Ordner geht nicht", grund or "Bitte einen Ordner wählen.",
|
self._melden("Ordner geht nicht", grund or "Bitte einen Ordner wählen.")
|
||||||
parent=self)
|
|
||||||
return
|
return
|
||||||
if not messagebox.askyesno(
|
if not self._frage(
|
||||||
"Sind Sie sicher?",
|
"Sind Sie sicher?",
|
||||||
f"Gemeinsamer Ordner:\n\n{ordner}\n\n"
|
f"Gemeinsamer Ordner:\n\n{ordner}\n\n"
|
||||||
"Steht im anderen Programm derselbe Ordner? Nur dann kennen beide "
|
"Steht im anderen Programm derselbe Ordner? Nur dann kennen beide "
|
||||||
"dieselben Rechnungsnummern.\n\n"
|
"dieselben Rechnungsnummern.\n\n"
|
||||||
"Ja = Ordner übernehmen\n"
|
"Ja = Ordner übernehmen\n"
|
||||||
"Nein = zurück zur Auswahl",
|
"Nein = zurück zur Auswahl"):
|
||||||
icon="question", default="yes", parent=self):
|
|
||||||
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
|
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
|
||||||
self.feld.focus_set()
|
self.feld.focus_set()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -146,14 +146,16 @@ antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
||||||
pdfs = [n for n in os.listdir(ordner) if n.lower().endswith(".pdf")]
|
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})")
|
pruefe(len(pdfs) == 1, f"im Testlauf schreibt die API die Rechnung ({pdfs})")
|
||||||
|
|
||||||
# Und im ECHTEN Betrieb nicht: dieselbe Aktion, aber ohne Testlauf-Flag.
|
# Und im ECHTEN Betrieb nicht: dieselbe Aktion, ohne den Pruefschalter.
|
||||||
A.TESTLAUF = False
|
# Umgeschaltet wird die Umgebungsvariable selbst - das Programm fragt sie bei
|
||||||
|
# jeder Aktion neu ab, genau wie beim echten Start.
|
||||||
|
os.environ.pop("CAMPINGHOF_TEST", None)
|
||||||
antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
antwort = anrufen_und_drehen(name="rechnung", schluessel=schluessel)
|
||||||
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
pruefe(isinstance(antwort, dict) and "nur im Testlauf" in str(antwort.get("fehler", "")),
|
pruefe(isinstance(antwort, dict) and "nur im Testlauf" in str(antwort.get("fehler", "")),
|
||||||
f"im echten Betrieb verweigert sie den Beleg ({antwort})")
|
f"im echten Betrieb verweigert sie den Beleg ({antwort})")
|
||||||
pruefe(len([n for n in os.listdir(ordner) if n.lower().endswith(".pdf")]) == 1,
|
pruefe(len([n for n in os.listdir(ordner) if n.lower().endswith(".pdf")]) == 1,
|
||||||
"und es kam keine zweite Rechnung dazu")
|
"und es kam keine zweite Rechnung dazu")
|
||||||
A.TESTLAUF = True
|
|
||||||
|
|
||||||
# Unbekannter Name sagt, was es gibt - statt still nichts zu tun.
|
# Unbekannter Name sagt, was es gibt - statt still nichts zu tun.
|
||||||
antwort = anrufen_und_drehen(name="tanz", schluessel=schluessel)
|
antwort = anrufen_und_drehen(name="tanz", schluessel=schluessel)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,15 @@ def knoepfe_von(w, aus=None):
|
||||||
return aus
|
return aus
|
||||||
|
|
||||||
|
|
||||||
|
def _alle_widgets(w, aus=None):
|
||||||
|
"""Jedes Widget unterhalb von w - egal wie tief."""
|
||||||
|
aus = [] if aus is None else aus
|
||||||
|
for k in w.winfo_children():
|
||||||
|
aus.append(k)
|
||||||
|
_alle_widgets(k, aus)
|
||||||
|
return aus
|
||||||
|
|
||||||
|
|
||||||
def felder_von(w, aus=None):
|
def felder_von(w, aus=None):
|
||||||
aus = [] if aus is None else aus
|
aus = [] if aus is None else aus
|
||||||
for k in w.winfo_children():
|
for k in w.winfo_children():
|
||||||
|
|
@ -208,7 +217,9 @@ pruefe(f.cfg.get("rechnung_zaehler") == 91,
|
||||||
f.cfg["nummer_eingerichtet"] = False
|
f.cfg["nummer_eingerichtet"] = False
|
||||||
f.cfg["rechnung_jahr"] = JAHR
|
f.cfg["rechnung_jahr"] = JAHR
|
||||||
config.save_config(f.cfg)
|
config.save_config(f.cfg)
|
||||||
A.TESTLAUF = False # den Erststart-Dialog wirklich zeigen
|
# Diesen einen Dialog will der Pruefstand wirklich sehen und bedienen. Alles
|
||||||
|
# andere bleibt im Pruflauf - sonst wartet der naechste Hinweis auf einen Klick.
|
||||||
|
f._erststart_zeigen = True
|
||||||
|
|
||||||
|
|
||||||
def erststart(zahl, erwartet_ja=True):
|
def erststart(zahl, erwartet_ja=True):
|
||||||
|
|
@ -227,6 +238,14 @@ def erststart(zahl, erwartet_ja=True):
|
||||||
top = tops[-1]
|
top = tops[-1]
|
||||||
felder = felder_von(top)
|
felder = felder_von(top)
|
||||||
if felder:
|
if felder:
|
||||||
|
# Der Dialog fragt seit 05.09.2026 zuerst nach dem WEG: bei 0
|
||||||
|
# anfangen oder die eigene Zaehlung weiterfuehren. Ohne diesen
|
||||||
|
# Klick bleibt das Zahlenfeld gesperrt - hier wird also dasselbe
|
||||||
|
# gedrueckt wie beim Benutzer, nicht daran vorbei.
|
||||||
|
for w in _alle_widgets(top):
|
||||||
|
if w.winfo_class() == "TRadiobutton" and "weiterführen" in str(w.cget("text")):
|
||||||
|
w.invoke()
|
||||||
|
break
|
||||||
felder[0].delete(0, "end")
|
felder[0].delete(0, "end")
|
||||||
felder[0].insert(0, str(zahl))
|
felder[0].insert(0, str(zahl))
|
||||||
for k in knoepfe_von(top):
|
for k in knoepfe_von(top):
|
||||||
|
|
|
||||||
106
theme.py
106
theme.py
|
|
@ -8,6 +8,7 @@ baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font as tkfont
|
from tkinter import font as tkfont
|
||||||
|
from tkinter import messagebox
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
@ -264,11 +265,15 @@ class ThemeMixin:
|
||||||
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
|
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
|
||||||
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
|
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
|
||||||
tabmargins=(2, 4, 2, 0), borderwidth=0)
|
tabmargins=(2, 4, 2, 0), borderwidth=0)
|
||||||
style.configure("TNotebook.Tab", background=self.col_field,
|
# Der OFFENE Reiter gehoert zur Kachel darunter und ist deshalb heller
|
||||||
|
# als die ruhenden. Vorher war es andersherum: die ruhenden lagen auf der
|
||||||
|
# helleren Flaeche und sahen aus, als waeren sie ausgewaehlt.
|
||||||
|
style.configure("TNotebook.Tab", background=bg,
|
||||||
foreground=self.col_muted, bordercolor=self.col_line,
|
foreground=self.col_muted, bordercolor=self.col_line,
|
||||||
padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=0)
|
padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=0)
|
||||||
style.map("TNotebook.Tab",
|
style.map("TNotebook.Tab",
|
||||||
background=[("selected", self.col_card), ("active", sel)],
|
background=[("selected", self.col_card),
|
||||||
|
("active", mischen(accent, bg, 0.12))],
|
||||||
foreground=[("selected", accent), ("active", fg)],
|
foreground=[("selected", accent), ("active", fg)],
|
||||||
font=[("selected", (SCHRIFT_NAME, 10, "bold"))])
|
font=[("selected", (SCHRIFT_NAME, 10, "bold"))])
|
||||||
# Kein expand beim ausgewaehlten Reiter: er wuchs dadurch um ein paar
|
# Kein expand beim ausgewaehlten Reiter: er wuchs dadurch um ein paar
|
||||||
|
|
@ -286,12 +291,16 @@ class ThemeMixin:
|
||||||
und Tastatur fest, waehrend nebenan jemand schreibt.
|
und Tastatur fest, waehrend nebenan jemand schreibt.
|
||||||
"""
|
"""
|
||||||
d.configure(bg=self.col_bg)
|
d.configure(bg=self.col_bg)
|
||||||
if config.TESTLAUF:
|
if config.testlauf():
|
||||||
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
|
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
|
||||||
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
|
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
|
||||||
# Menschen, der nebenher schreibt.
|
# Menschen, der nebenher schreibt.
|
||||||
d.geometry(f"+{d.winfo_screenwidth() + 20}+40")
|
d.geometry(f"+{d.winfo_screenwidth() + 20}+40")
|
||||||
d.grab_set = lambda *a, **k: None
|
d.grab_set = lambda *a, **k: None
|
||||||
|
try:
|
||||||
|
d.attributes("-toolwindow", True) # nicht in die Taskleiste
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
|
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
|
||||||
d.after(20, lambda: self._dark_titlebar(d))
|
d.after(20, lambda: self._dark_titlebar(d))
|
||||||
|
|
||||||
|
|
@ -312,3 +321,94 @@ class ThemeMixin:
|
||||||
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
|
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print("[theme] Titelleiste:", e)
|
print("[theme] Titelleiste:", e)
|
||||||
|
|
||||||
|
|
||||||
|
class MeldungMixin:
|
||||||
|
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.
|
||||||
|
|
||||||
|
Warum nicht `messagebox`: das sind Fenster von Windows selbst. Sie sind
|
||||||
|
immer hell, egal was das Programm einstellt - mitten in einer dunklen
|
||||||
|
Oberflaeche blendet das und sieht nach Fremdkoerper aus. Dieselbe Erfahrung
|
||||||
|
steckt schon in der eigenen Menueleiste weiter oben.
|
||||||
|
|
||||||
|
⚠️ Im PRUEFLAUF geht beides weiter ueber `messagebox`. Die Pruefstaende
|
||||||
|
haengen ihre Antworten dort ein ("auf diese Frage antworte Ja") und lesen
|
||||||
|
dort mit, was das Programm gemeldet hat. Wuerde hier ein eigenes Fenster
|
||||||
|
aufgehen, warteten sie auf einen Klick, den niemand macht. Geprueft wird
|
||||||
|
damit derselbe Weg - nur die Anzeige unterscheidet sich.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Die drei Sorten Meldung. `parent=`, `icon=` und `default=` kommen aus den
|
||||||
|
# frueheren messagebox-Aufrufen mit - sie werden geschluckt, damit an den
|
||||||
|
# Aufrufstellen nichts umgeschrieben werden muss.
|
||||||
|
def melden_info(self, titel, text="", **_kw):
|
||||||
|
return self.melden(titel, text, "info")
|
||||||
|
|
||||||
|
def melden_warnung(self, titel, text="", **_kw):
|
||||||
|
return self.melden(titel, text, "warnung")
|
||||||
|
|
||||||
|
def melden_fehler(self, titel, text="", **_kw):
|
||||||
|
return self.melden(titel, text, "fehler")
|
||||||
|
|
||||||
|
def melden(self, titel, text, art="warnung"):
|
||||||
|
"""Eine Meldung mit einem OK-Knopf."""
|
||||||
|
if config.testlauf():
|
||||||
|
{"info": messagebox.showinfo,
|
||||||
|
"warnung": messagebox.showwarning,
|
||||||
|
"fehler": messagebox.showerror}.get(art, messagebox.showwarning)(titel, text)
|
||||||
|
return None
|
||||||
|
return self._meldungsfenster(titel, text, art, knoepfe=None)
|
||||||
|
|
||||||
|
def frage(self, titel, text="", ja="Ja", nein="Nein", vorgabe=True, **_kw):
|
||||||
|
"""Ja/Nein. Gibt True oder False zurueck - wie askyesno."""
|
||||||
|
if config.testlauf():
|
||||||
|
return bool(messagebox.askyesno(titel, text))
|
||||||
|
return bool(self._meldungsfenster(titel, text, "frage",
|
||||||
|
knoepfe=(ja, nein), vorgabe=vorgabe))
|
||||||
|
|
||||||
|
def _meldungsfenster(self, titel, text, art, knoepfe=None, vorgabe=True):
|
||||||
|
farbe = {"fehler": self.col_gefahr, "warnung": self.col_gefahr,
|
||||||
|
"frage": self.col_accent}.get(art, self.col_accent)
|
||||||
|
d = tk.Toplevel(self)
|
||||||
|
d.title(titel)
|
||||||
|
d.transient(self)
|
||||||
|
d.resizable(False, False)
|
||||||
|
self._style_dialog(d)
|
||||||
|
antwort = {"wert": False}
|
||||||
|
|
||||||
|
rahmen = ttk.Frame(d, padding=20)
|
||||||
|
rahmen.pack(fill="both", expand=True)
|
||||||
|
tk.Label(rahmen, text=titel, bg=self.col_bg, fg=farbe, justify="left",
|
||||||
|
font=(SCHRIFT_NAME, 12, "bold")).pack(anchor="w")
|
||||||
|
tk.Label(rahmen, text=text, bg=self.col_bg, fg=self.col_fg, justify="left",
|
||||||
|
wraplength=460, font=(SCHRIFT_NAME, 10)).pack(anchor="w", pady=(10, 0))
|
||||||
|
|
||||||
|
leiste = ttk.Frame(rahmen)
|
||||||
|
leiste.pack(fill="x", pady=(20, 0))
|
||||||
|
|
||||||
|
def schliessen(wert):
|
||||||
|
antwort["wert"] = wert
|
||||||
|
d.destroy()
|
||||||
|
|
||||||
|
if knoepfe:
|
||||||
|
ja, nein = knoepfe
|
||||||
|
ttk.Button(leiste, text=ja, style="Haupt.TButton",
|
||||||
|
command=lambda: schliessen(True)).pack(side="right")
|
||||||
|
ttk.Button(leiste, text=nein,
|
||||||
|
command=lambda: schliessen(False)).pack(side="right", padx=(0, 8))
|
||||||
|
d.bind("<Return>", lambda _e: schliessen(vorgabe))
|
||||||
|
else:
|
||||||
|
ttk.Button(leiste, text="OK", style="Haupt.TButton",
|
||||||
|
command=lambda: schliessen(True)).pack(side="right")
|
||||||
|
d.bind("<Return>", lambda _e: schliessen(True))
|
||||||
|
d.bind("<Escape>", lambda _e: schliessen(False))
|
||||||
|
d.protocol("WM_DELETE_WINDOW", lambda: schliessen(False))
|
||||||
|
|
||||||
|
d.update_idletasks()
|
||||||
|
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
|
||||||
|
y = self.winfo_rooty() + 140
|
||||||
|
d.geometry(f"+{max(0, x)}+{max(0, y)}")
|
||||||
|
d.grab_set()
|
||||||
|
d.focus_set()
|
||||||
|
self.wait_window(d)
|
||||||
|
return antwort["wert"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue