Neu: - Eigene Kennungen (eigene Filiale) ausblenden: nicht angezeigt, nicht mitgerechnet, nicht exportiert; per Rechtsklick, Händler-Menü oder Import-Bericht; Händler-Zuordnung hat Vorrang - Menüs im Win95-Stil: wechseln beim Überfahren der Menüleiste, Tastatur, Kontextmenü gleich - Dialoge sperren nur den Inhalt: Hauptfenster bleibt verschiebbar/maximierbar; Dialoge haben eigenen Taskleisten-Eintrag; Klick auf gesperrten Inhalt holt den Dialog nach vorn (blinkt) - Trennlinie zwischen Händler- und Positionsliste sichtbar und ziehbar, Position wird gemerkt - Strg+Rücktaste / Strg+Entf löschen ganze Wörter in allen Eingabefeldern Behoben: - Gedrückte Knöpfe zeigten keine Beschriftung (activeForeground gehört in Tk zur Klasse Background) - Tipp des Tages: Text lief aus dem sichtbaren Bereich - Größenänderung: breiterer Griffbereich mit Ecken, Zeiger wird zuverlässig zurückgesetzt, flüssiger; Verschieben über Windows selbst (inkl. Andocken), Minimieren über Windows - Zeichen, die MS Sans Serif nicht hat (…, ▸, →, •), durch Win95-übliche ersetzt - Menü/Tooltip/Dialog: Größe+Position zusammen setzen (Tk ignoriert sonst die Position) - Suche reagiert erst nach kurzer Tipp-Pause (flüssigeres Tippen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
613 lines
29 KiB
Python
613 lines
29 KiB
Python
"""Ersteinrichtung im Stil der Windows-95-Setup-Assistenten, Programm-Rundgang und „Tipp des Tages“."""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import tkinter as tk
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
from tkinter import filedialog, ttk
|
||
|
||
from provtool import APP_NAME
|
||
from provtool.db import CAPS_ANBIETER, CAPS_WERT, Haendler, ist_grossgeschrieben
|
||
from provtool.gui import win32
|
||
from provtool.gui import win95 as w
|
||
from provtool.importer import lies_datei
|
||
from provtool.money import fmt, parse_percent
|
||
|
||
DATEITYPEN = [("Abrechnungen (CSV, Excel)", "*.csv *.xlsx *.xlsm *.xls *.txt"), ("Alle Dateien", "*.*")]
|
||
|
||
|
||
def _nummern(text: str) -> list[str]:
|
||
"""'11111102 (6 Pos.), 27518999' -> ['11111102', '27518999']"""
|
||
out = []
|
||
for teil in text.split(","):
|
||
m = re.match(r"\s*([A-Za-z0-9]+)", teil)
|
||
if m:
|
||
out.append(m.group(1))
|
||
return out
|
||
|
||
|
||
# =========================================================================== Assistent
|
||
|
||
class Einrichtungsassistent(w.Dialog):
|
||
BREITE, HOEHE = 470, 340
|
||
|
||
def __init__(self, app):
|
||
super().__init__(app.root, "Provisions-Manager 95 – Einrichtung")
|
||
self.app = app
|
||
self.dateien: list[str] = []
|
||
self.vorschlaege: dict[str, list[str]] = {"freenet": [], "vodafone": []}
|
||
self.caps_anzahl = 0
|
||
|
||
haupt = tk.Frame(self, bg=w.FACE)
|
||
haupt.pack(fill="both", expand=True, padx=10, pady=10)
|
||
bild = tk.Canvas(haupt, width=150, height=self.HOEHE, bg=w.TEAL, bd=2, relief="sunken",
|
||
highlightthickness=0)
|
||
bild.pack(side="left", fill="y")
|
||
self._bild(bild)
|
||
self.seite = tk.Frame(haupt, bg=w.FACE, width=self.BREITE, height=self.HOEHE)
|
||
self.seite.pack(side="left", fill="both", expand=True, padx=(14, 0))
|
||
self.seite.pack_propagate(False)
|
||
|
||
w.trenner(self, vertikal=False).pack(fill="x", padx=10)
|
||
k = tk.Frame(self, bg=w.FACE)
|
||
k.pack(fill="x", padx=10, pady=10)
|
||
self.b_abbr = w.knopf(k, "Abbrechen", self.abbrechen, breite=10)
|
||
self.b_abbr.pack(side="right")
|
||
self.b_weiter = w.knopf(k, "Weiter >", self._weiter, breite=10, default="active")
|
||
self.b_weiter.pack(side="right", padx=(0, 10))
|
||
self.b_zurueck = w.knopf(k, "< Zurück", self._zurueck, breite=10)
|
||
self.b_zurueck.pack(side="right")
|
||
self.bind("<Return>", lambda e: self._weiter() if not isinstance(e.widget, tk.Entry) else None)
|
||
|
||
self.v_titel = tk.StringVar(value=app._vorschlag_titel())
|
||
self.v_abrechnung = tk.BooleanVar(value=True)
|
||
self.v_rundgang = tk.BooleanVar(value=True)
|
||
self.v_db_eigen = tk.BooleanVar(value=bool(app.settings.get("db_pfad")))
|
||
self.v_db_pfad = tk.StringVar(value=str(app.db.path))
|
||
|
||
self.seiten = [self._s_willkommen, self._s_datenbank, self._s_haendler, self._s_abrechnung, self._s_fertig]
|
||
self.nr = 0
|
||
self._zeige(0)
|
||
|
||
# ---------------------------------------------------------------- Rahmen
|
||
def _bild(self, c: tk.Canvas):
|
||
self._img = w.make_icon(self, "app", 7)
|
||
c.create_rectangle(0, self.HOEHE - 70, 160, self.HOEHE + 10, fill="#006060", outline="")
|
||
for i, (x, y) in enumerate(((20, 30), (120, 55), (35, 120), (128, 150), (60, 20))):
|
||
c.create_rectangle(x, y, x + 2 + i % 2, y + 2 + i % 2, fill=w.WHITE, outline="")
|
||
c.create_image(77, self.HOEHE // 2 - 10, image=self._img)
|
||
c.create_text(77, self.HOEHE - 38, text="Provisions-\nManager 95", fill=w.WHITE, font=w.FONT_BOLD,
|
||
justify="center")
|
||
|
||
def _leeren(self):
|
||
for kind in self.seite.winfo_children():
|
||
kind.destroy()
|
||
|
||
def _ueberschrift(self, text: str):
|
||
tk.Label(self.seite, text=text, font=w.FONT_BIG, anchor="w", justify="left",
|
||
wraplength=self.BREITE).pack(fill="x", pady=(0, 10))
|
||
|
||
def _text(self, text: str, master=None, **kw):
|
||
lbl = tk.Label(master or self.seite, text=text, anchor="w", justify="left", wraplength=self.BREITE, **kw)
|
||
lbl.pack(fill="x", pady=(0, 6))
|
||
return lbl
|
||
|
||
def _zeige(self, nr: int):
|
||
self.nr = nr
|
||
self._leeren()
|
||
self.seiten[nr]()
|
||
self.b_zurueck.configure(state="normal" if nr > 0 else "disabled")
|
||
self.b_weiter.configure(text="Fertig stellen" if nr == len(self.seiten) - 1 else "Weiter >")
|
||
if self.titelleiste:
|
||
self.titelleiste.set_titel(f"Provisions-Manager 95 – Einrichtung (Schritt {nr + 1} von {len(self.seiten)})")
|
||
|
||
def _weiter(self):
|
||
pruefer = getattr(self, f"_pruefe_{self.nr}", None)
|
||
if pruefer and not pruefer():
|
||
return
|
||
if self.nr == len(self.seiten) - 1:
|
||
self.result = dict(titel=self.v_titel.get().strip() if self.v_abrechnung.get() else "",
|
||
dateien=list(self.dateien) if self.v_abrechnung.get() else [],
|
||
rundgang=self.v_rundgang.get())
|
||
self.destroy()
|
||
return
|
||
self._zeige(self.nr + 1)
|
||
|
||
def _zurueck(self):
|
||
if self.nr > 0:
|
||
self._zeige(self.nr - 1)
|
||
|
||
def abbrechen(self):
|
||
if w.frage(self, "Einrichtung abbrechen",
|
||
"Möchten Sie die Einrichtung wirklich abbrechen?\n\n"
|
||
"Bereits angelegte Händler bleiben gespeichert. Den Assistenten finden Sie jederzeit "
|
||
"unter ? > Einrichtungs-Assistent.", default_ja=False):
|
||
self.result = None
|
||
self.destroy()
|
||
|
||
# ---------------------------------------------------------------- Seite 1
|
||
def _s_willkommen(self):
|
||
self._ueberschrift(f"Willkommen beim {APP_NAME}!")
|
||
self._text("Dieser Assistent richtet das Programm in wenigen Schritten ein:")
|
||
for punkt in ("Speicherort der Daten festlegen",
|
||
"Unterhändler mit ihren Kennungen anlegen",
|
||
"die erste Abrechnung anlegen und Dateien importieren"):
|
||
self._text(f" - {punkt}")
|
||
self._text("\nDanach zeigt Ihnen ein kurzer Rundgang, wo Sie was finden.\n\n"
|
||
"Alle Angaben lassen sich später jederzeit ändern.")
|
||
self._text("Klicken Sie auf „Weiter“, um fortzufahren.", fg=w.NAVY)
|
||
|
||
# ---------------------------------------------------------------- Seite 2
|
||
def _s_datenbank(self):
|
||
self._ueberschrift("Wo sollen die Daten gespeichert werden?")
|
||
self._text("In der Datenbank liegen Ihre Händler, die Auszahlungsregeln und alle Abrechnungen "
|
||
"mit Ihren Änderungen.")
|
||
f = tk.Frame(self.seite, bg=w.FACE)
|
||
f.pack(fill="x", pady=6)
|
||
from provtool import config
|
||
standard = str(config.app_dir() / "provisionen.db")
|
||
tk.Radiobutton(f, text="Auf diesem Computer (empfohlen)", variable=self.v_db_eigen, value=False,
|
||
command=lambda: self.v_db_pfad.set(standard)).pack(anchor="w")
|
||
tk.Label(f, text=standard, fg=w.SHADOW, anchor="w", wraplength=self.BREITE - 30,
|
||
justify="left").pack(anchor="w", padx=22)
|
||
tk.Radiobutton(f, text="An einem eigenen Ort, z. B. auf einem Netzlaufwerk:", variable=self.v_db_eigen,
|
||
value=True).pack(anchor="w", pady=(10, 0))
|
||
zeile = tk.Frame(f, bg=w.FACE)
|
||
zeile.pack(fill="x", padx=22, pady=2)
|
||
e = tk.Entry(zeile, textvariable=self.v_db_pfad)
|
||
e.pack(side="left", fill="x", expand=True)
|
||
e.bind("<Key>", lambda ev: self.v_db_eigen.set(True))
|
||
w.knopf(zeile, "Durchsuchen...", self._db_waehlen).pack(side="left", padx=(4, 0))
|
||
self._text("\nTipp: Auf einem Netzlaufwerk können mehrere PCs dieselben Händler nutzen – "
|
||
"bitte aber nicht gleichzeitig damit arbeiten.", fg=w.SHADOW)
|
||
|
||
def _db_waehlen(self):
|
||
p = filedialog.asksaveasfilename(parent=self, title="Speicherort der Datenbank", defaultextension=".db",
|
||
initialfile="provisionen.db", confirmoverwrite=False,
|
||
filetypes=[("Datenbank", "*.db"), ("Alle Dateien", "*.*")])
|
||
if p:
|
||
self.v_db_pfad.set(p)
|
||
self.v_db_eigen.set(True)
|
||
|
||
def _pruefe_1(self) -> bool:
|
||
from provtool import config
|
||
ziel = self.v_db_pfad.get().strip() if self.v_db_eigen.get() else str(config.app_dir() / "provisionen.db")
|
||
if not ziel:
|
||
w.warnung(self, "Speicherort", "Bitte einen Speicherort angeben.")
|
||
return False
|
||
if Path(ziel) != Path(self.app.db.path):
|
||
try:
|
||
self.app.db_wechseln(ziel if self.v_db_eigen.get() else None)
|
||
except Exception as exc:
|
||
w.fehler(self, "Speicherort", f"Die Datenbank konnte nicht angelegt werden:\n{exc}")
|
||
return False
|
||
return True
|
||
|
||
# ---------------------------------------------------------------- Seite 3
|
||
def _s_haendler(self):
|
||
db = self.app.db
|
||
tk.Label(self.seite, text="Unterhändler anlegen", font=w.FONT_BIG, anchor="w").pack(fill="x")
|
||
self._text("Für jeden Unterhändler: Name, Auszahlung und woran seine Positionen zu erkennen sind. "
|
||
"Ihre eigene Filiale legen Sie nicht an.", fg=w.SHADOW)
|
||
|
||
g = tk.Frame(self.seite, bg=w.FACE)
|
||
g.pack(fill="x")
|
||
self.v_name, self.v_proz = tk.StringVar(), tk.StringVar(value="100")
|
||
self.v_fn, self.v_vf, self.v_caps = tk.StringVar(), tk.StringVar(), tk.BooleanVar()
|
||
tk.Label(g, text="Name / Firma:").grid(row=0, column=0, sticky="w", pady=1)
|
||
e_name = tk.Entry(g, textvariable=self.v_name)
|
||
e_name.grid(row=0, column=1, columnspan=2, sticky="we", pady=1)
|
||
tk.Label(g, text="Auszahlung:").grid(row=1, column=0, sticky="w", pady=1)
|
||
pf = tk.Frame(g, bg=w.FACE)
|
||
pf.grid(row=1, column=1, sticky="w")
|
||
tk.Entry(pf, textvariable=self.v_proz, width=6, justify="right").pack(side="left")
|
||
tk.Label(pf, text="% der Provision").pack(side="left", padx=4)
|
||
tk.Label(g, text="freenet Unterhändler-Nr.:").grid(row=2, column=0, sticky="w", pady=1)
|
||
self.cb_fn = ttk.Combobox(g, textvariable=self.v_fn, values=self.vorschlaege["freenet"])
|
||
self.cb_fn.grid(row=2, column=1, columnspan=2, sticky="we", pady=1)
|
||
tk.Label(g, text="Vodafone VO-Nr.:").grid(row=3, column=0, sticky="w", pady=1)
|
||
self.cb_vf = ttk.Combobox(g, textvariable=self.v_vf, values=self.vorschlaege["vodafone"])
|
||
self.cb_vf.grid(row=3, column=1, columnspan=2, sticky="we", pady=1)
|
||
caps_besitzer = db.kennung_map().get((CAPS_ANBIETER, CAPS_WERT))
|
||
caps_text = "Vodafone-Kunden mit Nachnamen in GROSSBUCHSTABEN gehören zu diesem Händler"
|
||
if self.caps_anzahl:
|
||
caps_text += f" ({self.caps_anzahl} Pos. in der Datei)"
|
||
cb = tk.Checkbutton(g, text=caps_text, variable=self.v_caps, wraplength=self.BREITE - 30, justify="left")
|
||
cb.grid(row=4, column=0, columnspan=3, sticky="w", pady=(2, 0))
|
||
if caps_besitzer:
|
||
cb.configure(state="disabled", text=f"GROSSBUCHSTABEN-Regel: bereits bei „{db.haendler(caps_besitzer).name}“")
|
||
g.columnconfigure(1, weight=1)
|
||
|
||
k = tk.Frame(self.seite, bg=w.FACE)
|
||
k.pack(fill="x", pady=(4, 4))
|
||
w.knopf(k, "Händler hinzufügen", self._haendler_dazu, self.app.icons("haendler")).pack(side="left")
|
||
w.knopf(k, "Nummern aus Abrechnung vorschlagen...", self._vorschlagen).pack(side="left", padx=6)
|
||
|
||
tk.Label(self.seite, text="Bereits angelegt:", anchor="w").pack(fill="x")
|
||
lf = tk.Frame(self.seite, bg=w.FACE)
|
||
lf.pack(fill="both", expand=True)
|
||
self.lb = tk.Listbox(lf, height=4, activestyle="none")
|
||
self.lb.pack(side="left", fill="both", expand=True)
|
||
w.knopf(lf, "Entfernen", self._haendler_weg).pack(side="left", anchor="n", padx=(6, 0))
|
||
self._liste()
|
||
e_name.focus_set()
|
||
|
||
def _liste(self):
|
||
self.lb.delete(0, "end")
|
||
self._lb_ids = []
|
||
for h in self.app.db.haendler_liste():
|
||
teile = []
|
||
for anb, wert in self.app.db.kennungen(h.id):
|
||
teile.append("GROSSBUCHSTABEN" if anb == CAPS_ANBIETER else f"{anb} {wert}")
|
||
proz = f"{h.standard_prozent:g} %".replace(".", ",")
|
||
self.lb.insert("end", f"{h.name} – {proz} – {', '.join(teile) or 'keine Kennung'}")
|
||
self._lb_ids.append(h.id)
|
||
|
||
def _haendler_dazu(self) -> bool:
|
||
name = self.v_name.get().strip()
|
||
if not name:
|
||
w.warnung(self, "Händler", "Bitte einen Namen eingeben.")
|
||
return False
|
||
try:
|
||
proz = parse_percent(self.v_proz.get())
|
||
except ValueError:
|
||
w.warnung(self, "Händler", "Die Auszahlung muss eine Zahl sein, z. B. 100 oder 85.")
|
||
return False
|
||
kenn = [("freenet", n) for n in _nummern(self.v_fn.get())]
|
||
kenn += [("vodafone", n) for n in _nummern(self.v_vf.get())]
|
||
if self.v_caps.get():
|
||
kenn.append((CAPS_ANBIETER, CAPS_WERT))
|
||
if not kenn and not w.frage(self, "Händler", f"Für „{name}“ ist keine Kennung angegeben – ihm werden "
|
||
"daher keine Positionen zugeordnet.\n\nTrotzdem anlegen?"):
|
||
return False
|
||
try:
|
||
self.app.db.haendler_speichern(Haendler(0, name, standard_prozent=proz), kenn)
|
||
except Exception as exc:
|
||
msg = str(exc)
|
||
if "UNIQUE" in msg:
|
||
msg = f"Es gibt bereits einen Händler „{name}“."
|
||
w.fehler(self, "Händler", msg)
|
||
return False
|
||
for v in (self.v_name, self.v_fn, self.v_vf):
|
||
v.set("")
|
||
self.v_proz.set("100")
|
||
self._zeige(self.nr) # neu aufbauen (GROSSBUCHSTABEN-Regel evtl. vergeben)
|
||
return True
|
||
|
||
def _haendler_weg(self):
|
||
sel = self.lb.curselection()
|
||
if sel:
|
||
self.app.db.haendler_loeschen(self._lb_ids[sel[0]])
|
||
self._zeige(self.nr)
|
||
|
||
def _vorschlagen(self):
|
||
dateien = filedialog.askopenfilenames(parent=self, title="Abrechnung zum Auslesen der Nummern wählen",
|
||
filetypes=DATEITYPEN)
|
||
if not dateien:
|
||
return
|
||
anzahl: dict[str, Counter] = defaultdict(Counter)
|
||
summe: dict[tuple[str, str], int] = defaultdict(int)
|
||
caps = 0
|
||
fehler = []
|
||
for f in dateien:
|
||
try:
|
||
erg = lies_datei(f)
|
||
except Exception as exc:
|
||
fehler.append(f"{Path(f).name}: {exc}")
|
||
continue
|
||
if f not in self.dateien:
|
||
self.dateien.append(f)
|
||
for p in erg.positionen:
|
||
anzahl[p.anbieter][p.kennung] += 1
|
||
summe[(p.anbieter, p.kennung)] += p.betrag_cent
|
||
if p.anbieter == "vodafone" and ist_grossgeschrieben(p.name):
|
||
caps += 1
|
||
vergeben = self.app.db.kennung_map()
|
||
for anb in ("freenet", "vodafone"):
|
||
self.vorschlaege[anb] = [f"{k} ({n} Pos., {fmt(summe[(anb, k)])})"
|
||
for k, n in anzahl[anb].most_common() if k and (anb, k) not in vergeben]
|
||
self.caps_anzahl = caps
|
||
self._zeige(self.nr)
|
||
if fehler:
|
||
w.warnung(self, "Nummern vorschlagen", "Nicht lesbar:\n" + "\n".join(fehler))
|
||
else:
|
||
w.info(self, "Nummern vorschlagen",
|
||
"Die gefundenen Nummern stehen jetzt in den Auswahllisten (Pfeil-Knopf rechts) bei freenet und Vodafone – "
|
||
"sortiert nach Anzahl der Positionen.\n\nDie Nummer mit den meisten Positionen ist meist "
|
||
"Ihre eigene Filiale.\n\nDie Dateien werden im nächsten Schritt gleich mit importiert.")
|
||
|
||
def _pruefe_2(self) -> bool:
|
||
if self.v_name.get().strip():
|
||
if w.frage(self, "Händler", f"„{self.v_name.get().strip()}“ wurde noch nicht hinzugefügt.\n\n"
|
||
"Jetzt hinzufügen?"):
|
||
return self._haendler_dazu() and False # Seite bleibt offen, Liste ist aktualisiert
|
||
if not self.app.db.haendler_liste():
|
||
return w.frage(self, "Händler", "Es ist noch kein Händler angelegt.\n\n"
|
||
"Trotzdem weiter? (Händler lassen sich später unter „Händler“ anlegen.)",
|
||
default_ja=False)
|
||
return True
|
||
|
||
# ---------------------------------------------------------------- Seite 4
|
||
def _s_abrechnung(self):
|
||
self._ueberschrift("Die erste Abrechnung")
|
||
self._text("Pro Abrechnungszeitraum (meist ein Monat) legen Sie eine Abrechnung an und importieren "
|
||
"dort alle Dateien dieses Zeitraums – z. B. alle freenet-Wochenlisten und die Vodafone-Datei.")
|
||
tk.Checkbutton(self.seite, text="Jetzt eine Abrechnung anlegen", variable=self.v_abrechnung,
|
||
command=self._s_abrechnung_status).pack(anchor="w")
|
||
f = tk.Frame(self.seite, bg=w.FACE)
|
||
f.pack(fill="both", expand=True, padx=(22, 0))
|
||
z = tk.Frame(f, bg=w.FACE)
|
||
z.pack(fill="x", pady=4)
|
||
tk.Label(z, text="Bezeichnung:").pack(side="left")
|
||
self._e_titel = tk.Entry(z, textvariable=self.v_titel, width=28)
|
||
self._e_titel.pack(side="left", padx=6)
|
||
tk.Label(f, text="Diese Dateien gleich importieren (optional):", anchor="w").pack(fill="x", pady=(6, 2))
|
||
lf = tk.Frame(f, bg=w.FACE)
|
||
lf.pack(fill="both", expand=True)
|
||
self.lb_d = tk.Listbox(lf, height=5)
|
||
self.lb_d.pack(side="left", fill="both", expand=True)
|
||
kf = tk.Frame(lf, bg=w.FACE)
|
||
kf.pack(side="left", anchor="n", padx=(6, 0))
|
||
self._b_dazu = w.knopf(kf, "Hinzufügen...", self._datei_dazu, breite=11)
|
||
self._b_dazu.pack(pady=(0, 4))
|
||
self._b_weg = w.knopf(kf, "Entfernen", self._datei_weg, breite=11)
|
||
self._b_weg.pack()
|
||
for d in self.dateien:
|
||
self.lb_d.insert("end", Path(d).name)
|
||
self._s_abrechnung_status()
|
||
|
||
def _s_abrechnung_status(self):
|
||
st = "normal" if self.v_abrechnung.get() else "disabled"
|
||
for wdg in (self._e_titel, self.lb_d, self._b_dazu, self._b_weg):
|
||
wdg.configure(state=st)
|
||
|
||
def _datei_dazu(self):
|
||
for f in filedialog.askopenfilenames(parent=self, title="Dateien zum Import wählen", filetypes=DATEITYPEN):
|
||
if f not in self.dateien:
|
||
self.dateien.append(f)
|
||
self.lb_d.insert("end", Path(f).name)
|
||
|
||
def _datei_weg(self):
|
||
sel = self.lb_d.curselection()
|
||
if sel:
|
||
del self.dateien[sel[0]]
|
||
self.lb_d.delete(sel[0])
|
||
|
||
def _pruefe_3(self) -> bool:
|
||
if self.v_abrechnung.get() and not self.v_titel.get().strip():
|
||
w.warnung(self, "Abrechnung", "Bitte eine Bezeichnung eingeben, z. B. „Oktober 2026“.")
|
||
return False
|
||
return True
|
||
|
||
# ---------------------------------------------------------------- Seite 5
|
||
def _s_fertig(self):
|
||
self._ueberschrift("Fertig!")
|
||
n = len(self.app.db.haendler_liste())
|
||
self._text(f"Der {APP_NAME} ist eingerichtet.")
|
||
self._text(f" - {n} Händler angelegt")
|
||
if self.v_abrechnung.get():
|
||
self._text(f" - Abrechnung „{self.v_titel.get().strip()}“"
|
||
+ (f" mit {len(self.dateien)} Datei(en)" if self.dateien else " (noch ohne Dateien)"))
|
||
self._text("\nSo geht es danach jeden Monat weiter:\n"
|
||
" Neu > Import > prüfen & anpassen > Export")
|
||
tk.Checkbutton(self.seite, text="Kurzen Rundgang durch das Programm starten", variable=self.v_rundgang
|
||
).pack(anchor="w", pady=(12, 0))
|
||
self._text("\nKlicken Sie auf „Fertig stellen“.", fg=w.NAVY)
|
||
|
||
|
||
# =========================================================================== Rundgang
|
||
|
||
class Rundgang:
|
||
"""Führt mit gelben Hinweis-Sprechblasen durch die Oberfläche und rahmt das jeweilige Element rot ein."""
|
||
|
||
def __init__(self, app, schritte: list[tuple]):
|
||
"""schritte: (widget_oder_funktion, titel, text)"""
|
||
self.app, self.root, self.schritte = app, app.root, schritte
|
||
self.nr = 0
|
||
self.blase: tk.Toplevel | None = None
|
||
self.rahmen: list[tk.Toplevel] = []
|
||
self._sperre = None # Windows: Hauptfenster während des Rundgangs gesperrt (win32.Modal)
|
||
self._zeige(0)
|
||
|
||
@staticmethod
|
||
def _entsorgen(blase, rahmen):
|
||
for r in rahmen:
|
||
try:
|
||
r.destroy()
|
||
except tk.TclError:
|
||
pass
|
||
if blase is not None:
|
||
try:
|
||
blase.grab_release()
|
||
except tk.TclError:
|
||
pass
|
||
try:
|
||
blase.destroy()
|
||
except tk.TclError:
|
||
pass
|
||
|
||
def _weg(self):
|
||
self._entsorgen(self.blase, self.rahmen)
|
||
self.rahmen = []
|
||
self.blase = None
|
||
|
||
def beenden(self):
|
||
if self._sperre is not None: # vor dem Schließen freigeben, sonst springt Windows woandershin
|
||
self._sperre.beenden()
|
||
self._sperre = None
|
||
self._weg()
|
||
|
||
def _markieren(self, x, y, bw, bh, dicke=3, farbe="#ff0000"):
|
||
for gx, gy, gw, gh in ((x - dicke, y - dicke, bw + 2 * dicke, dicke), (x - dicke, y + bh, bw + 2 * dicke, dicke),
|
||
(x - dicke, y, dicke, bh), (x + bw, y, dicke, bh)):
|
||
t = tk.Toplevel(self.root, bg=farbe)
|
||
t.overrideredirect(True) # kein transient() – siehe win32.py
|
||
t.geometry(f"{max(1, gw)}x{max(1, gh)}+{gx}+{gy}")
|
||
t.update_idletasks()
|
||
win32.besitzer_setzen(t, self.root) # bleibt über dem Hauptfenster, auch nach Alt+Tab
|
||
self.rahmen.append(t)
|
||
|
||
def _zeige(self, nr: int):
|
||
# Die alte Sprechblase verschwindet erst, wenn die neue steht: Wäre kurz kein Popup da,
|
||
# während das Hauptfenster gesperrt ist, würde Windows ein fremdes Programm aktivieren.
|
||
alt_blase, alt_rahmen = self.blase, self.rahmen
|
||
self.blase, self.rahmen = None, []
|
||
if getattr(self.root, "_provtool_schliesst", False):
|
||
self._entsorgen(alt_blase, alt_rahmen)
|
||
return
|
||
self.nr = nr
|
||
ziel, titel, text = self.schritte[nr]
|
||
if callable(ziel) and not isinstance(ziel, tk.Misc):
|
||
ziel = ziel()
|
||
self.root.update_idletasks()
|
||
x, y = ziel.winfo_rootx(), ziel.winfo_rooty()
|
||
bw, bh = ziel.winfo_width(), ziel.winfo_height()
|
||
self._markieren(x, y, bw, bh)
|
||
|
||
b = self.blase = tk.Toplevel(self.root, bg=w.DARK)
|
||
b.abbrechen = self.beenden # Alt+F4 auf der Sprechblase beendet den Rundgang sauber
|
||
b.withdraw()
|
||
b.overrideredirect(True) # kein transient() – Besitzer setzt win32.Modal
|
||
innen = tk.Frame(b, bg=w.INFO_BG, padx=10, pady=8)
|
||
innen.pack(padx=1, pady=1)
|
||
kopf = tk.Frame(innen, bg=w.INFO_BG)
|
||
kopf.pack(fill="x")
|
||
self._ic = w.make_icon(b, "birne")
|
||
tk.Label(kopf, image=self._ic, bg=w.INFO_BG).pack(side="left")
|
||
tk.Label(kopf, text=titel, bg=w.INFO_BG, font=w.FONT_BOLD).pack(side="left", padx=6)
|
||
tk.Label(innen, text=text, bg=w.INFO_BG, justify="left", anchor="w", wraplength=330).pack(fill="x", pady=(6, 8))
|
||
fuss = tk.Frame(innen, bg=w.INFO_BG)
|
||
fuss.pack(fill="x")
|
||
tk.Label(fuss, text=f"Schritt {nr + 1} von {len(self.schritte)}", bg=w.INFO_BG,
|
||
fg=w.SHADOW).pack(side="left")
|
||
letzter = nr == len(self.schritte) - 1
|
||
w.knopf(fuss, "Fertig" if letzter else "Weiter >", self._weiter, default="active").pack(side="right")
|
||
zb = w.knopf(fuss, "< Zurück", self._zurueck)
|
||
zb.pack(side="right", padx=4)
|
||
if nr == 0:
|
||
zb.configure(state="disabled")
|
||
if not letzter:
|
||
w.knopf(fuss, "Beenden", self.beenden).pack(side="right", padx=(0, 8))
|
||
b.bind("<Return>", lambda e: self._weiter())
|
||
b.bind("<Right>", lambda e: self._weiter())
|
||
b.bind("<Left>", lambda e: self._zurueck())
|
||
b.bind("<Escape>", lambda e: self.beenden())
|
||
|
||
# Position: unter dem Element, sonst darüber, sonst innen oben links; immer im Hauptfenster
|
||
b.update_idletasks()
|
||
pw, ph = b.winfo_reqwidth(), b.winfo_reqheight()
|
||
rx, ry = self.root.winfo_rootx(), self.root.winfo_rooty()
|
||
rw, rh = self.root.winfo_width(), self.root.winfo_height()
|
||
if y + bh + 12 + ph <= ry + rh:
|
||
px, py = x, y + bh + 12
|
||
elif y - 12 - ph >= ry:
|
||
px, py = x, y - 12 - ph
|
||
else:
|
||
px, py = x + 40, y + 40
|
||
px = min(max(rx + 8, px), rx + rw - pw - 8)
|
||
py = min(max(ry + 8, py), ry + rh - ph - 8)
|
||
b.geometry(f"{pw}x{ph}+{px}+{py}")
|
||
b.deiconify()
|
||
b.lift()
|
||
b.update_idletasks()
|
||
b.focus_force()
|
||
if self._sperre is None:
|
||
self._sperre = win32.Modal(b, self.root)
|
||
else:
|
||
win32.besitzer_setzen(b, self.root)
|
||
self._entsorgen(alt_blase, alt_rahmen)
|
||
try:
|
||
b.grab_set()
|
||
except tk.TclError:
|
||
pass
|
||
b.focus_force()
|
||
|
||
def _weiter(self):
|
||
if self.nr >= len(self.schritte) - 1:
|
||
self.beenden()
|
||
else:
|
||
self._zeige(self.nr + 1)
|
||
|
||
def _zurueck(self):
|
||
if self.nr > 0:
|
||
self._zeige(self.nr - 1)
|
||
|
||
|
||
# =========================================================================== Tipp des Tages
|
||
|
||
TIPPS = [
|
||
"Sie können mehrere Dateien gleichzeitig importieren – zum Beispiel alle freenet-Wochenlisten eines Monats "
|
||
"und die Vodafone-Datei in einem Rutsch.",
|
||
"Markieren Sie eine ganze Provisionsart (die graue Zeile) und klicken Sie auf „Prozent...“, um z. B. nur 80 % "
|
||
"auszuzahlen. Mit dem Häkchen „Als Standard merken“ gilt das automatisch auch in den nächsten Monaten.",
|
||
"Doppelklick auf eine Position – und Sie können den Auszahlungsbetrag direkt überschreiben. Mit Pfeil runter geht es "
|
||
"gleich in die nächste Zeile.",
|
||
"Blau dargestellte Beträge wurden von Hand geändert, rote weichen durch eine Prozent-Regel von der "
|
||
"Provision ab.",
|
||
"Eine Position soll gar nicht ausgezahlt werden? Rechtsklick > „Auszahlung 0 €“.",
|
||
"Die GROSSBUCHSTABEN-Regel hat einen Kunden falsch erwischt? Rechtsklick > „Händler zuweisen...“ – diese "
|
||
"Zuordnung hat immer Vorrang.",
|
||
"Unter „(Nicht zugeordnet)“ sehen Sie alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
|
||
"Diese Positionen werden nicht exportiert.",
|
||
"Mit Strg+F springen Sie ins Suchfeld. Gesucht wird in Rufnummer, Name, Tarif, Provisionsart und mehr.",
|
||
"Wurde eine Datei versehentlich doppelt oder falsch importiert? Datei > Importierte Dateien... – dort lässt "
|
||
"sie sich wieder entfernen.",
|
||
"Erstellen Sie ab und zu eine Datensicherung: Datei > Datensicherung erstellen...",
|
||
"Alle Änderungen werden sofort gespeichert. Ein „Speichern“-Knopf ist daher nicht nötig.",
|
||
"Ein Händler wurde erst nach dem Import angelegt? Kein Problem – die Zuordnung passt sich sofort an.",
|
||
]
|
||
|
||
|
||
BILDER = Path(__file__).resolve().parent / "bilder"
|
||
|
||
|
||
def tipp_bild(master) -> tk.PhotoImage | None:
|
||
"""Der Büroklammer-Assistent für den Tipp des Tages (liegt auch in der .exe, siehe .spec)."""
|
||
try:
|
||
return tk.PhotoImage(master=master, file=str(BILDER / "tipp.png"))
|
||
except tk.TclError:
|
||
return None
|
||
|
||
|
||
class TippDesTages(w.Dialog):
|
||
def __init__(self, app):
|
||
super().__init__(app.root, "Tipp des Tages")
|
||
self.app = app
|
||
haupt = tk.Frame(self, bg=w.FACE)
|
||
haupt.pack(padx=12, pady=12)
|
||
box = tk.Frame(haupt, bg=w.WHITE, bd=2, relief="sunken")
|
||
box.pack(side="left", fill="both")
|
||
self._bild = tipp_bild(self)
|
||
self.bild_ok = self._bild is not None
|
||
if self._bild is None: # Ersatz, falls das Bild fehlt
|
||
self._bild = w.make_icon(self, "birne", 3)
|
||
tk.Label(box, image=self._bild, bg=w.WHITE).pack(side="left", anchor="n", padx=(12, 4), pady=10)
|
||
# fester Textbereich: der Tipp bricht darin um und kann nicht mehr abgeschnitten werden
|
||
rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10, width=380, height=self._bild.height() + 20)
|
||
rechts.pack(side="left", fill="both")
|
||
rechts.pack_propagate(False)
|
||
tk.Label(rechts, text="Wussten Sie schon...", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"),
|
||
anchor="w").pack(fill="x")
|
||
tk.Frame(rechts, bg=w.DARK, height=1).pack(fill="x", pady=6)
|
||
self.text = tk.Label(rechts, bg=w.WHITE, justify="left", anchor="nw", wraplength=380 - 2 * 12)
|
||
self.text.pack(fill="both", expand=True)
|
||
knoepfe = tk.Frame(haupt, bg=w.FACE)
|
||
knoepfe.pack(side="left", anchor="n", padx=(10, 0))
|
||
w.knopf(knoepfe, "Schließen", self.abbrechen, breite=12, default="active").pack(pady=(0, 6))
|
||
w.knopf(knoepfe, "Nächster Tipp", self._naechster, breite=12).pack()
|
||
self.v_start = tk.BooleanVar(value=app.settings.get("tipps_beim_start", True))
|
||
tk.Checkbutton(self, text="Tipps beim Start anzeigen", variable=self.v_start).pack(anchor="w", padx=12,
|
||
pady=(0, 10))
|
||
self.bind("<Return>", lambda e: self.abbrechen())
|
||
self._naechster()
|
||
|
||
def _naechster(self):
|
||
nr = self.app.settings.get("tipp_nr", 0) % len(TIPPS)
|
||
self.text.configure(text=TIPPS[nr])
|
||
self.app.settings["tipp_nr"] = nr + 1
|
||
|
||
def abbrechen(self):
|
||
self.app.settings["tipps_beim_start"] = self.v_start.get()
|
||
self.destroy()
|