"""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("", 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("", 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("", lambda e: self._weiter()) b.bind("", lambda e: self._weiter()) b.bind("", lambda e: self._zurueck()) b.bind("", 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("", 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()