Win95-Titelleiste und -Menüleiste, Einrichtungs-Assistent, Rundgang, Tipp des Tages

- selbst gezeichneter Fensterrahmen (verschieben, maximieren, minimieren, Größe ändern), abschaltbar
- Dialoge mit Win95-Titelleiste; vorherige Modalsperre wird nach Rückfragen wiederhergestellt
- Assistent: Speicherort, Unterhändler (Nummern aus Abrechnung vorschlagen), erste Abrechnung
- Rundgang mit Hinweis-Sprechblasen, Tipp des Tages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-11-02 10:14:19 +01:00
1 parent a837ddc4d4
commit 4cfe0e6c47
7 files changed
+1128 -43

No files matched your search

+119 -40
View File
@@ -15,7 +15,9 @@ from tkinter import filedialog
from provtool import APP_NAME, config
from provtool import export as exp
from provtool.db import UNZUGEORDNET, Datenbank
from provtool.gui import assistent as ass
from provtool.gui import dialogs as dlg
from provtool.gui import rahmen as rh
from provtool.gui import win95 as w
from provtool.importer import ANBIETER, ImportFehler, lies_datei
from provtool.money import fmt, fmt_percent, parse_cent
@@ -67,30 +69,40 @@ class App:
self.icons = w.Icons(root)
root._app_icon = self.icons("app", 2) # type: ignore[attr-defined]
root.iconphoto(True, self.icons("app", 2), self.icons("app", 1))
root.title(APP_NAME)
root.geometry(self.settings.get("fenster", "1200x720"))
root.minsize(900, 520)
root.protocol("WM_DELETE_WINDOW", self.beenden)
root.report_callback_exception = self._fehler_handler
klassisch = bool(self.settings.get("klassischer_rahmen", True))
w.KLASSISCH = klassisch
self.rahmen = rh.Hauptrahmen(root, APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch)
self.rahmen.set_titel(APP_NAME)
self.inhalt = self.rahmen.inhalt
self._menue()
self._toolbar()
self._body()
self.status = w.Statusleiste(root, [("info", 0), ("pos", 16), ("orig", 22), ("ausz", 22)])
self.status = w.Statusleiste(self.inhalt, [("info", 0), ("pos", 16), ("orig", 22), ("ausz", 22)])
griff = self.rahmen.griff(self.status)
if griff:
griff.pack(side="right", anchor="se")
self.status.pack(side="bottom", fill="x")
self._tasten()
self.rahmen.geometrie_setzen(self.settings.get("fenster"), self.settings.get("maximiert", False))
abr = self.db.abrechnungen()
if abr:
self.oeffnen(abr[0]["id"])
else:
self._aktualisieren()
root.after(300, self._willkommen)
if not self.settings.get("assistent_erledigt") and not self.db.haendler_liste():
root.after(400, self.assistent)
elif self.settings.get("tipps_beim_start", True):
root.after(500, self.tipp_des_tages)
# ================================================================= Aufbau
def _menue(self):
m = tk.Menu(self.root, tearoff=0)
d = tk.Menu(m, tearoff=0)
self.menueleiste = rh.Menueleiste(self.inhalt, self.root)
self.menueleiste.pack(side="top", fill="x")
d = self.menueleiste.menue("Datei")
d.add_command(label="Neue Abrechnung…", accelerator="Strg+N", command=self.neu)
d.add_command(label="Abrechnung öffnen…", accelerator="Strg+O", command=self.abrechnung_waehlen)
d.add_separator()
@@ -103,32 +115,32 @@ class App:
d.add_command(label="Einstellungen…", command=self.einstellungen)
d.add_separator()
d.add_command(label="Beenden", accelerator="Alt+F4", command=self.beenden)
m.add_cascade(label="Datei", menu=d, underline=0)
b = tk.Menu(m, tearoff=0)
b = self.menueleiste.menue("Bearbeiten")
b.add_command(label="Betrag ändern…", accelerator="F2", command=self.betrag_bearbeiten)
b.add_command(label="Auszahlung in Prozent…", accelerator="F4", command=self.prozent)
b.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen)
b.add_separator()
b.add_command(label="Alle Beträge neu berechnen", command=self.neu_berechnen)
m.add_cascade(label="Bearbeiten", menu=b, underline=0)
h = tk.Menu(m, tearoff=0)
h = self.menueleiste.menue("Händler")
h.add_command(label="Händler verwalten…", accelerator="Strg+H", command=self.haendler)
h.add_command(label="Kennung des Eintrags zuordnen…", command=self.kennung_zuordnen_auswahl)
h.add_command(label="Auswahl fest einem Händler zuweisen…", command=self.haendler_zuweisen)
h.add_command(label="Auswahl automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True))
m.add_cascade(label="Händler", menu=h, underline=0)
hi = tk.Menu(m, tearoff=0)
hi = self.menueleiste.menue("?", underline=None)
hi.add_command(label="Kurzanleitung", accelerator="F1", command=self.hilfe)
hi.add_command(label="Rundgang durch das Programm", command=self.rundgang)
hi.add_command(label="Tipp des Tages…", command=self.tipp_des_tages)
hi.add_separator()
hi.add_command(label="Einrichtungs-Assistent…", command=self.assistent)
hi.add_separator()
hi.add_command(label=f"Info über {APP_NAME}…", command=lambda: dlg.InfoDialog(self.root).zeigen())
m.add_cascade(label="?", menu=hi)
self.root.configure(menu=m)
def _toolbar(self):
tb = tk.Frame(self.root, bg=w.FACE, bd=1, relief="raised")
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=1, relief="raised")
self.tb: dict[str, w.ToolbarButton] = {}
tb.pack(side="top", fill="x")
for text, icon, cmd, tip in (
("Neu", "neu", self.neu, "Neue Abrechnung anlegen (Strg+N)"),
@@ -144,7 +156,8 @@ class App:
if text is None:
w.trenner(tb).pack(side="left", fill="y", padx=4, pady=3)
continue
w.ToolbarButton(tb, text, self.icons(icon), cmd, tip).pack(side="left", padx=1, pady=1)
self.tb[text] = w.ToolbarButton(tb, text, self.icons(icon), cmd, tip)
self.tb[text].pack(side="left", padx=1, pady=1)
self.v_suche = tk.StringVar()
sf = tk.Frame(tb, bg=w.FACE)
sf.pack(side="right", padx=8)
@@ -153,9 +166,10 @@ class App:
e.pack(side="left", padx=4)
self.v_suche.trace_add("write", lambda *_: self._tabelle())
self._suchfeld = e
self._suchrahmen = sf
def _body(self):
desk = tk.Frame(self.root, bg=w.TEAL)
desk = tk.Frame(self.inhalt, bg=w.TEAL)
desk.pack(side="top", fill="both", expand=True)
pw = tk.PanedWindow(desk, orient="horizontal", bg=w.TEAL, sashwidth=6, bd=0)
pw.pack(fill="both", expand=True, padx=6, pady=6)
@@ -202,7 +216,8 @@ class App:
leiste = tk.Frame(self.f_p.inhalt, bg=w.FACE)
leiste.pack(fill="x", pady=(4, 0))
w.knopf(leiste, "Prozent…", self.prozent, self.icons("prozent")).pack(side="left")
self.b_prozent = w.knopf(leiste, "Prozent…", self.prozent, self.icons("prozent"))
self.b_prozent.pack(side="left")
w.knopf(leiste, "Betrag ändern…", self.betrag_bearbeiten, self.icons("rechner")).pack(side="left", padx=4)
w.knopf(leiste, "Zurücksetzen", self.zuruecksetzen).pack(side="left")
self.v_alle_auf = tk.BooleanVar(value=True)
@@ -235,12 +250,12 @@ class App:
if self.aid and self.db.abrechnung(self.aid):
self.positionen = self.db.positionen(self.aid)
titel = self.db.abrechnung(self.aid)["titel"]
self.root.title(f"{APP_NAME} - [{titel}]")
self.rahmen.set_titel(f"{APP_NAME} - [{titel}]")
self.status.set("info", f" Abrechnung: {titel}")
else:
self.aid = None
self.positionen = []
self.root.title(APP_NAME)
self.rahmen.set_titel(APP_NAME)
self.status.set("info", " Keine Abrechnung geöffnet – Datei ▸ Neue Abrechnung oder Import")
self._haendlerliste(behalte_auswahl)
self._tabelle()
@@ -746,22 +761,32 @@ class App:
self.ansicht = ALLE
self._aktualisieren()
def db_wechseln(self, pfad: str | None):
"""Öffnet (oder legt an) eine andere Datenbank. pfad=None: Standard-Speicherort."""
ziel = Path(pfad) if pfad else config.app_dir() / "provisionen.db"
db = Datenbank(ziel) # wirft bei Fehler – alte DB bleibt dann offen
self.db.close()
self.db = db
self.settings["db_pfad"] = str(ziel) if pfad else None
config.save(self.settings)
abr = self.db.abrechnungen()
self.aid = abr[0]["id"] if abr else None
self.ansicht = ALLE
self._aktualisieren()
def einstellungen(self):
neu = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen()
if neu and Path(neu) != self.db.path:
res = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen()
if not res:
return
if res["klassisch"] != bool(self.settings.get("klassischer_rahmen", True)):
self.settings["klassischer_rahmen"] = res["klassisch"]
config.save(self.settings)
w.info(self.root, "Einstellungen", "Die Änderung der Titelleiste wird beim nächsten Programmstart wirksam.")
if res["pfad"] and Path(res["pfad"]) != self.db.path:
try:
db = Datenbank(neu)
self.db_wechseln(res["pfad"])
except Exception as exc:
w.fehler(self.root, "Einstellungen", f"Datenbank konnte nicht geöffnet werden:\n{exc}")
return
self.db.close()
self.db = db
self.settings["db_pfad"] = neu
config.save(self.settings)
abr = self.db.abrechnungen()
self.aid = abr[0]["id"] if abr else None
self.ansicht = ALLE
self._aktualisieren()
def hilfe(self):
w.info(self.root, "Kurzanleitung",
@@ -779,12 +804,65 @@ class App:
"5. Exportieren (Strg+E): je Händler eine Excel-Datei im Abrechnungsformat.\n\n"
"Alles wird automatisch in der Datenbank gespeichert.")
def _willkommen(self):
if not self.db.haendler_liste():
if w.frage(self.root, "Willkommen",
f"Willkommen beim {APP_NAME}!\n\nEs sind noch keine Händler angelegt.\n"
"Jetzt die Unterhändler mit ihren Kennungen anlegen?"):
self.haendler()
def assistent(self):
res = ass.Einrichtungsassistent(self).zeigen()
self.settings["assistent_erledigt"] = True
config.save(self.settings)
self._aktualisieren()
if not res:
return
if res["titel"]:
self.aid = self.db.abrechnung_neu(res["titel"])
self.ansicht = ALLE
self._aktualisieren()
if res["dateien"]:
self.importieren(res["dateien"])
if res["rundgang"]:
self.root.after(200, self.rundgang)
def rundgang(self):
tb = self.tb
schritte = [
(self.rahmen.titelleiste or self.menueleiste, "Willkommen zum Rundgang!",
"In ein paar Schritten sehen Sie, wo Sie was finden.\n\nWeiter geht es mit „Weiter >“ oder der "
"Eingabetaste, beenden können Sie jederzeit mit Esc."),
(tb["Händler"], "Händler",
"Hier legen Sie Ihre Unterhändler an: Name, Auszahlung in % und woran ihre Positionen erkannt werden "
"(freenet-Unterhändlernummer, Vodafone-VO-Nr. oder Kundenname in GROSSBUCHSTABEN)."),
(tb["Neu"], "Neue Abrechnung",
"Für jeden Abrechnungszeitraum – meist ein Monat – legen Sie eine neue Abrechnung an. "
"Mit „Öffnen“ holen Sie frühere Abrechnungen wieder hervor."),
(tb["Import"], "Dateien importieren",
"Wählen Sie die CSV- und Excel-Dateien von freenet und Vodafone – gerne mehrere auf einmal. "
"Das Programm erkennt das Format selbst und warnt bei doppelt importierten Dateien."),
(self.f_h, "Händlerliste",
"Links sehen Sie jeden Händler mit Anzahl der Positionen und Auszahlungssumme.\n\n"
"„(Nicht zugeordnet)“ ist alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
"Das wird nicht exportiert."),
(self.f_p, "Positionen prüfen",
"Rechts stehen die Positionen des gewählten Händlers, gruppiert nach Provisionsart.\n\n"
"• Doppelklick auf eine Position: Betrag frei ändern\n"
"• Rechtsklick: Auszahlung 0 €, Händler zuweisen, …\n"
"• Blau = von Hand geändert, Rot = durch Prozent-Regel geändert"),
(self.b_prozent, "Auszahlung in Prozent",
"Graue Gruppenzeile (Provisionsart) markieren und hier klicken – z. B. 80 % für den Sonderbonus. "
"Mit „Als Standard merken“ gilt das auch in den nächsten Monaten automatisch."),
(tb["Export"], "Exportieren",
"Zum Schluss erstellt „Export“ für jeden Händler eine Excel-Datei im gewohnten Abrechnungsformat."),
(self.status, "Summen",
"Unten sehen Sie immer die Summe der angezeigten bzw. markierten Positionen: "
"Provision laut Anbieter und Ihre Auszahlung."),
(self._suchrahmen, "Suchen",
"Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."),
(self.menueleiste.knoepfe["?"], "Das war's!",
"Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü „?“.\n\n"
"Viel Erfolg mit dem Provisions-Manager 95!"),
]
self._rundgang = ass.Rundgang(self, schritte)
def tipp_des_tages(self):
ass.TippDesTages(self).zeigen()
config.save(self.settings)
def _fehler_handler(self, exc, val, tb):
text = "".join(traceback.format_exception(exc, val, tb))
@@ -799,7 +877,8 @@ class App:
def beenden(self):
try:
self.settings["fenster"] = self.root.geometry()
self.settings["fenster"] = self.rahmen.normale_geometrie()
self.settings["maximiert"] = self.rahmen.maximiert
config.save(self.settings)
self.db.close()
finally: