1065 lines
52 KiB
Python
1065 lines
52 KiB
Python
"""Dialogfenster des Provisions-Managers."""
|
||
from __future__ import annotations
|
||
|
||
import tkinter as tk
|
||
from tkinter import filedialog, ttk
|
||
|
||
from provtool import APP_ORG, __version__
|
||
from provtool import regeln
|
||
from provtool import zeitraum as zr
|
||
from provtool.db import CAPS_ANBIETER, CAPS_WERT, AbrechnungErledigt, Datenbank, Haendler
|
||
from provtool.gui import win95 as w
|
||
from provtool.importer import ANBIETER
|
||
from provtool.money import fmt, fmt_percent, parse_cent, parse_percent
|
||
|
||
# Auswahl im Händler-Dialog: Schlüssel -> Anzeige
|
||
KENNUNG_ARTEN = {"freenet": "freenet (Unterhändler-Nr.)", "vodafone": "Vodafone (VO-Nr.)"}
|
||
CAPS_TEXT = "Kunden-Nachname komplett in GROSSBUCHSTABEN"
|
||
CAPS_HAKEN = "Vodafone-Kunden mit Nachnamen in GROSSBUCHSTABEN gehören zu diesem Händler"
|
||
|
||
|
||
# =========================================================================== Händlerverwaltung
|
||
|
||
class HaendlerDialog(w.Dialog):
|
||
def __init__(self, master, db: Datenbank, auswahl_id: int | None = None,
|
||
neue_kennung: tuple[str, str] | None = None):
|
||
super().__init__(master, "Händler verwalten", resizable=True)
|
||
self.db = db
|
||
self.aktuell: Haendler | None = None
|
||
self.geaendert = False
|
||
|
||
haupt = tk.Frame(self, bg=w.FACE)
|
||
haupt.pack(fill="both", expand=True, padx=8, pady=8)
|
||
|
||
# --- links: Liste
|
||
links = tk.Frame(haupt, bg=w.FACE)
|
||
links.pack(side="left", fill="y")
|
||
tk.Label(links, text="Händler:", anchor="w").pack(fill="x")
|
||
rahmen, self.liste = w.scroll_tree(links, columns=("name",), show="tree", selectmode="browse", height=16)
|
||
self.liste.column("#0", width=0, stretch=False)
|
||
self.liste.column("name", width=190)
|
||
rahmen.pack(fill="y", expand=True)
|
||
self.liste.bind("<<TreeviewSelect>>", self._auswahl)
|
||
lk = tk.Frame(links, bg=w.FACE)
|
||
lk.pack(fill="x", pady=(6, 0))
|
||
w.knopf(lk, "Neu", self._neu, breite=8).pack(side="left")
|
||
w.knopf(lk, "Löschen", self._loeschen, breite=8).pack(side="left", padx=4)
|
||
|
||
# --- rechts: Formular
|
||
rechts = tk.Frame(haupt, bg=w.FACE)
|
||
rechts.pack(side="left", fill="both", expand=True, padx=(10, 0))
|
||
|
||
g = w.gruppe(rechts, "Stammdaten")
|
||
g.pack(fill="x")
|
||
self.v_name, self.v_ap, self.v_mail = tk.StringVar(), tk.StringVar(), tk.StringVar()
|
||
self.v_proz, self.v_aktiv = tk.StringVar(value="100"), tk.BooleanVar(value=True)
|
||
self.v_air = tk.StringVar(value="0")
|
||
for r, (lbl, var) in enumerate((("Name / Firma:", self.v_name), ("Ansprechpartner:", self.v_ap),
|
||
("E-Mail:", self.v_mail))):
|
||
tk.Label(g, text=lbl, anchor="w").grid(row=r, column=0, sticky="w", pady=2)
|
||
tk.Entry(g, textvariable=var, width=36).grid(row=r, column=1, columnspan=2, sticky="we", pady=2)
|
||
tk.Label(g, text="Standard-Auszahlung:", anchor="w").grid(row=3, column=0, sticky="w", pady=2)
|
||
pf = tk.Frame(g, bg=w.FACE)
|
||
pf.grid(row=3, column=1, sticky="w")
|
||
tk.Entry(pf, textvariable=self.v_proz, width=7, justify="right").pack(side="left")
|
||
tk.Label(pf, text="% der Provision").pack(side="left", padx=4)
|
||
tk.Checkbutton(g, text="Aktiv", variable=self.v_aktiv).grid(row=3, column=2, sticky="e")
|
||
tk.Label(g, text="Airtime:", anchor="w").grid(row=4, column=0, sticky="w", pady=2)
|
||
af = tk.Frame(g, bg=w.FACE)
|
||
af.grid(row=4, column=1, columnspan=2, sticky="w")
|
||
self.e_air = tk.Entry(af, textvariable=self.v_air, width=7, justify="right")
|
||
self.e_air.pack(side="left")
|
||
tk.Label(af, text="% der Airtime (0 % = keine Airtime)").pack(side="left", padx=4)
|
||
tk.Label(g, text="Notiz:", anchor="nw").grid(row=5, column=0, sticky="nw", pady=2)
|
||
self.t_notiz = tk.Text(g, width=36, height=2, font=w.FONT)
|
||
self.t_notiz.grid(row=5, column=1, columnspan=2, sticky="we", pady=2)
|
||
g.columnconfigure(1, weight=1)
|
||
|
||
g2 = w.gruppe(rechts, "Kennungen (ordnet Positionen diesem Händler zu)")
|
||
g2.pack(fill="both", expand=True, pady=(8, 0))
|
||
r2, self.kliste = w.scroll_tree(g2, columns=("anbieter", "wert"), show="headings", height=3,
|
||
selectmode="browse")
|
||
self.kliste.heading("anbieter", text="Anbieter")
|
||
self.kliste.heading("wert", text="Unterhändler-Nr. / VO-Nr.")
|
||
self.kliste.column("anbieter", width=90)
|
||
self.kliste.column("wert", width=260)
|
||
r2.pack(fill="both", expand=True)
|
||
kf = tk.Frame(g2, bg=w.FACE)
|
||
kf.pack(fill="x", pady=(4, 0))
|
||
self.v_kanb = tk.StringVar(value=KENNUNG_ARTEN["freenet"])
|
||
cb = ttk.Combobox(kf, textvariable=self.v_kanb, values=list(KENNUNG_ARTEN.values()), width=34,
|
||
state="readonly")
|
||
cb.pack(side="left")
|
||
cb.bind("<<ComboboxSelected>>", lambda e: self._art_gewaehlt())
|
||
self.v_kwert = tk.StringVar()
|
||
ke = tk.Entry(kf, textvariable=self.v_kwert, width=14)
|
||
self._ke = ke
|
||
ke.pack(side="left", padx=4)
|
||
ke.bind("<Return>", lambda e: self._kennung_dazu())
|
||
w.knopf(kf, "Hinzufügen", self._kennung_dazu).pack(side="left")
|
||
w.knopf(kf, "Entfernen", self._kennung_weg).pack(side="left", padx=4)
|
||
self.v_caps = tk.BooleanVar(value=False)
|
||
self.cb_caps = tk.Checkbutton(g2, text=CAPS_HAKEN, variable=self.v_caps, anchor="w")
|
||
self.cb_caps.pack(fill="x", pady=(4, 0))
|
||
|
||
g4 = w.gruppe(rechts, "Jahresbonus Neuaktivierung (Betrag je Karte nach Staffel)")
|
||
g4.pack(fill="x", pady=(8, 0))
|
||
self.v_eigen_staffel = tk.BooleanVar(value=False)
|
||
sf = tk.Frame(g4, bg=w.FACE)
|
||
sf.pack(fill="x")
|
||
tk.Radiobutton(sf, text="Standard-Staffel", variable=self.v_eigen_staffel, value=False,
|
||
command=self._staffel_zeigen).pack(side="left")
|
||
tk.Radiobutton(sf, text="Eigene Staffel für diesen Händler", variable=self.v_eigen_staffel, value=True,
|
||
command=self._staffel_zeigen).pack(side="left", padx=(8, 0))
|
||
w.knopf(sf, "Bearbeiten...", self._staffel_bearbeiten).pack(side="right")
|
||
self.l_staffel = tk.Label(g4, text="", anchor="w", justify="left", fg=w.NAVY, wraplength=560)
|
||
self.l_staffel.pack(fill="x", pady=(2, 0))
|
||
self._eigene_staffel: regeln.Staffel | None = None
|
||
|
||
g3 = w.gruppe(rechts, "Abweichende Auszahlung je Provisionsart")
|
||
g3.pack(fill="both", expand=True, pady=(8, 0))
|
||
r3, self.rliste = w.scroll_tree(g3, columns=("art", "proz"), show="headings", height=3, selectmode="browse")
|
||
self.rliste.heading("art", text="Provisionsart")
|
||
self.rliste.heading("proz", text="Auszahlung")
|
||
self.rliste.column("art", width=240)
|
||
self.rliste.column("proz", width=70, anchor="e")
|
||
r3.pack(fill="both", expand=True)
|
||
rf = tk.Frame(g3, bg=w.FACE)
|
||
rf.pack(fill="x", pady=(4, 0))
|
||
tk.Label(rf, text="Regeln entstehen im Hauptfenster über „Prozent...“.", fg=w.SHADOW).pack(side="left")
|
||
w.knopf(rf, "Entfernen", self._regel_weg).pack(side="right")
|
||
|
||
unten = tk.Frame(self, bg=w.FACE)
|
||
unten.pack(fill="x", padx=8, pady=(0, 8))
|
||
w.knopf(unten, "Schließen", self.abbrechen, breite=10).pack(side="right")
|
||
w.knopf(unten, "Speichern", self._speichern, breite=10, default="active").pack(side="right", padx=6)
|
||
|
||
self._kennungen: list[tuple[str, str]] = []
|
||
self._liste_laden(auswahl_id)
|
||
if neue_kennung:
|
||
self._neu()
|
||
if neue_kennung[0] == CAPS_ANBIETER:
|
||
self.v_caps.set(True)
|
||
else:
|
||
self._kennungen = [neue_kennung]
|
||
self._kennungen_zeigen()
|
||
elif self.liste.selection():
|
||
self._auswahl() # sofort füllen – sonst wächst der Dialog erst nach der Größenberechnung
|
||
self.minsize(660, 560)
|
||
|
||
# ---- intern
|
||
@staticmethod
|
||
def _anbieter_key(anzeige: str) -> str:
|
||
return next((k for k, v in KENNUNG_ARTEN.items() if v == anzeige), anzeige)
|
||
|
||
def _art_gewaehlt(self):
|
||
self._ke.focus_set()
|
||
|
||
def _caps_einstellen(self, hid: int, an: bool):
|
||
"""GROSSBUCHSTABEN-Häkchen: gesperrt, wenn die Regel schon einem anderen Händler gehört."""
|
||
self.v_caps.set(an)
|
||
besitzer = self.db.kennung_map().get((CAPS_ANBIETER, CAPS_WERT))
|
||
if besitzer and besitzer != hid:
|
||
h = self.db.haendler(besitzer)
|
||
self.cb_caps.configure(state="disabled", text=f"GROSSBUCHSTABEN-Regel: gehört zu „{h.name if h else '?'}“")
|
||
else:
|
||
self.cb_caps.configure(state="normal", text=CAPS_HAKEN)
|
||
|
||
def _staffel_zeigen(self):
|
||
eigen = self.v_eigen_staffel.get()
|
||
if eigen and self._eigene_staffel is None:
|
||
self._eigene_staffel = list(self.db.standard_staffel())
|
||
st = self._eigene_staffel if eigen else self.db.standard_staffel()
|
||
self.l_staffel.configure(text=("Eigene Staffel: " if eigen else "Standard: ")
|
||
+ regeln.staffel_beschreibung(st, kurz=True))
|
||
|
||
def _staffel_bearbeiten(self):
|
||
if not self.v_eigen_staffel.get():
|
||
if w.frage(self, "Jahresbonus-Staffel",
|
||
"Die Standard-Staffel gilt für alle Händler ohne eigene Staffel.\n\n"
|
||
"Soll dieser Händler eine eigene Staffel bekommen?\n"
|
||
"(Die Standard-Staffel ändern Sie unter Händler > Standard-Staffel Jahresbonus...)",
|
||
ja="Eigene Staffel", nein="Abbrechen"):
|
||
self.v_eigen_staffel.set(True)
|
||
else:
|
||
return
|
||
neu = StaffelDialog(self, self._eigene_staffel or self.db.standard_staffel(),
|
||
f"Jahresbonus-Staffel – {self.v_name.get().strip() or 'neuer Händler'}",
|
||
standard=self.db.standard_staffel()).zeigen()
|
||
if neu is not None:
|
||
self._eigene_staffel = neu
|
||
self._staffel_zeigen()
|
||
|
||
def _liste_laden(self, auswahl_id=None):
|
||
self.liste.delete(*self.liste.get_children())
|
||
for h in self.db.haendler_liste():
|
||
self.liste.insert("", "end", iid=str(h.id), values=(h.name + ("" if h.aktiv else " (inaktiv)"),))
|
||
if auswahl_id and self.liste.exists(str(auswahl_id)):
|
||
self.liste.selection_set(str(auswahl_id))
|
||
self.liste.see(str(auswahl_id))
|
||
elif self.liste.get_children():
|
||
self.liste.selection_set(self.liste.get_children()[0])
|
||
else:
|
||
self._neu()
|
||
|
||
def _auswahl(self, _e=None):
|
||
sel = self.liste.selection()
|
||
if not sel:
|
||
return
|
||
h = self.db.haendler(int(sel[0]))
|
||
if not h:
|
||
return
|
||
self.aktuell = h
|
||
self.v_name.set(h.name)
|
||
self.v_ap.set(h.ansprechpartner)
|
||
self.v_mail.set(h.email)
|
||
self.v_proz.set(fmt_percent(h.standard_prozent).replace(" %", ""))
|
||
self.v_air.set(fmt_percent(h.airtime_prozent).replace(" %", ""))
|
||
self.v_aktiv.set(h.aktiv)
|
||
self.t_notiz.delete("1.0", "end")
|
||
self.t_notiz.insert("1.0", h.notiz or "")
|
||
kenn = self.db.kennungen(h.id)
|
||
self._kennungen = [k for k in kenn if k[0] != CAPS_ANBIETER]
|
||
self._caps_einstellen(h.id, (CAPS_ANBIETER, CAPS_WERT) in kenn)
|
||
self._eigene_staffel = list(h.staffel) if h.staffel is not None else None
|
||
self.v_eigen_staffel.set(h.staffel is not None)
|
||
self._staffel_zeigen()
|
||
self._kennungen_zeigen()
|
||
self._regeln_zeigen()
|
||
|
||
def _kennungen_zeigen(self):
|
||
self.kliste.delete(*self.kliste.get_children())
|
||
for i, (a, v) in enumerate(self._kennungen):
|
||
self.kliste.insert("", "end", iid=str(i), values=(ANBIETER.get(a, a), v))
|
||
|
||
def _regeln_zeigen(self):
|
||
self.rliste.delete(*self.rliste.get_children())
|
||
if self.aktuell and self.aktuell.id:
|
||
for art, p in sorted(self.db.regeln(self.aktuell.id).items()):
|
||
self.rliste.insert("", "end", iid=art, values=(art, fmt_percent(p)))
|
||
|
||
def _neu(self):
|
||
self.liste.selection_remove(self.liste.selection())
|
||
self.aktuell = Haendler(0, "")
|
||
for v in (self.v_name, self.v_ap, self.v_mail):
|
||
v.set("")
|
||
self.v_proz.set("100")
|
||
self.v_air.set("0")
|
||
self.v_aktiv.set(True)
|
||
self.t_notiz.delete("1.0", "end")
|
||
self._kennungen = []
|
||
self._caps_einstellen(0, False)
|
||
self._eigene_staffel = None
|
||
self.v_eigen_staffel.set(False)
|
||
self._staffel_zeigen()
|
||
self._kennungen_zeigen()
|
||
self._regeln_zeigen()
|
||
|
||
def _kennung_dazu(self):
|
||
anb = self._anbieter_key(self.v_kanb.get())
|
||
wert = self.v_kwert.get().strip()
|
||
if not wert:
|
||
return
|
||
if (anb, wert) not in self._kennungen:
|
||
self._kennungen.append((anb, wert))
|
||
self.v_kwert.set("")
|
||
self._kennungen_zeigen()
|
||
|
||
def _kennung_weg(self):
|
||
sel = self.kliste.selection()
|
||
if sel:
|
||
del self._kennungen[int(sel[0])]
|
||
self._kennungen_zeigen()
|
||
|
||
def _regel_weg(self):
|
||
sel = self.rliste.selection()
|
||
if sel and self.aktuell and self.aktuell.id:
|
||
self.db.regel_setzen(self.aktuell.id, sel[0], None)
|
||
self.geaendert = True
|
||
self._regeln_zeigen()
|
||
|
||
def _speichern(self):
|
||
name = self.v_name.get().strip()
|
||
if not name:
|
||
w.warnung(self, "Händler", "Bitte einen Namen eingeben.")
|
||
return
|
||
try:
|
||
proz = parse_percent(self.v_proz.get())
|
||
except ValueError:
|
||
w.warnung(self, "Händler", "Die Standard-Auszahlung muss eine Zahl sein (z. B. 100 oder 85,5).")
|
||
return
|
||
try:
|
||
air = parse_percent(self.v_air.get() or "0")
|
||
except ValueError:
|
||
w.warnung(self, "Händler", "Die Airtime-Auszahlung muss eine Zahl sein (z. B. 0, 50 oder 100).")
|
||
return
|
||
if self.v_kwert.get().strip():
|
||
self._kennung_dazu()
|
||
h = Haendler(self.aktuell.id if self.aktuell else 0, name, self.v_ap.get().strip(), self.v_mail.get().strip(),
|
||
self.t_notiz.get("1.0", "end").strip(), proz, self.v_aktiv.get(), air,
|
||
self._eigene_staffel if self.v_eigen_staffel.get() else None)
|
||
kennungen = list(self._kennungen)
|
||
if self.v_caps.get() and str(self.cb_caps.cget("state")) != "disabled":
|
||
kennungen.append((CAPS_ANBIETER, CAPS_WERT))
|
||
try:
|
||
hid = self.db.haendler_speichern(h, kennungen)
|
||
except Exception as exc: # sqlite3.IntegrityError / ValueError
|
||
msg = str(exc)
|
||
if "UNIQUE" in msg and "haendler.name" in msg:
|
||
msg = f"Es gibt bereits einen Händler „{name}“."
|
||
w.fehler(self, "Händler speichern", msg)
|
||
return
|
||
self.geaendert = True
|
||
self._liste_laden(hid)
|
||
|
||
def _loeschen(self):
|
||
if not self.aktuell or not self.aktuell.id:
|
||
return
|
||
if w.frage(self, "Händler löschen",
|
||
f"Händler „{self.aktuell.name}“ mit allen Kennungen und Regeln löschen?\n\n"
|
||
"Positionen in Abrechnungen bleiben erhalten, gelten dann aber als „nicht zugeordnet“.",
|
||
default_ja=False):
|
||
self.db.haendler_loeschen(self.aktuell.id)
|
||
self.geaendert = True
|
||
self.aktuell = None
|
||
self._liste_laden()
|
||
|
||
def abbrechen(self):
|
||
self.result = self.geaendert
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Prozent anwenden
|
||
|
||
class ProzentDialog(w.Dialog):
|
||
def __init__(self, master, haendler_name: str, provisionsart: str | None, anzahl: int, summe_orig: int,
|
||
vorschlag: float, regel_moeglich: bool):
|
||
super().__init__(master, "Auszahlung festlegen")
|
||
self.summe_orig = summe_orig
|
||
f = tk.Frame(self, bg=w.FACE)
|
||
f.pack(fill="both", padx=12, pady=12)
|
||
ic = w.make_icon(self, "prozent", 2)
|
||
self._ic = ic
|
||
tk.Label(f, image=ic).grid(row=0, column=0, rowspan=4, sticky="n", padx=(0, 12))
|
||
was = f"Provisionsart „{provisionsart}“" if provisionsart else f"{anzahl} ausgewählte Position(en)"
|
||
tk.Label(f, text=f"Händler: {haendler_name}\n{was}\nProvision laut Abrechnung: {fmt(summe_orig)}",
|
||
justify="left", anchor="w").grid(row=0, column=1, columnspan=3, sticky="w")
|
||
tk.Label(f, text="Auszahlung:").grid(row=1, column=1, sticky="w", pady=(10, 2))
|
||
self.v = tk.StringVar(value=fmt_percent(vorschlag).replace(" %", ""))
|
||
ef = tk.Frame(f, bg=w.FACE)
|
||
ef.grid(row=1, column=2, columnspan=2, sticky="w", pady=(10, 2))
|
||
e = tk.Entry(ef, textvariable=self.v, width=8, justify="right")
|
||
e.pack(side="left")
|
||
tk.Label(ef, text="% der Provision").pack(side="left", padx=4)
|
||
self.vorschau = tk.Label(f, text="", fg=w.NAVY, anchor="w")
|
||
self.vorschau.grid(row=2, column=1, columnspan=3, sticky="w")
|
||
self.v_regel = tk.BooleanVar(value=bool(provisionsart and regel_moeglich))
|
||
self.v_manuell = tk.BooleanVar(value=False)
|
||
cbf = tk.Frame(f, bg=w.FACE)
|
||
cbf.grid(row=3, column=1, columnspan=3, sticky="w", pady=(8, 0))
|
||
if provisionsart and regel_moeglich:
|
||
tk.Checkbutton(cbf, text="Als Standard für diesen Händler und diese Provisionsart merken",
|
||
variable=self.v_regel).pack(anchor="w")
|
||
tk.Checkbutton(cbf, text="Auch von Hand geänderte Beträge überschreiben",
|
||
variable=self.v_manuell).pack(anchor="w")
|
||
self.v.trace_add("write", lambda *_: self._vorschau())
|
||
self._vorschau()
|
||
self.bind("<Return>", lambda _e: self._ok())
|
||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 12))
|
||
e.select_range(0, "end")
|
||
self._e = e
|
||
|
||
def _vorschau(self):
|
||
from provtool.money import apply_percent
|
||
try:
|
||
p = parse_percent(self.v.get())
|
||
self.vorschau.configure(text=f"-> Auszahlung ca. {fmt(apply_percent(self.summe_orig, p))}")
|
||
except ValueError:
|
||
self.vorschau.configure(text="(ungültiger Wert)")
|
||
|
||
def _ok(self):
|
||
try:
|
||
p = parse_percent(self.v.get())
|
||
except ValueError:
|
||
w.warnung(self, "Auszahlung", "Bitte einen Prozentwert eingeben, z. B. 100, 80 oder 72,5.")
|
||
return
|
||
self.result = (p, self.v_regel.get(), self.v_manuell.get())
|
||
self.destroy()
|
||
|
||
def zeigen(self, fokus=None):
|
||
return super().zeigen(self._e)
|
||
|
||
|
||
# =========================================================================== Jahresbonus-Staffel
|
||
|
||
class StaffelDialog(w.Dialog):
|
||
"""Staffel bearbeiten: je Stufe „ab N Karten“ und Betrag je Karte (oder „voll“)."""
|
||
|
||
def __init__(self, master, staffel: regeln.Staffel, titel: str = "Jahresbonus-Staffel",
|
||
standard: regeln.Staffel | None = None):
|
||
super().__init__(master, titel)
|
||
self.standard = standard
|
||
f = tk.Frame(self, bg=w.FACE)
|
||
f.pack(fill="both", expand=True, padx=12, pady=(10, 4))
|
||
tk.Label(f, justify="left", anchor="w", wraplength=380, text=(
|
||
"Jahresbonus Neuaktivierung: Der Händler bekommt je Karte den Betrag der Stufe, die zu seiner "
|
||
"Anzahl Karten in der Abrechnung passt. „voll“ = ganzer Betrag laut Abrechnung. Nie mehr als "
|
||
"die Provision laut Abrechnung; Stornos zählen ab.")).pack(fill="x")
|
||
kopf = tk.Frame(f, bg=w.FACE)
|
||
kopf.pack(fill="x", pady=(8, 2))
|
||
tk.Label(kopf, text="ab Karten", width=9, anchor="w").pack(side="left")
|
||
tk.Label(kopf, text="Betrag je Karte (oder „voll“)", anchor="w").pack(side="left", padx=(8, 0))
|
||
self.tabelle = tk.Frame(f, bg=w.FACE)
|
||
self.tabelle.pack(fill="x")
|
||
self.zeilen: list[tuple[tk.Frame, tk.StringVar, tk.StringVar]] = []
|
||
for ab, cent in staffel:
|
||
self._zeile(ab, cent)
|
||
kk = tk.Frame(f, bg=w.FACE)
|
||
kk.pack(fill="x", pady=(6, 0))
|
||
w.knopf(kk, "Stufe hinzufügen", self._dazu).pack(side="left")
|
||
w.knopf(kk, "Letzte entfernen", self._weg).pack(side="left", padx=4)
|
||
if standard is not None:
|
||
w.knopf(kk, "Wie Standard", lambda: self._setzen(standard)).pack(side="right")
|
||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(6, 10))
|
||
|
||
def _zeile(self, ab: int | str = "", cent: int | None | str = ""):
|
||
z = tk.Frame(self.tabelle, bg=w.FACE)
|
||
z.pack(fill="x", pady=1)
|
||
v_ab = tk.StringVar(value=str(ab))
|
||
v_bt = tk.StringVar(value="voll" if cent is None else (fmt(cent, symbol=False) if isinstance(cent, int)
|
||
else str(cent)))
|
||
tk.Entry(z, textvariable=v_ab, width=8, justify="right").pack(side="left")
|
||
tk.Entry(z, textvariable=v_bt, width=10, justify="right").pack(side="left", padx=(12, 0))
|
||
tk.Label(z, text="€ je Karte").pack(side="left", padx=4)
|
||
self.zeilen.append((z, v_ab, v_bt))
|
||
|
||
def _dazu(self):
|
||
letzte = self.zeilen[-1][1].get() if self.zeilen else "0"
|
||
try:
|
||
ab = int(letzte) + 5
|
||
except ValueError:
|
||
ab = ""
|
||
self._zeile(ab, "")
|
||
|
||
def _weg(self):
|
||
if len(self.zeilen) > 1:
|
||
self.zeilen.pop()[0].destroy()
|
||
|
||
def _setzen(self, staffel):
|
||
for z, _, _ in self.zeilen:
|
||
z.destroy()
|
||
self.zeilen = []
|
||
for ab, cent in staffel:
|
||
self._zeile(ab, cent)
|
||
|
||
def lesen(self) -> regeln.Staffel:
|
||
out = []
|
||
for i, (_, v_ab, v_bt) in enumerate(self.zeilen, start=1):
|
||
try:
|
||
ab = int(v_ab.get().strip())
|
||
except ValueError:
|
||
raise ValueError(f"Stufe {i}: „ab Karten“ muss eine ganze Zahl sein.") from None
|
||
text = v_bt.get().strip()
|
||
if text.lower() == "voll":
|
||
cent = None
|
||
else:
|
||
try:
|
||
cent = parse_cent(text)
|
||
except ValueError:
|
||
raise ValueError(f"Stufe {i}: Bitte einen Betrag (z. B. 5 oder 7,50) oder „voll“ eingeben.") \
|
||
from None
|
||
out.append((ab, cent))
|
||
return regeln.staffel_pruefen(out)
|
||
|
||
def _ok(self):
|
||
try:
|
||
self.result = self.lesen()
|
||
except ValueError as exc:
|
||
w.warnung(self, "Staffel", str(exc))
|
||
return
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Provisionsarten
|
||
|
||
TYP_AUTO = "Automatisch"
|
||
|
||
|
||
class ProvisionsartenDialog(w.Dialog):
|
||
"""Welche Provisionsart ist Airtime bzw. Jahresbonus? Erkannt wird automatisch, umstellbar je Art."""
|
||
|
||
def __init__(self, master, db: Datenbank):
|
||
super().__init__(master, "Provisionsarten", resizable=True)
|
||
self.db = db
|
||
self.geaendert = False
|
||
f = tk.Frame(self, bg=w.FACE)
|
||
f.pack(fill="both", expand=True, padx=10, pady=10)
|
||
tk.Label(f, anchor="w", justify="left", text=(
|
||
"Airtime wird pro Händler mit dem Airtime-Prozentsatz ausgezahlt und im Export als eigene Liste\n"
|
||
"geführt. Der Jahresbonus Neuaktivierung wird nach der Staffel berechnet.")).pack(fill="x")
|
||
r, self.tree = w.scroll_tree(f, columns=("art", "anzahl", "typ"), show="headings", height=12,
|
||
selectmode="browse")
|
||
for c, t, b, a in (("art", "Provisionsart", 300, "w"), ("anzahl", "Pos.", 50, "e"),
|
||
("typ", "Behandlung", 230, "w")):
|
||
self.tree.heading(c, text=t)
|
||
self.tree.column(c, width=b, anchor=a)
|
||
r.pack(fill="both", expand=True, pady=(6, 4))
|
||
self.tree.bind("<<TreeviewSelect>>", lambda e: self._auswahl())
|
||
u = tk.Frame(f, bg=w.FACE)
|
||
u.pack(fill="x")
|
||
tk.Label(u, text="Behandlung:").pack(side="left")
|
||
self.v_typ = tk.StringVar(value=TYP_AUTO)
|
||
self.cb = ttk.Combobox(u, textvariable=self.v_typ, state="readonly", width=36,
|
||
values=[TYP_AUTO] + list(regeln.TYPEN.values()))
|
||
self.cb.pack(side="left", padx=4)
|
||
w.knopf(u, "Übernehmen", self._setzen).pack(side="left")
|
||
self.knopfleiste([("Schließen", self.abbrechen)]).pack(pady=(0, 10))
|
||
self._laden()
|
||
self.minsize(620, 380)
|
||
|
||
def _laden(self, auswahl: str | None = None):
|
||
self.tree.delete(*self.tree.get_children())
|
||
fest = self.db.art_typen()
|
||
self._arten = {}
|
||
for i, (art, erkannt, n) in enumerate(self.db.provisionsarten()):
|
||
typ = fest.get(art)
|
||
text = regeln.TYPEN[typ] if typ else f"{TYP_AUTO}: {regeln.TYPEN[erkannt]}"
|
||
iid = str(i)
|
||
self._arten[iid] = art
|
||
self.tree.insert("", "end", iid=iid, values=(art or "(ohne)", n, text))
|
||
if art == auswahl:
|
||
self.tree.selection_set(iid)
|
||
self.tree.see(iid)
|
||
|
||
def _auswahl(self):
|
||
sel = self.tree.selection()
|
||
if not sel:
|
||
return
|
||
typ = self.db.art_typen().get(self._arten[sel[0]])
|
||
self.v_typ.set(regeln.TYPEN[typ] if typ else TYP_AUTO)
|
||
|
||
def _setzen(self):
|
||
sel = self.tree.selection()
|
||
if not sel:
|
||
w.info(self, "Provisionsarten", "Bitte zuerst eine Provisionsart in der Liste auswählen.")
|
||
return
|
||
art = self._arten[sel[0]]
|
||
wahl = self.v_typ.get()
|
||
typ = next((k for k, v in regeln.TYPEN.items() if v == wahl), None)
|
||
self.db.art_typ_setzen(art, typ)
|
||
self.geaendert = True
|
||
self._laden(art)
|
||
|
||
def abbrechen(self):
|
||
self.result = self.geaendert
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Abrechnungen
|
||
|
||
class AbrechnungenDialog(w.Dialog):
|
||
def __init__(self, master, db: Datenbank, aktuelle_id: int | None, fuer_import: bool = False):
|
||
"""fuer_import: Ziel eines Imports wählen (Ergebnis ("oeffnen", id) oder ("neu",))."""
|
||
super().__init__(master, "Import – in welche Abrechnung?" if fuer_import else "Abrechnung öffnen",
|
||
resizable=True)
|
||
self.db = db
|
||
tk.Label(self, text="In welche Abrechnung sollen die Dateien importiert werden?" if fuer_import
|
||
else "Gespeicherte Abrechnungen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
||
r, self.tree = w.scroll_tree(self, columns=("titel", "status", "geaendert", "anzahl", "dateien"),
|
||
show="headings", height=12, selectmode="browse")
|
||
for c, t, b in (("titel", "Bezeichnung", 180), ("status", "Status", 110), ("geaendert", "Zuletzt geändert", 120),
|
||
("anzahl", "Positionen", 70), ("dateien", "Dateien", 260)):
|
||
self.tree.heading(c, text=t)
|
||
self.tree.column(c, width=b, anchor="e" if c == "anzahl" else "w")
|
||
r.pack(fill="both", expand=True, padx=10)
|
||
self.tree.bind("<Double-1>", lambda e: self._oeffnen())
|
||
k = tk.Frame(self, bg=w.FACE)
|
||
k.pack(fill="x", padx=10, pady=10)
|
||
if fuer_import:
|
||
w.knopf(k, "Hierhin importieren", self._oeffnen, default="active").pack(side="left")
|
||
w.knopf(k, "Neue Abrechnung...", self._neu).pack(side="left", padx=6)
|
||
else:
|
||
w.knopf(k, "Öffnen", self._oeffnen, breite=10, default="active").pack(side="left")
|
||
w.knopf(k, "Umbenennen...", self._umbenennen, breite=12).pack(side="left", padx=6)
|
||
w.knopf(k, "Löschen", self._loeschen, breite=10).pack(side="left")
|
||
w.knopf(k, "Abbrechen", self.abbrechen, breite=10).pack(side="right")
|
||
self.bind("<Return>", lambda e: self._oeffnen())
|
||
self._laden(aktuelle_id)
|
||
|
||
def _laden(self, sel=None):
|
||
self.tree.delete(*self.tree.get_children())
|
||
for a in self.db.abrechnungen():
|
||
status = "erledigt" if a["erledigt"] else (f"{a['abgerechnet']} abgerechnet" if a["abgerechnet"] else "offen")
|
||
self.tree.insert("", "end", iid=str(a["id"]),
|
||
values=(a["titel"], status, a["geaendert"][:16], a["anzahl"], a["dateien"] or ""))
|
||
kids = self.tree.get_children()
|
||
if sel and self.tree.exists(str(sel)):
|
||
self.tree.selection_set(str(sel))
|
||
elif kids:
|
||
self.tree.selection_set(kids[0])
|
||
|
||
def _sel(self) -> int | None:
|
||
s = self.tree.selection()
|
||
return int(s[0]) if s else None
|
||
|
||
def _oeffnen(self):
|
||
if self._sel():
|
||
self.result = ("oeffnen", self._sel())
|
||
self.destroy()
|
||
|
||
def _neu(self):
|
||
self.result = ("neu",)
|
||
self.destroy()
|
||
|
||
def _umbenennen(self):
|
||
aid = self._sel()
|
||
if not aid:
|
||
return
|
||
t = w.eingabe(self, "Umbenennen", "Neue Bezeichnung:", self.db.abrechnung(aid)["titel"])
|
||
if t and t.strip():
|
||
self.db.abrechnung_umbenennen(aid, t.strip())
|
||
self._laden(aid)
|
||
self.result = ("geaendert", aid)
|
||
|
||
def _loeschen(self):
|
||
aid = self._sel()
|
||
if not aid:
|
||
return
|
||
titel = self.db.abrechnung(aid)["titel"]
|
||
if w.frage(self, "Löschen", f"Abrechnung „{titel}“ mit allen Positionen und Änderungen endgültig löschen?",
|
||
default_ja=False):
|
||
self.db.abrechnung_loeschen(aid)
|
||
self._laden()
|
||
self.result = ("geloescht", aid)
|
||
|
||
def abbrechen(self):
|
||
self.destroy()
|
||
|
||
|
||
class DateienDialog(w.Dialog):
|
||
"""Zeigt die importierten Dateien einer Abrechnung; einzelne Dateien können wieder entfernt werden."""
|
||
|
||
def __init__(self, master, db: Datenbank, aid: int):
|
||
super().__init__(master, "Importierte Dateien", resizable=True)
|
||
self.db, self.aid, self.geaendert = db, aid, False
|
||
r, self.tree = w.scroll_tree(self, columns=("datei", "format", "anzahl", "zeit"), show="headings", height=8,
|
||
selectmode="browse")
|
||
for c, t, b in (("datei", "Datei", 260), ("format", "Format", 80), ("anzahl", "Positionen", 70),
|
||
("zeit", "Importiert", 120)):
|
||
self.tree.heading(c, text=t)
|
||
self.tree.column(c, width=b)
|
||
r.pack(fill="both", expand=True, padx=10, pady=(10, 0))
|
||
k = tk.Frame(self, bg=w.FACE)
|
||
k.pack(fill="x", padx=10, pady=10)
|
||
w.knopf(k, "Datei entfernen", self._weg).pack(side="left")
|
||
w.knopf(k, "Schließen", self.abbrechen, breite=10).pack(side="right")
|
||
self._laden()
|
||
|
||
def _laden(self):
|
||
self.tree.delete(*self.tree.get_children())
|
||
for d in self.db.dateien(self.aid):
|
||
self.tree.insert("", "end", iid=str(d["id"]),
|
||
values=(d["dateiname"], d["format"], d["anzahl"], d["importiert"][:16]))
|
||
|
||
def _weg(self):
|
||
s = self.tree.selection()
|
||
if not s:
|
||
return
|
||
name = self.tree.item(s[0], "values")[0]
|
||
if w.frage(self, "Datei entfernen", f"Alle Positionen aus „{name}“ aus dieser Abrechnung entfernen?\n"
|
||
"Von Hand geänderte Beträge dieser Datei gehen verloren.", default_ja=False):
|
||
try:
|
||
self.db.datei_entfernen(int(s[0]))
|
||
except AbrechnungErledigt as exc:
|
||
w.info(self, "Datei entfernen", str(exc))
|
||
return
|
||
self.geaendert = True
|
||
self._laden()
|
||
|
||
def abbrechen(self):
|
||
self.result = self.geaendert
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Import-Bericht
|
||
|
||
class ImportBericht(w.Dialog):
|
||
def __init__(self, master, zeilen: list[tuple[str, str, int, int, str]], unbekannt: list[tuple[str, str, int, int]]):
|
||
"""zeilen: (datei, format, anzahl, summe_cent, hinweis); unbekannt: (anbieter, kennung, anzahl, summe)."""
|
||
super().__init__(master, "Import abgeschlossen", resizable=True)
|
||
tk.Label(self, text="Folgende Dateien wurden eingelesen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
||
r, t = w.scroll_tree(self, columns=("datei", "format", "anzahl", "summe", "hinweis"), show="headings",
|
||
height=min(6, max(2, len(zeilen))))
|
||
for c, h, b, a in (("datei", "Datei", 220, "w"), ("format", "Format", 70, "w"), ("anzahl", "Pos.", 50, "e"),
|
||
("summe", "Summe", 90, "e"), ("hinweis", "Hinweis", 260, "w")):
|
||
t.heading(c, text=h)
|
||
t.column(c, width=b, anchor=a)
|
||
for d, f, n, s, hw in zeilen:
|
||
t.insert("", "end", values=(d, f, n, fmt(s) if n else "", hw))
|
||
r.pack(fill="both", expand=True, padx=10)
|
||
self.zuordnen = None
|
||
if unbekannt:
|
||
tk.Label(self, text="Nicht zugeordnete Kennungen (kein Händler hinterlegt – z. B. eigene Filiale):",
|
||
anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
||
r2, self.ut = w.scroll_tree(self, columns=("anbieter", "kennung", "anzahl", "summe"), show="headings",
|
||
height=min(6, len(unbekannt)), selectmode="extended")
|
||
for c, h, b, a in (("anbieter", "Anbieter", 90, "w"), ("kennung", "Kennung", 140, "w"),
|
||
("anzahl", "Pos.", 50, "e"), ("summe", "Summe", 90, "e")):
|
||
self.ut.heading(c, text=h)
|
||
self.ut.column(c, width=b, anchor=a)
|
||
for a, k, n, s in unbekannt:
|
||
self.ut.insert("", "end", iid=f"{a}\t{k}", values=(ANBIETER.get(a, a), k or "(leer)", n, fmt(s)))
|
||
r2.pack(fill="both", expand=True, padx=10)
|
||
k = tk.Frame(self, bg=w.FACE)
|
||
k.pack(fill="x", padx=10, pady=10)
|
||
if unbekannt:
|
||
w.knopf(k, "Kennung einem Händler zuordnen...", self._zuordnen).pack(side="left")
|
||
w.knopf(k, "Als eigene ausblenden...", self._eigen).pack(side="left", padx=6)
|
||
w.knopf(k, "OK", self.abbrechen, breite=10, default="active").pack(side="right")
|
||
self.bind("<Return>", lambda e: self.abbrechen())
|
||
|
||
def _zuordnen(self):
|
||
s = self.ut.selection()
|
||
if s:
|
||
a, k = s[0].split("\t", 1)
|
||
self.result = ("zuordnen", a, k)
|
||
self.destroy()
|
||
|
||
def _eigen(self):
|
||
s = self.ut.selection()
|
||
if s:
|
||
self.result = ("eigen", [tuple(x.split("\t", 1)) for x in s])
|
||
self.destroy()
|
||
|
||
|
||
class EigeneKennungenDialog(w.Dialog):
|
||
"""Kennungen der eigenen Filiale: Positionen damit werden ausgeblendet und nicht exportiert."""
|
||
|
||
def __init__(self, master, db: Datenbank):
|
||
super().__init__(master, "Eigene Kennungen")
|
||
self.db, self.geaendert = db, False
|
||
tk.Label(self, text="Positionen mit diesen Kennungen gehören zur eigenen Filiale. Sie werden\n"
|
||
"ausgeblendet, nicht mitgerechnet und nicht exportiert.\n"
|
||
"(Ist eine Kennung einem Händler zugeordnet, hat der Händler Vorrang.)",
|
||
justify="left", anchor="w").pack(fill="x", padx=10, pady=(10, 4))
|
||
r, self.tree = w.scroll_tree(self, columns=("anbieter", "wert"), show="headings", height=7,
|
||
selectmode="extended")
|
||
self.tree.heading("anbieter", text="Anbieter")
|
||
self.tree.heading("wert", text="Unterhändler-Nr. / VO-Nr.")
|
||
self.tree.column("anbieter", width=100)
|
||
self.tree.column("wert", width=200)
|
||
r.pack(fill="both", expand=True, padx=10)
|
||
z = tk.Frame(self, bg=w.FACE)
|
||
z.pack(fill="x", padx=10, pady=6)
|
||
self.v_anb = tk.StringVar(value="freenet")
|
||
ttk.Combobox(z, textvariable=self.v_anb, values=list(ANBIETER.values()), width=10,
|
||
state="readonly").pack(side="left")
|
||
self.v_wert = tk.StringVar()
|
||
e = tk.Entry(z, textvariable=self.v_wert, width=18)
|
||
e.pack(side="left", padx=4)
|
||
e.bind("<Return>", lambda ev: self._dazu())
|
||
w.knopf(z, "Hinzufügen", self._dazu).pack(side="left")
|
||
w.knopf(z, "Entfernen", self._weg).pack(side="left", padx=4)
|
||
self.knopfleiste([("Schließen", self.abbrechen)]).pack(pady=(0, 10))
|
||
self._laden()
|
||
|
||
def _laden(self):
|
||
self.tree.delete(*self.tree.get_children())
|
||
for a, k in self.db.eigene_kennungen():
|
||
self.tree.insert("", "end", iid=f"{a}\t{k}", values=(ANBIETER.get(a, a), k))
|
||
|
||
def _dazu(self):
|
||
wert = self.v_wert.get().strip()
|
||
if not wert:
|
||
return
|
||
anb = next((k for k, v in ANBIETER.items() if v == self.v_anb.get()), self.v_anb.get())
|
||
self.db.eigene_setzen(anb, wert, True)
|
||
self.v_wert.set("")
|
||
self.geaendert = True
|
||
self._laden()
|
||
|
||
def _weg(self):
|
||
for iid in self.tree.selection():
|
||
a, k = iid.split("\t", 1)
|
||
self.db.eigene_setzen(a, k, False)
|
||
self.geaendert = True
|
||
self._laden()
|
||
|
||
def abbrechen(self):
|
||
self.result = self.geaendert
|
||
self.destroy()
|
||
|
||
|
||
class KennungZuordnen(w.Dialog):
|
||
"""Fragt, ob eine Kennung einem bestehenden oder neuen Händler zugeordnet werden soll."""
|
||
|
||
def __init__(self, master, db: Datenbank, anbieter: str, kennung: str):
|
||
super().__init__(master, "Kennung zuordnen")
|
||
self.db, self.anbieter, self.kennung = db, anbieter, kennung
|
||
tk.Label(self, text=f"{ANBIETER.get(anbieter, anbieter)}-Kennung {kennung} zuordnen zu:",
|
||
anchor="w").pack(fill="x", padx=12, pady=(12, 6))
|
||
self.v = tk.StringVar(value="__neu__")
|
||
f = tk.Frame(self, bg=w.FACE)
|
||
f.pack(fill="x", padx=20)
|
||
tk.Radiobutton(f, text="Neuem Händler ...", variable=self.v, value="__neu__").pack(anchor="w")
|
||
haendler = db.haendler_liste()
|
||
if haendler:
|
||
tk.Radiobutton(f, text="Bestehendem Händler:", variable=self.v, value="__alt__").pack(anchor="w")
|
||
self.cb = ttk.Combobox(f, values=[h.name for h in haendler], state="readonly", width=34)
|
||
self.cb.pack(anchor="w", padx=22, pady=(0, 4))
|
||
self.cb.bind("<<ComboboxSelected>>", lambda e: self.v.set("__alt__"))
|
||
self._hmap = {h.name: h.id for h in haendler}
|
||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=12)
|
||
|
||
def _ok(self):
|
||
if self.v.get() == "__neu__":
|
||
self.result = ("neu", None)
|
||
else:
|
||
name = self.cb.get()
|
||
if not name:
|
||
w.warnung(self, "Kennung zuordnen", "Bitte einen Händler auswählen.")
|
||
return
|
||
hid = self._hmap[name]
|
||
kenn = self.db.kennungen(hid) + [(self.anbieter, self.kennung)]
|
||
try:
|
||
self.db.haendler_speichern(self.db.haendler(hid), kenn)
|
||
except ValueError as exc:
|
||
w.fehler(self, "Kennung zuordnen", str(exc))
|
||
return
|
||
self.result = ("alt", hid)
|
||
self.destroy()
|
||
|
||
|
||
class HaendlerWaehlen(w.Dialog):
|
||
"""Feste Zuweisung ausgewählter Positionen zu einem Händler."""
|
||
|
||
KEINER = "(keinem Händler – nicht auszahlen)"
|
||
|
||
def __init__(self, master, db: Datenbank, anzahl: int):
|
||
super().__init__(master, "Händler zuweisen")
|
||
self._hmap = {h.name: h.id for h in db.haendler_liste(nur_aktive=True)}
|
||
tk.Label(self, text=f"{anzahl} Position(en) fest zuweisen an:", anchor="w").pack(fill="x", padx=12,
|
||
pady=(12, 4))
|
||
self.cb = ttk.Combobox(self, values=list(self._hmap) + [self.KEINER], state="readonly", width=40)
|
||
self.cb.pack(padx=12, pady=4)
|
||
tk.Label(self, text="Die feste Zuweisung hat Vorrang vor Kennungen und der GROSSBUCHSTABEN-Regel.\n"
|
||
"Rückgängig: Rechtsklick > „Automatisch zuordnen“.", justify="left",
|
||
fg=w.SHADOW).pack(fill="x", padx=12)
|
||
self.bind("<Return>", lambda e: self._ok())
|
||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=12)
|
||
|
||
def _ok(self):
|
||
name = self.cb.get()
|
||
if not name:
|
||
w.warnung(self, "Händler zuweisen", "Bitte einen Händler auswählen.")
|
||
return
|
||
self.result = 0 if name == self.KEINER else self._hmap[name]
|
||
self.destroy()
|
||
|
||
def zeigen(self, fokus=None):
|
||
return super().zeigen(self.cb)
|
||
|
||
|
||
# =========================================================================== Export
|
||
|
||
class ExportDialog(w.Dialog):
|
||
def __init__(self, master, haendler: list[tuple[int, str, int, int]], vorauswahl: set[int], settings: dict,
|
||
titel: str):
|
||
"""haendler: (id, name, anzahl, summe_cent)"""
|
||
super().__init__(master, "Abrechnungen exportieren")
|
||
self.settings = settings
|
||
rumpf = tk.Frame(self, bg=w.FACE)
|
||
rumpf.pack(fill="both", expand=True, padx=10, pady=10)
|
||
# zwei Spalten (links Händler, rechts Zeitraum und Optionen) – passt auch auf kleine Bildschirme
|
||
f = tk.Frame(rumpf, bg=w.FACE)
|
||
f.pack(side="left", fill="both", expand=True)
|
||
rechts = tk.Frame(rumpf, bg=w.FACE)
|
||
rechts.pack(side="left", fill="y", padx=(14, 0))
|
||
tk.Label(f, text="Für welche Händler soll eine Excel-Datei erstellt werden?", anchor="w").pack(fill="x")
|
||
r, self.tree = w.scroll_tree(f, columns=("name", "anzahl", "summe"), show="tree headings", height=12,
|
||
selectmode="extended")
|
||
self.tree.heading("#0", text="")
|
||
self.tree.column("#0", width=40, stretch=False)
|
||
for c, t, b, a in (("name", "Händler", 220, "w"), ("anzahl", "Pos.", 50, "e"), ("summe", "Auszahlung", 100, "e")):
|
||
self.tree.heading(c, text=t)
|
||
self.tree.column(c, width=b, anchor=a)
|
||
self.haken = {}
|
||
for hid, name, n, s in haendler:
|
||
self.haken[hid] = hid in vorauswahl
|
||
self.tree.insert("", "end", iid=str(hid), text="[x]" if self.haken[hid] else "[ ]",
|
||
values=(name, n, fmt(s)))
|
||
self.tree.bind("<Button-1>", self._klick)
|
||
self.tree.bind("<space>", lambda e: [self._toggle(int(i)) for i in self.tree.selection()])
|
||
r.pack(fill="both", expand=True, pady=(2, 4))
|
||
kk = tk.Frame(f, bg=w.FACE)
|
||
kk.pack(fill="x")
|
||
w.knopf(kk, "Alle", lambda: self._alle(True)).pack(side="left")
|
||
w.knopf(kk, "Keine", lambda: self._alle(False)).pack(side="left", padx=4)
|
||
|
||
g = w.gruppe(rechts, "Optionen")
|
||
g.pack(fill="x", pady=(8, 0))
|
||
tk.Label(g, text="Bezeichnung im Dateinamen:").grid(row=0, column=0, sticky="w")
|
||
self.v_titel = tk.StringVar(value=titel)
|
||
tk.Entry(g, textvariable=self.v_titel, width=30).grid(row=0, column=1, columnspan=2, sticky="we", pady=2)
|
||
tk.Label(g, text="Zielordner:").grid(row=1, column=0, sticky="w")
|
||
self.v_ordner = tk.StringVar(value=settings.get("export_ordner") or "")
|
||
tk.Entry(g, textvariable=self.v_ordner, width=40).grid(row=1, column=1, sticky="we", pady=2)
|
||
w.knopf(g, "Durchsuchen...", self._ordner).grid(row=1, column=2, padx=(4, 0))
|
||
# Neuer Schlüssel: freenet und Vodafone stehen jetzt standardmäßig in EINER Datei
|
||
self.v_anb = tk.BooleanVar(value=settings.get("export_anbieter_getrennt", False))
|
||
self.v_null = tk.BooleanVar(value=settings.get("export_null_weglassen", True))
|
||
self.v_kopf = tk.BooleanVar(value=settings.get("export_kopfzeile", False))
|
||
self.v_sum = tk.BooleanVar(value=settings.get("export_summe", True))
|
||
self.v_auf = tk.BooleanVar(value=True)
|
||
for zeile, (text, var) in enumerate((
|
||
("freenet und Vodafone in getrennte Dateien (sonst beide in einer)", self.v_anb),
|
||
("Positionen mit 0,00 € weglassen", self.v_null),
|
||
("Kopfzeile mit Spaltennamen", self.v_kopf),
|
||
("Summenzeile unter der Tabelle", self.v_sum),
|
||
("Ordner nach dem Export öffnen", self.v_auf)), start=2):
|
||
tk.Checkbutton(g, text=text, variable=var).grid(row=zeile, column=0, columnspan=3, sticky="w")
|
||
tk.Label(g, text="Airtime steht in der Abrechnung als eine Summenzeile, die Einzelposten kommen in eine "
|
||
"eigene Datei „... - Airtime - ...xlsx“.", fg=w.SHADOW, justify="left", anchor="w",
|
||
wraplength=420).grid(
|
||
row=7, column=0, columnspan=3, sticky="w", pady=(4, 0))
|
||
g.columnconfigure(1, weight=1)
|
||
|
||
z = w.gruppe(rechts, "Zeitraum und Abrechnung")
|
||
z.pack(fill="x", before=g)
|
||
zf = tk.Frame(z, bg=w.FACE)
|
||
zf.pack(fill="x")
|
||
tk.Label(zf, text="Nur von:").pack(side="left")
|
||
self.v_von, self.v_bis = tk.StringVar(), tk.StringVar()
|
||
tk.Entry(zf, textvariable=self.v_von, width=11).pack(side="left", padx=4)
|
||
tk.Label(zf, text="bis:").pack(side="left")
|
||
tk.Entry(zf, textvariable=self.v_bis, width=11).pack(side="left", padx=4)
|
||
w.knopf(zf, "Vormonat", self._vormonat).pack(side="left", padx=(4, 0))
|
||
# ohne Zeitraum = alles, was noch nicht abgerechnet ist (abgerechnete Positionen exportiert er nie)
|
||
w.knopf(zf, "Noch nicht abgerechnet", lambda: (self.v_von.set(""), self.v_bis.set(""))).pack(
|
||
side="left", padx=(4, 0))
|
||
self.v_basis = tk.StringVar(value=zr.BASEN[settings.get("export_basis", zr.AKTIVIERUNG)])
|
||
ttk.Combobox(z, textvariable=self.v_basis, values=list(zr.BASEN.values()), state="readonly",
|
||
width=40).pack(anchor="w", pady=(4, 0))
|
||
tk.Label(z, text="(leer bzw. „Noch nicht abgerechnet“ = alle offenen Positionen; Datum als TT.MM.JJJJ)",
|
||
fg=w.SHADOW, anchor="w", justify="left", wraplength=420).pack(fill="x")
|
||
self.v_kennz = tk.BooleanVar(value=True)
|
||
tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch "
|
||
"einmal abgerechnet)", variable=self.v_kennz, anchor="w", justify="left",
|
||
wraplength=400).pack(fill="x", pady=(4, 0))
|
||
self.knopfleiste([("Exportieren", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 10))
|
||
|
||
def _vormonat(self):
|
||
von, bis = zr.vormonat()
|
||
self.v_von.set(von.strftime("%d.%m.%Y"))
|
||
self.v_bis.set(bis.strftime("%d.%m.%Y"))
|
||
|
||
def _klick(self, e):
|
||
if self.tree.identify_column(e.x) == "#0":
|
||
iid = self.tree.identify_row(e.y)
|
||
if iid:
|
||
self._toggle(int(iid))
|
||
|
||
def _toggle(self, hid):
|
||
self.haken[hid] = not self.haken[hid]
|
||
self.tree.item(str(hid), text="[x]" if self.haken[hid] else "[ ]")
|
||
|
||
def _alle(self, an):
|
||
for hid in self.haken:
|
||
self.haken[hid] = an
|
||
self.tree.item(str(hid), text="[x]" if an else "[ ]")
|
||
|
||
def _ordner(self):
|
||
d = filedialog.askdirectory(parent=self, title="Zielordner wählen",
|
||
initialdir=self.v_ordner.get() or None)
|
||
if d:
|
||
self.v_ordner.set(d)
|
||
|
||
def _ok(self):
|
||
ids = [h for h, an in self.haken.items() if an]
|
||
if not ids:
|
||
w.warnung(self, "Export", "Bitte mindestens einen Händler anhaken.")
|
||
return
|
||
try:
|
||
von, bis = zr.datum_lesen(self.v_von.get()), zr.datum_lesen(self.v_bis.get())
|
||
except ValueError as exc:
|
||
w.warnung(self, "Zeitraum", str(exc))
|
||
return
|
||
basis = next(k for k, v in zr.BASEN.items() if v == self.v_basis.get())
|
||
if not self.v_ordner.get().strip():
|
||
self._ordner()
|
||
if not self.v_ordner.get().strip():
|
||
return
|
||
self.settings.update(export_ordner=self.v_ordner.get().strip(), export_anbieter_getrennt=self.v_anb.get(),
|
||
export_null_weglassen=self.v_null.get(), export_kopfzeile=self.v_kopf.get(),
|
||
export_summe=self.v_sum.get())
|
||
self.settings.pop("export_pro_anbieter", None)
|
||
self.result = dict(ids=ids, ordner=self.v_ordner.get().strip(), titel=self.v_titel.get().strip(),
|
||
pro_anbieter=self.v_anb.get(), null_weglassen=self.v_null.get(),
|
||
kopfzeile=self.v_kopf.get(), summe=self.v_sum.get(), oeffnen=self.v_auf.get(),
|
||
zeitraum=zr.Zeitraum(von, bis, basis) if (von or bis) else None,
|
||
kennzeichnen=self.v_kennz.get())
|
||
self.settings["export_basis"] = basis
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Einstellungen
|
||
|
||
class EinstellungenDialog(w.Dialog):
|
||
def __init__(self, master, settings: dict, db_pfad: str):
|
||
super().__init__(master, "Einstellungen")
|
||
self.settings = settings
|
||
g = w.gruppe(self, "Datenbank")
|
||
g.pack(fill="x", padx=10, pady=10)
|
||
tk.Label(g, text="Speicherort der Datenbank (Händler, Regeln, Abrechnungen):", anchor="w").grid(
|
||
row=0, column=0, columnspan=2, sticky="w")
|
||
self.v = tk.StringVar(value=db_pfad)
|
||
tk.Entry(g, textvariable=self.v, width=56).grid(row=1, column=0, sticky="we", pady=4)
|
||
w.knopf(g, "Ändern...", self._waehlen).grid(row=1, column=1, padx=(4, 0))
|
||
tk.Label(g, text="Tipp: Liegt die Datei auf einem Netzlaufwerk, können mehrere PCs dieselben\n"
|
||
"Händler nutzen – aber bitte nicht gleichzeitig damit arbeiten.",
|
||
justify="left", fg=w.SHADOW).grid(row=2, column=0, columnspan=2, sticky="w")
|
||
g2 = w.gruppe(self, "Darstellung")
|
||
g2.pack(fill="x", padx=10, pady=(0, 10))
|
||
self.v_design = tk.StringVar(value=settings.get("design", "modern"))
|
||
tk.Radiobutton(g2, text="Modern hell", variable=self.v_design, value="modern").pack(anchor="w")
|
||
tk.Radiobutton(g2, text="Modern dunkel", variable=self.v_design, value="dunkel").pack(anchor="w")
|
||
tk.Radiobutton(g2, text="Klassisch (Windows 95, Legacy)", variable=self.v_design, value="win95").pack(
|
||
anchor="w")
|
||
tk.Label(g2, text="Das Fenster wird nach OK mit dem neuen Design neu aufgebaut.", fg=w.SHADOW).pack(anchor="w")
|
||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 10))
|
||
|
||
def _waehlen(self):
|
||
p = filedialog.asksaveasfilename(parent=self, title="Datenbank wählen oder neu anlegen",
|
||
defaultextension=".db", confirmoverwrite=False,
|
||
filetypes=[("Datenbank", "*.db"), ("Alle Dateien", "*.*")])
|
||
if p:
|
||
self.v.set(p)
|
||
|
||
def _ok(self):
|
||
self.result = dict(pfad=self.v.get().strip(), design=self.v_design.get())
|
||
self.destroy()
|
||
|
||
|
||
# =========================================================================== Info
|
||
|
||
class InfoDialog(w.Dialog):
|
||
def __init__(self, master):
|
||
super().__init__(master, f"Info über {w.APP_NAME}")
|
||
top = tk.Frame(self, bg=w.FACE)
|
||
top.pack(padx=14, pady=14)
|
||
c = tk.Canvas(top, width=110, height=110, bg=w.TEAL, bd=2, relief="sunken", highlightthickness=0)
|
||
c.pack(side="left", anchor="n")
|
||
self._img = w.make_icon(self, "app", 6)
|
||
c.create_image(57, 57, image=self._img)
|
||
rechts = tk.Frame(top, bg=w.FACE)
|
||
rechts.pack(side="left", padx=(14, 0), anchor="n")
|
||
tk.Label(rechts, text=w.APP_NAME, font=w.FONT_BIG).pack(anchor="w")
|
||
tk.Label(rechts, text=f"Version {__version__}", anchor="w").pack(anchor="w")
|
||
w.trenner(rechts, vertikal=False).pack(fill="x", pady=8)
|
||
tk.Label(rechts, text=f"Lizenziert für:\n{APP_ORG}\n\nProvisionsabrechnung für Unterhändler\n"
|
||
"freenet · Vodafone", justify="left").pack(anchor="w")
|
||
w.trenner(rechts, vertikal=False).pack(fill="x", pady=8)
|
||
tk.Label(rechts, text="Arbeitsspeicher für Provisionen: reichlich", fg=w.SHADOW).pack(anchor="w")
|
||
self.knopfleiste([("OK", self.abbrechen)]).pack(pady=(0, 12))
|
||
self.bind("<Return>", lambda e: self.abbrechen())
|