Provisions-Manager 95: Import, Händler-DB, Auszahlungsregeln, Excel-Export, Win95-Oberfläche, Wine-Build
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
322d2ed1c3
25 files changed
+3366
No files matched your search
@@ -0,0 +1,633 @@
|
||||
"""Dialogfenster des Provisions-Managers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, ttk
|
||||
|
||||
from provtool import APP_NAME, APP_ORG, __version__
|
||||
from provtool.db import Datenbank, Haendler
|
||||
from provtool.gui import win95 as w
|
||||
from provtool.importer import ANBIETER
|
||||
from provtool.money import fmt, fmt_percent, parse_percent
|
||||
|
||||
|
||||
# =========================================================================== 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)
|
||||
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="Notiz:", anchor="nw").grid(row=4, column=0, sticky="nw", pady=2)
|
||||
self.t_notiz = tk.Text(g, width=36, height=3, font=w.FONT)
|
||||
self.t_notiz.grid(row=4, 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=4,
|
||||
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=200)
|
||||
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="freenet")
|
||||
cb = ttk.Combobox(kf, textvariable=self.v_kanb, values=list(ANBIETER.values()), width=10, state="readonly")
|
||||
cb.pack(side="left")
|
||||
self.v_kwert = tk.StringVar()
|
||||
ke = tk.Entry(kf, textvariable=self.v_kwert, width=18)
|
||||
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)
|
||||
|
||||
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=4, 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()
|
||||
self._kennungen = [neue_kennung]
|
||||
self._kennungen_zeigen()
|
||||
self.minsize(640, 460)
|
||||
|
||||
# ---- intern
|
||||
@staticmethod
|
||||
def _anbieter_key(anzeige: str) -> str:
|
||||
return next((k for k, v in ANBIETER.items() if v == anzeige), anzeige)
|
||||
|
||||
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_aktiv.set(h.aktiv)
|
||||
self.t_notiz.delete("1.0", "end")
|
||||
self.t_notiz.insert("1.0", h.notiz or "")
|
||||
self._kennungen = self.db.kennungen(h.id)
|
||||
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_aktiv.set(True)
|
||||
self.t_notiz.delete("1.0", "end")
|
||||
self._kennungen = []
|
||||
self._kennungen_zeigen()
|
||||
self._regeln_zeigen()
|
||||
|
||||
def _kennung_dazu(self):
|
||||
wert = self.v_kwert.get().strip()
|
||||
if not wert:
|
||||
return
|
||||
anb = self._anbieter_key(self.v_kanb.get())
|
||||
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
|
||||
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())
|
||||
try:
|
||||
hid = self.db.haendler_speichern(h, self._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)
|
||||
|
||||
|
||||
# =========================================================================== Abrechnungen
|
||||
|
||||
class AbrechnungenDialog(w.Dialog):
|
||||
def __init__(self, master, db: Datenbank, aktuelle_id: int | None):
|
||||
super().__init__(master, "Abrechnung öffnen", resizable=True)
|
||||
self.db = db
|
||||
tk.Label(self, text="Gespeicherte Abrechnungen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
||||
r, self.tree = w.scroll_tree(self, columns=("titel", "geaendert", "anzahl", "dateien"), show="headings",
|
||||
height=12, selectmode="browse")
|
||||
for c, t, b in (("titel", "Bezeichnung", 180), ("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)
|
||||
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():
|
||||
self.tree.insert("", "end", iid=str(a["id"]),
|
||||
values=(a["titel"], 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 _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):
|
||||
self.db.datei_entfernen(int(s[0]))
|
||||
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="browse")
|
||||
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, "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:
|
||||
self.result = tuple(s[0].split("\t", 1))
|
||||
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()
|
||||
|
||||
|
||||
# =========================================================================== 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
|
||||
f = tk.Frame(self, bg=w.FACE)
|
||||
f.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
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=8,
|
||||
selectmode="extended")
|
||||
self.tree.heading("#0", text="")
|
||||
self.tree.column("#0", width=30, 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(f, "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))
|
||||
self.v_anb = tk.BooleanVar(value=settings.get("export_pro_anbieter", 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)
|
||||
tk.Checkbutton(g, text="Pro Anbieter (freenet / Vodafone) eine eigene Datei",
|
||||
variable=self.v_anb).grid(row=2, column=0, columnspan=3, sticky="w")
|
||||
tk.Checkbutton(g, text="Kopfzeile mit Spaltennamen", variable=self.v_kopf).grid(row=3, column=0, columnspan=3,
|
||||
sticky="w")
|
||||
tk.Checkbutton(g, text="Summenzeile unter der Tabelle", variable=self.v_sum).grid(row=4, column=0,
|
||||
columnspan=3, sticky="w")
|
||||
tk.Checkbutton(g, text="Ordner nach dem Export öffnen", variable=self.v_auf).grid(row=5, column=0,
|
||||
columnspan=3, sticky="w")
|
||||
g.columnconfigure(1, weight=1)
|
||||
self.knopfleiste([("Exportieren", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 10))
|
||||
|
||||
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
|
||||
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_pro_anbieter=self.v_anb.get(),
|
||||
export_kopfzeile=self.v_kopf.get(), export_summe=self.v_sum.get())
|
||||
self.result = dict(ids=ids, ordner=self.v_ordner.get().strip(), titel=self.v_titel.get().strip(),
|
||||
pro_anbieter=self.v_anb.get(), kopfzeile=self.v_kopf.get(), summe=self.v_sum.get(),
|
||||
oeffnen=self.v_auf.get())
|
||||
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")
|
||||
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 = self.v.get().strip()
|
||||
self.destroy()
|
||||
|
||||
|
||||
# =========================================================================== Info
|
||||
|
||||
class InfoDialog(w.Dialog):
|
||||
def __init__(self, master):
|
||||
super().__init__(master, f"Info über {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=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())
|
||||
Reference in new issue
Block a user