4 Commits
Author SHA1 Message Date
Kerim AtilganandClaude Opus 5.5 c96cf1bd94 Test: Filter-Knopf direkt auslösen (Wine)
Build / linux (push) Successful in 1m51s
Build / windows-exe (push) Successful in 7m49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:14:02 +01:00
Kerim AtilganandClaude Opus 5.5 c35356aeb9 Win95-Design an das neue Hauptfenster angepasst, Version 1.6.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:04:14 +01:00
Kerim AtilganandClaude Opus 5.5 f296483fdc Neues Design im Stil der Handy-Insel-Webtools: Kopfzeile, Seitenleiste, Kennzahlen, Filter, hell/dunkel, IBM Plex
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:55:17 +01:00
Kerim AtilganandClaude Opus 5.5 2600eea574 Abrechnungen links im Hauptfenster (Klick öffnet), Import fragt nach der Ziel-Abrechnung
Build / linux (push) Successful in 1m50s
Build / windows-exe (push) Successful in 7m49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:35:16 +01:00
14 changed files with 777 additions and 165 deletions

No files matched your search

+7 -4
View File
@@ -76,7 +76,7 @@ Welche Provisionsart Airtime bzw. Jahresbonus ist, erkennt das Programm an den T
„AirTime Provision“, „Umsatzbeteiligung“, „Jahresbonus“); umstellbar je Provisionsart unter „AirTime Provision“, „Umsatzbeteiligung“, „Jahresbonus“); umstellbar je Provisionsart unter
*Händler ▸ Provisionsarten…*. *Händler ▸ Provisionsarten…*.
Das Hauptfenster ist unter Windows ein echtes Programmfenster mit Win95-Optik: an den Bildschirmrand Das Hauptfenster ist unter Windows ein echtes Programmfenster: an den Bildschirmrand
ziehen (Andocken), `Win`+Pfeiltasten, Doppelklick auf die Titelleiste und Ziehen am Rand funktionieren ziehen (Andocken), `Win`+Pfeiltasten, Doppelklick auf die Titelleiste und Ziehen am Rand funktionieren
wie gewohnt. wie gewohnt.
@@ -86,8 +86,10 @@ gekennzeichnet (grau) und nie wieder exportiert – auch nicht, wenn dieselbe Po
anderen Datei oder Abrechnung auftaucht. Versehentlich? Rechtsklick ▸ *Kennzeichnung „abgerechnet“ aufheben*. anderen Datei oder Abrechnung auftaucht. Versehentlich? Rechtsklick ▸ *Kennzeichnung „abgerechnet“ aufheben*.
*Datei ▸ Abrechnung erledigt / wieder öffnen* sperrt eine fertige Abrechnung gegen Änderungen. *Datei ▸ Abrechnung erledigt / wieder öffnen* sperrt eine fertige Abrechnung gegen Änderungen.
**Design:** Standard ist ein modernes, helles Design (wie Windows 11); das Windows-95-Design gibt es weiter **Design:** Standard ist das Design der Handy-Insel-Webtools (Lila, Schrift IBM Plex – liegt dem Programm bei):
unter *Datei ▸ Einstellungen ▸ Darstellung* (wirkt nach Neustart). Kopfzeile mit Menüs und Suche, links Abrechnungen und Händler, oben Kennzahlen, über der Tabelle Filter
(nicht zugeordnet, von Hand geändert, doppelt, bereits abgerechnet). Oben rechts „Dunkel“/„Hell“ schaltet
das dunkle Design um. Das Windows-95-Design gibt es weiter unter *Datei ▸ Einstellungen ▸ Darstellung*.
**Linux:** `Provisionsmanager95-linux-x86_64-<Version>.tar.gz` entpacken und `Provisionsmanager95` starten **Linux:** `Provisionsmanager95-linux-x86_64-<Version>.tar.gz` entpacken und `Provisionsmanager95` starten
(ab Debian 12 / Ubuntu 24.04). Daten liegen unter `~/.config/Provisionsmanager95`. (ab Debian 12 / Ubuntu 24.04). Daten liegen unter `~/.config/Provisionsmanager95`.
@@ -129,7 +131,8 @@ Aufbau:
| `provtool/db.py` | SQLite: Händler, Kennungen, Regeln, Abrechnungen, Positionen | | `provtool/db.py` | SQLite: Händler, Kennungen, Regeln, Abrechnungen, Positionen |
| `provtool/export.py` | Excel-Export im Zielformat | | `provtool/export.py` | Excel-Export im Zielformat |
| `provtool/money.py` | Beträge (intern immer in Cent) | | `provtool/money.py` | Beträge (intern immer in Cent) |
| `provtool/gui/` | Oberfläche (Tkinter im Win95-Stil) | | `provtool/gui/` | Oberfläche (Tkinter; Designs hell, dunkel, Win95) |
| `provtool/gui/schriften/` | IBM Plex (SIL Open Font License, siehe `OFL.txt`) |
| `provtool/gui/rahmen.py` | selbst gezeichnete Titelleiste, Menüleiste, Größenänderung | | `provtool/gui/rahmen.py` | selbst gezeichnete Titelleiste, Menüleiste, Größenänderung |
| `provtool/gui/assistent.py` | Einrichtungs-Assistent, Rundgang, Tipp des Tages | | `provtool/gui/assistent.py` | Einrichtungs-Assistent, Rundgang, Tipp des Tages |
+2 -1
View File
@@ -12,7 +12,8 @@ a = Analysis(
[os.path.join(ROOT, "build", "start.py")], [os.path.join(ROOT, "build", "start.py")],
pathex=[ROOT], pathex=[ROOT],
hiddenimports=["openpyxl", "xlrd"], hiddenimports=["openpyxl", "xlrd"],
datas=[(os.path.join(ROOT, "provtool", "gui", "bilder"), os.path.join("provtool", "gui", "bilder"))], datas=[(os.path.join(ROOT, "provtool", "gui", "bilder"), os.path.join("provtool", "gui", "bilder")),
(os.path.join(ROOT, "provtool", "gui", "schriften"), os.path.join("provtool", "gui", "schriften"))],
excludes=["numpy", "pandas", "PIL", "pytest", "matplotlib", "unittest", "pydoc_data"], excludes=["numpy", "pandas", "PIL", "pytest", "matplotlib", "unittest", "pydoc_data"],
noarchive=False, noarchive=False,
) )
+1 -1
View File
@@ -1,5 +1,5 @@
"""Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach.""" """Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach."""
__version__ = "1.5.0" __version__ = "1.6.0"
APP_NAME = "Provisions-Manager 95" APP_NAME = "Provisions-Manager 95"
APP_ORG = "Handy-Insel Miesbach" APP_ORG = "Handy-Insel Miesbach"
+311 -96
View File
@@ -82,9 +82,13 @@ class App:
self.rahmen = rh.Hauptrahmen(root, w.APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch) self.rahmen = rh.Hauptrahmen(root, w.APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch)
self.rahmen.set_titel(w.APP_NAME) self.rahmen.set_titel(w.APP_NAME)
self.inhalt = self.rahmen.inhalt self.inhalt = self.rahmen.inhalt
if w.DUNKEL:
from provtool.gui import win32
win32.dunkle_titelleiste(root)
self._menue() self.neustart = False # Design umgeschaltet: main() baut das Fenster neu auf
self._toolbar() self.filter = "alle"
self._kopf()
self._body() self._body()
self.status = w.Statusleiste(self.inhalt, [("info", 0), ("pos", 16), ("orig", 22), ("ausz", 22)]) self.status = w.Statusleiste(self.inhalt, [("info", 0), ("pos", 16), ("orig", 22), ("ausz", 22)])
griff = self.rahmen.griff(self.status) griff = self.rahmen.griff(self.status)
@@ -107,9 +111,52 @@ class App:
root.after(500, self.tipp_des_tages) root.after(500, self.tipp_des_tages)
# ================================================================= Aufbau # ================================================================= Aufbau
def _menue(self): def _kopf(self):
self.menueleiste = mn.Menueleiste(self.inhalt, self.root) """Kopfzeile wie in den Webtools: Logo, Menüs, rechts Suche (modern auch Hell/Dunkel)."""
self.menueleiste.pack(side="top", fill="x") kopf = tk.Frame(self.inhalt, bg=w.FACE)
kopf.pack(side="top", fill="x")
if w.MODERN:
w.trenner(self.inhalt, vertikal=False).pack(side="top", fill="x")
innen = tk.Frame(kopf, bg=w.FACE)
innen.pack(fill="x", padx=(18, 14) if w.MODERN else 0, pady=10 if w.MODERN else 0)
if w.MODERN:
w.logo(innen).pack(side="left")
namen = tk.Frame(innen, bg=w.FACE)
namen.pack(side="left", padx=(10, 22))
tk.Label(namen, text=w.APP_NAME, font=(w.FONT_FAMILY, w.FONT_SIZE + 1, "bold")).pack(anchor="w")
tk.Label(namen, text="Handy-Insel", fg=w.SHADOW, font=(w.MONO_FAMILY, w.FONT_SIZE - 2)).pack(anchor="w")
self._menue(innen)
if w.MODERN:
self.b_design = w.knopf(innen, "Hell" if w.DUNKEL else "Dunkel", self.design_umschalten)
self.b_design.pack(side="right", padx=(8, 0))
w.Tooltip(self.b_design, "Zwischen hellem und dunklem Design wechseln")
self.v_suche = tk.StringVar()
if w.MODERN: # Suchfeld mit Rahmen, Hinweis und Tastenkürzel
sf = tk.Frame(innen, bg=w.WHITE, highlightthickness=1, highlightbackground=w.RAND2,
highlightcolor=w.NAVY)
sf.pack(side="right")
tk.Label(sf, text="Suchen", bg=w.WHITE, fg=w.SHADOW).pack(side="left", padx=(10, 2))
e = tk.Entry(sf, textvariable=self.v_suche, width=28, highlightthickness=0, bd=4)
e.pack(side="left")
tk.Label(sf, text="Strg F", bg=w.WHITE, fg=w.SHADOW, font=(w.MONO_FAMILY, w.FONT_SIZE - 2)).pack(
side="left", padx=(2, 10))
e.bind("<FocusIn>", lambda _e: sf.configure(highlightbackground=w.NAVY), add="+")
e.bind("<FocusOut>", lambda _e: sf.configure(highlightbackground=w.RAND2), add="+")
else:
sf = tk.Frame(innen, bg=w.FACE)
sf.pack(side="right", padx=8)
tk.Label(sf, text="Suchen:").pack(side="left")
e = tk.Entry(sf, textvariable=self.v_suche, width=22)
e.pack(side="left", padx=4)
self._suche_geplant = None
self.v_suche.trace_add("write", lambda *_: self._suche_planen())
self._suchfeld = e
self._suchrahmen = sf
def _menue(self, master):
self.menueleiste = mn.Menueleiste(master, self.root)
self.menueleiste.pack(side="left")
d = self.menueleiste.menue("Datei") d = self.menueleiste.menue("Datei")
d.add_command(label="Neue Abrechnung...", accelerator="Strg+N", command=self.neu) d.add_command(label="Neue Abrechnung...", accelerator="Strg+N", command=self.neu)
d.add_command(label="Abrechnung öffnen...", accelerator="Strg+O", command=self.abrechnung_waehlen) d.add_command(label="Abrechnung öffnen...", accelerator="Strg+O", command=self.abrechnung_waehlen)
@@ -144,7 +191,8 @@ class App:
h.add_command(label="Provisionsarten (Airtime / Jahresbonus)...", command=self.provisionsarten) h.add_command(label="Provisionsarten (Airtime / Jahresbonus)...", command=self.provisionsarten)
h.add_command(label="Standard-Staffel Jahresbonus...", command=self.standard_staffel) h.add_command(label="Standard-Staffel Jahresbonus...", command=self.standard_staffel)
hi = self.menueleiste.menue("?", underline=None) self.hilfe_menue = "Hilfe" if w.MODERN else "?"
hi = self.menueleiste.menue(self.hilfe_menue, underline=None)
hi.add_command(label="Kurzanleitung", accelerator="F1", command=self.hilfe) hi.add_command(label="Kurzanleitung", accelerator="F1", command=self.hilfe)
hi.add_command(label="Rundgang durch das Programm", command=self.rundgang) hi.add_command(label="Rundgang durch das Programm", command=self.rundgang)
hi.add_command(label="Tipp des Tages...", command=self.tipp_des_tages) hi.add_command(label="Tipp des Tages...", command=self.tipp_des_tages)
@@ -153,101 +201,183 @@ class App:
hi.add_separator() hi.add_separator()
hi.add_command(label=f"Info über {w.APP_NAME}...", command=lambda: dlg.InfoDialog(self.root).zeigen()) hi.add_command(label=f"Info über {w.APP_NAME}...", command=lambda: dlg.InfoDialog(self.root).zeigen())
def _toolbar(self): FILTER = (("alle", "Alle"), ("unzugeordnet", "Nicht zugeordnet"), ("manuell", "Von Hand geändert"),
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=0 if w.MODERN else 1, relief="raised") ("doppelt", "Doppelt"), ("abgerechnet", "Bereits abgerechnet"))
self.tb: dict[str, w.ToolbarButton] = {}
tb.pack(side="top", fill="x")
for text, icon, cmd, tip in (
("Neu", "neu", self.neu, "Neue Abrechnung anlegen (Strg+N)"),
("Öffnen", "oeffnen", self.abrechnung_waehlen, "Gespeicherte Abrechnung öffnen (Strg+O)"),
(None, None, None, None),
("Import", "import", self.importieren, "CSV-/Excel-Dateien einlesen (Strg+I)"),
("Export", "export", self.exportieren, "Händler-Abrechnungen als Excel speichern (Strg+E)"),
(None, None, None, None),
("Händler", "haendler", self.haendler, "Händler und Kennungen verwalten (Strg+H)"),
("Prozent", "prozent", self.prozent, "Auszahlung für Auswahl in Prozent festlegen (F4)"),
("Betrag", "rechner", self.betrag_bearbeiten, "Betrag der Position ändern (F2)"),
):
if text is None:
w.trenner(tb).pack(side="left", fill="y", padx=4, pady=3)
continue
self.tb[text] = w.ToolbarButton(tb, text, self.icons(icon), cmd, tip)
self.tb[text].pack(side="left", padx=1, pady=1)
self.v_suche = tk.StringVar()
sf = tk.Frame(tb, bg=w.FACE)
sf.pack(side="right", padx=8)
tk.Label(sf, text="Suchen:").pack(side="left")
e = tk.Entry(sf, textvariable=self.v_suche, width=22)
e.pack(side="left", padx=4)
self._suche_geplant = None
self.v_suche.trace_add("write", lambda *_: self._suche_planen())
self._suchfeld = e
self._suchrahmen = sf
def _body(self): def _body(self):
desk = tk.Frame(self.inhalt, bg=w.TEAL) m = w.MODERN
desk = tk.Frame(self.inhalt, bg=w.BG)
desk.pack(side="top", fill="both", expand=True) desk.pack(side="top", fill="both", expand=True)
# Trennlinie zwischen den Spalten: grau und erhaben wie ein Win95-Splitter, mit der Maus ziehbar. # Trennlinie zwischen den Spalten, mit der Maus ziehbar. Beim Ziehen wandert nur eine Linie mit;
# Wie im Win95-Explorer wandert beim Ziehen nur eine Linie mit; neu aufgebaut wird erst beim # neu aufgebaut wird erst beim Loslassen (flüssig, auch mit vielen Positionen).
# Loslassen (flüssig, auch mit vielen Positionen). pw = self.pw = tk.PanedWindow(desk, orient="horizontal", bg=w.BG, sashwidth=6 if m else 7,
pw = self.pw = tk.PanedWindow(desk, orient="horizontal", bg=w.FACE, sashwidth=7, sashrelief="flat" if m else "raised",
sashrelief="flat" if w.MODERN else "raised",
bd=0, opaqueresize=False, sashcursor="sb_h_double_arrow") bd=0, opaqueresize=False, sashcursor="sb_h_double_arrow")
pw.pack(fill="both", expand=True, padx=6, pady=6) pw.pack(fill="both", expand=True, padx=0 if m else 6, pady=0 if m else 6)
# --- Händler-Fenster # ---------------- Seitenleiste: Abrechnungen und Händler
self.f_h = w.Fenster(pw, "Händler", self.icons("haendler")) # Win95: zwei eigene Flächen mit blauer Titelleiste auf dem Desktop
r, self.htree = w.scroll_tree(self.f_h.inhalt, columns=("anzahl", "summe"), show="tree headings", links = tk.Frame(pw, bg=w.FACE if m else w.BG)
selectmode="browse") innen = tk.Frame(links, bg=w.FACE if m else w.BG)
self.htree.heading("#0", text="Händler", anchor="w") innen.pack(fill="both", expand=True, padx=12 if m else 0, pady=(16, 10) if m else 0)
self.htree.heading("anzahl", text="Pos.") seite = dict(style="Seite.Treeview") if m else {}
self.htree.heading("summe", text="Auszahlung") rand = {} if m else dict(padx=2, pady=(0, 2)) # Abstand der Liste zum Win95-Rahmen
self.htree.column("#0", width=150, minwidth=80, stretch=False)
self.htree.column("anzahl", width=38, anchor="e", stretch=False) self.f_a = tk.Frame(innen, bg=w.FACE) if m else w.Karte(innen)
self.htree.column("summe", width=84, anchor="e", stretch=False) self.f_a.pack(fill="x", pady=(0, 16 if m else 6))
zeile = self._abschnitt_zeile(self.f_a, "Abrechnungen")
self.b_neu = w.knopf(zeile, "+ Neu", self.neu, padx=8, pady=0, **self._link())
self.b_neu.pack(side="right", padx=0 if m else 1)
w.Tooltip(self.b_neu, "Neue Abrechnung anlegen (Strg+N)")
r, self.atree = w.scroll_tree(self.f_a, columns=("status",), show="tree", height=5, selectmode="browse",
auto=True, **seite)
self.atree.column("#0", width=150, minwidth=80, stretch=False)
self.atree.column("status", width=110, anchor="e", stretch=False)
w.auto_breite(self.atree, {"#0": 1})
self.atree.tag_configure("leise", foreground=w.SHADOW)
self._punkte = {f: w.punkt(self.root, getattr(w, f)) for f in ("BLAU", "GOLD", "GRUEN", "ROT", "SHADOW")}
r.pack(fill="both", expand=True, **rand)
self.atree.bind("<<TreeviewSelect>>", self._abrechnung_gewaehlt)
self.f_h = tk.Frame(innen, bg=w.FACE) if m else w.Karte(innen)
self.f_h.pack(fill="both", expand=True)
zeile = self._abschnitt_zeile(self.f_h, "Händler")
self.b_haendler = w.knopf(zeile, "Verwalten", self.haendler, padx=8, pady=0, **self._link())
self.b_haendler.pack(side="right", padx=0 if m else 1)
w.Tooltip(self.b_haendler, "Händler und Kennungen verwalten (Strg+H)")
r, self.htree = w.scroll_tree(self.f_h, columns=("anzahl", "summe"), show="tree", selectmode="browse",
auto=True, **seite)
self.htree.column("#0", width=140, minwidth=80, stretch=False)
self.htree.column("anzahl", width=40, anchor="e", stretch=False)
self.htree.column("summe", width=96, anchor="e", stretch=False)
w.auto_breite(self.htree, {"#0": 1}) w.auto_breite(self.htree, {"#0": 1})
self.htree.tag_configure("grau", foreground=w.SHADOW) self.htree.tag_configure("grau", foreground=w.SHADOW)
self.htree.tag_configure("fett", font=w.FONT_BOLD) self.htree.tag_configure("fett", font=w.FONT_BOLD)
r.pack(fill="both", expand=True) r.pack(fill="both", expand=True, **rand)
self.htree.bind("<<TreeviewSelect>>", self._haendler_gewaehlt) self.htree.bind("<<TreeviewSelect>>", self._haendler_gewaehlt)
self.htree.bind("<Double-1>", lambda e: self._haendler_doppelklick()) self.htree.bind("<Double-1>", lambda e: self._haendler_doppelklick())
pw.add(self.f_h, minsize=240, width=310) pw.add(links, minsize=240, width=320)
# ---------------- Inhalt: Kopf der Abrechnung, Kennzahlen, Positionen
rechts = tk.Frame(pw, bg=w.BG)
innen = tk.Frame(rechts, bg=w.BG)
innen.pack(fill="both", expand=True, padx=(16, 20) if m else 0, pady=(16, 0) if m else 0)
kopf = tk.Frame(innen, bg=w.BG)
kopf.pack(fill="x", pady=(0, 14 if m else 6))
texte = tk.Frame(kopf, bg=w.BG)
texte.pack(side="left", fill="x", expand=True)
self.l_status = w.abschnitt(texte, "", bg=w.BG)
self.l_status.pack(anchor="w")
self.l_titel = tk.Label(texte, text="", bg=w.BG, font=w.FONT_TITEL, anchor="w")
self.l_titel.pack(anchor="w")
self.l_info = tk.Label(texte, text="", bg=w.BG, fg=w.TEXT2, anchor="w")
self.l_info.pack(anchor="w")
if not m: # Win95: weiße Schrift auf dem Desktop, wie die Symbolbeschriftungen
for lbl in (self.l_status, self.l_titel, self.l_info):
lbl.configure(fg=w.WHITE)
self.b_export = w.knopf(kopf, "Exportieren", self.exportieren, primaer=True)
self.b_export.pack(side="right", anchor="s")
w.Tooltip(self.b_export, "Händler-Abrechnungen als Excel speichern (Strg+E)")
self.b_import = w.knopf(kopf, "Importieren", self.importieren)
self.b_import.pack(side="right", anchor="s", padx=8)
w.Tooltip(self.b_import, "CSV-/Excel-Dateien einlesen (Strg+I)")
self.b_erledigt = w.knopf(kopf, "Als erledigt markieren", self.erledigt_umschalten)
self.b_erledigt.pack(side="right", anchor="s")
self.tb = {"Neu": self.b_neu, "Import": self.b_import, "Export": self.b_export, "Händler": self.b_haendler}
kacheln = tk.Frame(innen, bg=w.BG)
kacheln.pack(fill="x", pady=(0, 14 if m else 6))
self.kpi: dict[str, tuple[tk.Label, tk.Label]] = {}
for i, (key, titel) in enumerate((("pos", "Positionen"), ("orig", "Provision"), ("ausz", "Auszahlung"),
("air", "Airtime"))):
k = w.Karte(kacheln)
k.grid(row=0, column=i, sticky="nsew", padx=(0 if i == 0 else 7, 0 if i == 3 else 7))
kacheln.columnconfigure(i, weight=1, uniform="kpi")
ki = tk.Frame(k, bg=w.FACE)
ki.pack(fill="both", expand=True, padx=16 if m else 8, pady=12 if m else 6)
w.abschnitt(ki, titel).pack(anchor="w")
farbe = {"ausz": w.NAVY, "air": w.GRUEN}.get(key, w.DARK)
wert = tk.Label(ki, text="", font=w.FONT_ZAHL, fg=farbe, anchor="w")
wert.pack(anchor="w", pady=(4, 0))
info = tk.Label(ki, text="", fg=w.TEXT2, anchor="w")
info.pack(anchor="w")
self.kpi[key] = (wert, info)
self.f_p = w.Karte(innen)
self.f_p.pack(fill="both", expand=True, pady=(0, 12 if m else 0))
if not m: # Win95: Titel in der blauen Leiste, Filter darunter wie eine Werkzeugleiste
_, self.l_ptitel = w.titelleiste(self.f_p, "Positionen")
kopfzeile = tk.Frame(self.f_p, bg=w.FACE)
kopfzeile.pack(fill="x", padx=14 if m else 4, pady=10 if m else 4)
if m:
self.l_ptitel = tk.Label(kopfzeile, text="Positionen", font=(w.FONT_FAMILY, w.FONT_SIZE + 1, "bold"))
self.l_ptitel.pack(side="left", padx=(0, 12))
self.pills: dict[str, w.Pill] = {}
for key, text in self.FILTER:
self.pills[key] = w.Pill(kopfzeile, text, lambda k=key: self._filter_setzen(k), aktiv=key == self.filter)
self.pills[key].pack(side="left", padx=(0, 6))
self.l_auswahl = tk.Label(kopfzeile, text="", fg=w.TEXT2)
self.l_auswahl.pack(side="right")
if m:
w.trenner(self.f_p, vertikal=False).pack(fill="x")
# --- Positionen-Fenster
self.f_p = w.Fenster(pw, "Positionen", self.icons("neu"))
cols = ("datum", "rufnummer", "tarif", "zeitraum", "kennung", "orig", "proz", "ausz") cols = ("datum", "rufnummer", "tarif", "zeitraum", "kennung", "orig", "proz", "ausz")
r2, self.ptree = w.scroll_tree(self.f_p.inhalt, columns=cols, show="tree headings", selectmode="extended") r2, self.ptree = w.scroll_tree(self.f_p, columns=cols, show="tree headings", selectmode="extended")
self.ptree.heading("#0", text="Provisionsart / Kunde", anchor="w") if m:
r2.configure(highlightthickness=0)
kopf_text = (lambda t: t.upper()) if m else (lambda t: t)
self.ptree.heading("#0", text=kopf_text("Provisionsart / Kunde"), anchor="w")
self.ptree.column("#0", width=200, minwidth=140, stretch=False) self.ptree.column("#0", width=200, minwidth=140, stretch=False)
for c, t, b, a in (("datum", "Datum", 76, "w"), ("rufnummer", "Rufnummer", 100, "w"), for c, t, b, a in (("datum", "Datum", 86, "w"), ("rufnummer", "Rufnummer", 112, "w"),
("tarif", "Tarif", 120, "w"), ("zeitraum", "Zeitraum", 84, "w"), ("tarif", "Tarif", 120, "w"), ("zeitraum", "Zeitraum", 94, "w"),
("kennung", "Kennung", 70, "w"), ("kennung", "Kennung", 80, "w"),
("orig", "Provision", 78, "e"), ("proz", "%", 52, "e"), ("ausz", "Auszahlung", 84, "e")): ("orig", "Provision", 100, "e"), ("proz", "%", 64, "e"), ("ausz", "Auszahlung", 112, "e")):
self.ptree.heading(c, text=t, anchor=a) self.ptree.heading(c, text=kopf_text(t), anchor=a)
self.ptree.column(c, width=b, minwidth=80 if c == "tarif" else 30, anchor=a, stretch=False) self.ptree.column(c, width=b, minwidth=80 if c == "tarif" else 30, anchor=a, stretch=False)
w.auto_breite(self.ptree, {"#0": 0.6, "tarif": 0.4}) w.auto_breite(self.ptree, {"#0": 0.6, "tarif": 0.4})
self.ptree.tag_configure("gruppe", background=w.LIGHT, font=w.FONT_BOLD) self.ptree.tag_configure("gruppe", background=w.LIGHT, font=w.FONT_BOLD)
self.ptree.tag_configure("manuell", foreground="#0000c0") self.ptree.tag_configure("zebra", background=w.ZEBRA)
self.ptree.tag_configure("abw", foreground="#a00000") self.ptree.tag_configure("manuell", foreground=w.MANUELL)
self.ptree.tag_configure("negativ", foreground="#a00000") self.ptree.tag_configure("abw", foreground=w.ROT)
self.ptree.tag_configure("negativ", foreground=w.ROT)
self.ptree.tag_configure("abgerechnet", foreground=w.SHADOW) self.ptree.tag_configure("abgerechnet", foreground=w.SHADOW)
r2.pack(fill="both", expand=True) r2.pack(fill="both", expand=True)
self.ptree.bind("<Double-1>", self._doppelklick) self.ptree.bind("<Double-1>", self._doppelklick)
self.ptree.bind("<Button-3>", self._kontextmenue) self.ptree.bind("<Button-3>", self._kontextmenue)
self.ptree.bind("<<TreeviewSelect>>", lambda e: self._status_auswahl()) self.ptree.bind("<<TreeviewSelect>>", lambda e: self._status_auswahl())
leiste = tk.Frame(self.f_p.inhalt, bg=w.FACE) leiste = tk.Frame(self.f_p, bg=w.FACE)
leiste.pack(fill="x", pady=(4, 0)) leiste.pack(fill="x", padx=14 if m else 2, pady=10 if m else (4, 2))
self.b_prozent = w.knopf(leiste, "Prozent...", self.prozent, self.icons("prozent")) self.b_prozent = w.knopf(leiste, "Prozent...", self.prozent, None if m else self.icons("prozent"))
self.b_prozent.pack(side="left") self.b_prozent.pack(side="left")
w.knopf(leiste, "Betrag ändern...", self.betrag_bearbeiten, self.icons("rechner")).pack(side="left", padx=4) w.knopf(leiste, "Betrag ändern...", self.betrag_bearbeiten,
None if m else self.icons("rechner")).pack(side="left", padx=6)
w.knopf(leiste, "Zurücksetzen", self.zuruecksetzen).pack(side="left") w.knopf(leiste, "Zurücksetzen", self.zuruecksetzen).pack(side="left")
self.v_alle_auf = tk.BooleanVar(value=True)
w.knopf(leiste, "Alle zuklappen", lambda: self._klappen(False)).pack(side="right") w.knopf(leiste, "Alle zuklappen", lambda: self._klappen(False)).pack(side="right")
w.knopf(leiste, "Alle aufklappen", lambda: self._klappen(True)).pack(side="right", padx=4) w.knopf(leiste, "Alle aufklappen", lambda: self._klappen(True)).pack(side="right", padx=6)
tk.Label(leiste, text="Blau = von Hand · Rot = abweichend · Grau = abgerechnet", fg=w.SHADOW).pack( pw.add(rechts, minsize=560)
side="left", padx=8)
pw.add(self.f_p, minsize=500) @staticmethod
def _abschnitt_zeile(master, titel: str) -> tk.Frame:
"""Überschrift eines Abschnitts der Seitenleiste; Win95 als blaue Titelleiste. Knöpfe kommen rechts hinein."""
if not w.MODERN:
return w.titelleiste(master, titel)[0]
zeile = tk.Frame(master, bg=w.FACE)
zeile.pack(fill="x", pady=(0, 4))
w.abschnitt(zeile, titel).pack(side="left", padx=(8, 0))
return zeile
@staticmethod
def _link() -> dict:
"""Kleine Textknöpfe in der Seitenleiste (modern ohne Rahmen, in Akzentfarbe)."""
return dict(fg=w.NAVY, highlightthickness=0, font=w.FONT_BOLD) if w.MODERN else {}
def _filter_setzen(self, key: str):
self.filter = key
for k, pill in self.pills.items():
pill.setzen(k == key)
self._tabelle()
def _tasten(self): def _tasten(self):
r = self.root r = self.root
@@ -284,9 +414,46 @@ class App:
self.positionen = [] self.positionen = []
self.rahmen.set_titel(w.APP_NAME) self.rahmen.set_titel(w.APP_NAME)
self.status.set("info", " Keine Abrechnung geöffnet – Datei > Neue Abrechnung oder Import") self.status.set("info", " Keine Abrechnung geöffnet – Datei > Neue Abrechnung oder Import")
self._abrechnungsliste()
self._seitenkopf()
self._haendlerliste(behalte_auswahl) self._haendlerliste(behalte_auswahl)
self._tabelle() self._tabelle()
def _seitenkopf(self):
"""Titel, Status und Knöpfe über den Kennzahlen."""
a = self.db.abrechnung(self.aid) if self.aid else None
if not a:
self.l_status.configure(text="Keine Abrechnung geöffnet".upper() if w.MODERN else "Keine Abrechnung")
self.l_titel.configure(text="Willkommen")
self.l_info.configure(text="Links „+ Neu“ anlegen oder gleich Dateien importieren.")
self.b_erledigt.configure(text="Als erledigt markieren", state="disabled")
return
erledigt = self.db.erledigt(self.aid)
dateien = self.db.dateien(self.aid)
status = "erledigt" if erledigt else "offen"
self.l_status.configure(text=f"Abrechnung · {status}".upper() if w.MODERN else f"Abrechnung ({status})")
self.l_titel.configure(text=a["titel"])
self.l_info.configure(text=f"{len(dateien)} {'Datei' if len(dateien) == 1 else 'Dateien'} importiert · "
f"{sum(d['anzahl'] for d in dateien)} Zeilen" if dateien else "Noch nichts importiert")
self.b_erledigt.configure(text="Wieder öffnen" if erledigt else "Als erledigt markieren", state="normal")
def _abrechnungsliste(self):
t = self.atree
t.delete(*t.get_children())
for a in self.db.abrechnungen():
status, farbe = (("erledigt", "GRUEN") if a["erledigt"] else
(f"{a['abgerechnet']} abgerechnet", "GOLD") if a["abgerechnet"] else ("offen", "BLAU"))
t.insert("", "end", iid=str(a["id"]), text=" " + a["titel"], values=(status,), image=self._punkte[farbe])
if self.aid and t.exists(str(self.aid)):
t.selection_set(str(self.aid))
t.see(str(self.aid))
def _abrechnung_gewaehlt(self, _e=None):
sel = self.atree.selection()
if sel and int(sel[0]) != self.aid:
self.ansicht = ALLE
self.oeffnen(int(sel[0]))
def _suche_planen(self): def _suche_planen(self):
"""Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen).""" """Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen)."""
if self._suche_geplant is not None: if self._suche_geplant is not None:
@@ -366,6 +533,10 @@ class App:
pos = [p for p in pos if such in " ".join(str(p.get(k) or "") for k in ( pos = [p for p in pos if such in " ".join(str(p.get(k) or "") for k in (
"rufnummer", "vorname", "name", "tarif", "provisionsart", "kennung", "zeitraum", "aktionscode", "rufnummer", "vorname", "name", "tarif", "provisionsart", "kennung", "zeitraum", "aktionscode",
"imei")).lower()] "imei")).lower()]
pruef = {"unzugeordnet": lambda p: p["haendler_id"] == UNZUGEORDNET, "manuell": lambda p: p["manuell"],
"doppelt": lambda p: p["doppelt"], "abgerechnet": lambda p: p["abgerechnet"]}.get(self.filter)
if pruef:
pos = [p for p in pos if pruef(p)]
gruppen: dict[str, list[dict]] = defaultdict(list) gruppen: dict[str, list[dict]] = defaultdict(list)
for p in exp.sortiert(pos): for p in exp.sortiert(pos):
gruppen[self._gruppe_von(p)].append(p) gruppen[self._gruppe_von(p)].append(p)
@@ -382,8 +553,8 @@ class App:
pz = fmt_percent(next(iter(proz))) if len(proz) == 1 and None not in proz else ("gemischt" if so else "") pz = fmt_percent(next(iter(proz))) if len(proz) == 1 and None not in proz else ("gemischt" if so else "")
t.insert("", "end", iid=giid, text=f"{gname} ({len(plist)})", open=(war_leer or giid in offen or bool(such)), t.insert("", "end", iid=giid, text=f"{gname} ({len(plist)})", open=(war_leer or giid in offen or bool(such)),
values=("", "", "", "", "", fmt(so), pz, fmt(sa)), tags=("gruppe",)) values=("", "", "", "", "", fmt(so), pz, fmt(sa)), tags=("gruppe",))
for p in plist: for i, p in enumerate(plist):
tags = [] tags = ["zebra"] if i % 2 else []
if p["abgerechnet"] or p["doppelt"]: if p["abgerechnet"] or p["doppelt"]:
tags.append("abgerechnet") tags.append("abgerechnet")
elif p["manuell"]: elif p["manuell"]:
@@ -402,17 +573,30 @@ class App:
name = {ALLE: "Alle Positionen", UNZUGEORDNET: "Nicht zugeordnet", name = {ALLE: "Alle Positionen", UNZUGEORDNET: "Nicht zugeordnet",
EIGEN: "Eigene Kennungen (ausgeblendet, werden nicht exportiert)"}.get( EIGEN: "Eigene Kennungen (ausgeblendet, werden nicht exportiert)"}.get(
self.ansicht, self.haendler_namen.get(self.ansicht, "")) self.ansicht, self.haendler_namen.get(self.ansicht, ""))
self.f_p.set_titel(f"Positionen – {name}") self.l_ptitel.configure(text=f"Positionen – {name}")
so = sum(p["betrag_orig"] for p in pos) so = sum(p["betrag_orig"] for p in pos)
sa = sum(p["betrag"] for p in pos) sa = sum(p["betrag"] for p in pos)
air = [p for p in pos if p.get("typ") == regeln.AIRTIME]
self.status.set("pos", f" {len(pos)} Positionen") self.status.set("pos", f" {len(pos)} Positionen")
self.status.set("orig", f" Provision: {fmt(so)}") self.status.set("orig", f" Provision: {fmt(so)}")
self.status.set("ausz", f" Auszahlung: {fmt(sa)}") self.status.set("ausz", f" Auszahlung: {fmt(sa)}")
self._kennzahl("pos", str(len(pos)), name)
self._kennzahl("orig", fmt(so), "laut Anbieter")
self._kennzahl("ausz", fmt(sa), f"{round(sa * 100 / so)} % im Schnitt" if so > 0 else "")
self._kennzahl("air", fmt(sum(p["betrag"] for p in air)),
(f"{len(air)} Posten · eigene Excel-Datei" if any(p["betrag"] for p in air) else f"{len(air)} Posten")
if air else "keine Airtime")
self.l_auswahl.configure(text="")
def _kennzahl(self, key: str, wert: str, info: str):
self.kpi[key][0].configure(text=wert)
self.kpi[key][1].configure(text=info)
def _status_auswahl(self): def _status_auswahl(self):
pids = self._ausgewaehlte_pids() pids = self._ausgewaehlte_pids()
if len(pids) > 1 or (pids and any(i.startswith("g") for i in self.ptree.selection())): if len(pids) > 1 or (pids and any(i.startswith("g") for i in self.ptree.selection())):
ps = [p for p in self.positionen if p["id"] in set(pids)] ps = [p for p in self.positionen if p["id"] in set(pids)]
self.l_auswahl.configure(text=f"{len(ps)} ausgewählt · {fmt(sum(p['betrag'] for p in ps))}")
self.status.set("pos", f" {len(ps)} ausgewählt") self.status.set("pos", f" {len(ps)} ausgewählt")
self.status.set("orig", f" Provision: {fmt(sum(p['betrag_orig'] for p in ps))}") self.status.set("orig", f" Provision: {fmt(sum(p['betrag_orig'] for p in ps))}")
self.status.set("ausz", f" Auszahlung: {fmt(sum(p['betrag'] for p in ps))}") self.status.set("ausz", f" Auszahlung: {fmt(sum(p['betrag'] for p in ps))}")
@@ -466,7 +650,7 @@ class App:
else: else:
self._aktualisieren() self._aktualisieren()
def importieren(self, dateien: list[str] | None = None): def importieren(self, dateien: list[str] | None = None, ziel: int | None = None):
if not dateien: if not dateien:
dateien = list(filedialog.askopenfilenames( dateien = list(filedialog.askopenfilenames(
parent=self.root, title="Provisionsabrechnungen importieren", parent=self.root, title="Provisionsabrechnungen importieren",
@@ -477,7 +661,22 @@ class App:
if not dateien: if not dateien:
return return
self.settings["import_ordner"] = str(Path(dateien[0]).parent) self.settings["import_ordner"] = str(Path(dateien[0]).parent)
if not self._brauche_abrechnung() or self._gesperrt(): if ziel is None: # jedes Mal fragen, in welche Abrechnung
if not self.db.abrechnungen():
if not self.neu():
return
else:
res = dlg.AbrechnungenDialog(self.root, self.db, self.aid, fuer_import=True).zeigen()
if not res:
return
if res[0] == "neu":
if not self.neu():
return
else:
self.aid, self.ansicht = res[1], ALLE
else:
self.aid = ziel
if self._gesperrt():
return return
bericht = [] bericht = []
self.root.configure(cursor="watch") self.root.configure(cursor="watch")
@@ -968,15 +1167,22 @@ class App:
res = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen() res = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen()
if not res: if not res:
return return
if res["design"] != w.DESIGN:
self.settings["design"] = res["design"]
config.save(self.settings)
w.info(self.root, "Einstellungen", "Das neue Design wird beim nächsten Programmstart wirksam.")
if res["pfad"] and Path(res["pfad"]) != self.db.path: if res["pfad"] and Path(res["pfad"]) != self.db.path:
try: try:
self.db_wechseln(res["pfad"]) self.db_wechseln(res["pfad"])
except Exception as exc: except Exception as exc:
w.fehler(self.root, "Einstellungen", f"Datenbank konnte nicht geöffnet werden:\n{exc}") w.fehler(self.root, "Einstellungen", f"Datenbank konnte nicht geöffnet werden:\n{exc}")
if res["design"] != w.DESIGN:
self.design_wechseln(res["design"])
def design_umschalten(self):
self.design_wechseln("modern" if w.DUNKEL else "dunkel")
def design_wechseln(self, design: str):
"""Fenster mit dem neuen Design neu aufbauen (main() startet es nach dem Schließen wieder)."""
self.settings["design"] = design
self.neustart = True
self.root.after_idle(self.beenden)
def hilfe(self): def hilfe(self):
w.info(self.root, "Kurzanleitung", w.info(self.root, "Kurzanleitung",
@@ -1018,7 +1224,7 @@ class App:
self.ansicht = ALLE self.ansicht = ALLE
self._aktualisieren() self._aktualisieren()
if res["dateien"]: if res["dateien"]:
self.importieren(res["dateien"]) self.importieren(res["dateien"], ziel=self.aid)
if res["rundgang"]: if res["rundgang"]:
self.root.after(200, self.rundgang) self.root.after(200, self.rundgang)
@@ -1035,7 +1241,7 @@ class App:
"(freenet-Unterhändlernummer, Vodafone-VO-Nr. oder Kundenname in GROSSBUCHSTABEN)."), "(freenet-Unterhändlernummer, Vodafone-VO-Nr. oder Kundenname in GROSSBUCHSTABEN)."),
(tb["Neu"], "Neue Abrechnung", (tb["Neu"], "Neue Abrechnung",
"Für jeden Abrechnungszeitraum – meist ein Monat – legen Sie eine neue Abrechnung an. " "Für jeden Abrechnungszeitraum – meist ein Monat – legen Sie eine neue Abrechnung an. "
"Mit „Öffnen“ holen Sie frühere Abrechnungen wieder hervor."), "Frühere Abrechnungen öffnen Sie mit einem Klick in der Liste darunter."),
(tb["Import"], "Dateien importieren", (tb["Import"], "Dateien importieren",
"Wählen Sie die CSV- und Excel-Dateien von freenet und Vodafone – gerne mehrere auf einmal. " "Wählen Sie die CSV- und Excel-Dateien von freenet und Vodafone – gerne mehrere auf einmal. "
"Das Programm erkennt das Format selbst und warnt bei doppelt importierten Dateien."), "Das Programm erkennt das Format selbst und warnt bei doppelt importierten Dateien."),
@@ -1043,24 +1249,28 @@ class App:
"Links sehen Sie jeden Händler mit Anzahl der Positionen und Auszahlungssumme.\n\n" "Links sehen Sie jeden Händler mit Anzahl der Positionen und Auszahlungssumme.\n\n"
"„(Nicht zugeordnet)“ ist alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. " "„(Nicht zugeordnet)“ ist alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
"Das wird nicht exportiert."), "Das wird nicht exportiert."),
(self.pills["alle"], "Filter",
"Mit diesen Knöpfen zeigen Sie nur bestimmte Positionen: nicht zugeordnete, von Hand geänderte, "
"doppelte oder bereits abgerechnete."),
(self.f_p, "Positionen prüfen", (self.f_p, "Positionen prüfen",
"Rechts stehen die Positionen des gewählten Händlers, gruppiert nach Provisionsart.\n\n" "Rechts stehen die Positionen des gewählten Händlers, gruppiert nach Provisionsart.\n\n"
"- Doppelklick auf eine Position: Betrag frei ändern\n" "- Doppelklick auf eine Position: Betrag frei ändern\n"
"- Rechtsklick: Auszahlung 0 €, Händler zuweisen, ...\n" "- Rechtsklick: Auszahlung 0 €, Händler zuweisen, ...\n"
"- Blau = von Hand geändert, Rot = durch Prozent-Regel geändert"), "- Blau = von Hand geändert, Rot = durch Prozent-Regel geändert"),
(self.b_prozent, "Auszahlung in Prozent", (self.b_prozent, "Auszahlung in Prozent",
"Graue Gruppenzeile (Provisionsart) markieren und hier klicken – z. B. 80 % für den Sonderbonus. " "Gruppenzeile (Provisionsart) markieren und hier klicken – z. B. 80 % für den Sonderbonus. "
"Mit „Als Standard merken“ gilt das auch in den nächsten Monaten automatisch."), "Mit „Als Standard merken“ gilt das auch in den nächsten Monaten automatisch."),
(tb["Export"], "Exportieren", (tb["Export"], "Exportieren",
"Zum Schluss erstellt „Export“ für jeden Händler eine Excel-Datei im gewohnten Abrechnungsformat " "Zum Schluss erstellt „Exportieren“ für jeden Händler eine Excel-Datei im gewohnten Abrechnungsformat "
"(Airtime darin als eine Summenzeile) und – falls vorhanden – die Airtime-Liste."), "(Airtime darin als eine Summenzeile) und – falls vorhanden – die Airtime-Liste."),
(self.status, "Summen", (self.status, "Summen",
"Unten sehen Sie immer die Summe der angezeigten bzw. markierten Positionen: " "Unten sehen Sie immer die Summe der angezeigten bzw. markierten Positionen: "
"Provision laut Anbieter und Ihre Auszahlung."), "Provision laut Anbieter und Ihre Auszahlung."),
(self._suchrahmen, "Suchen", (self._suchrahmen, "Suchen",
"Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."), "Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."),
(self.menueleiste.knoepfe["?"], "Das war's!", (self.menueleiste.knoepfe[self.hilfe_menue], "Das war's!",
"Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü „?“.\n\n" "Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü "
f"„{self.hilfe_menue}“.\n\n"
f"Viel Erfolg mit dem {w.APP_NAME}!"), f"Viel Erfolg mit dem {w.APP_NAME}!"),
] ]
self._rundgang = ass.Rundgang(self, schritte) self._rundgang = ass.Rundgang(self, schritte)
@@ -1164,11 +1374,16 @@ def main():
ctypes.windll.shcore.SetProcessDpiAwareness(1) ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception: except Exception:
pass pass
root = tk.Tk(className="Provisionsmanager95") dateien = [a for a in sys.argv[1:] if Path(a).exists()]
app = App(root) while True:
if len(sys.argv) > 1: root = tk.Tk(className="Provisionsmanager95")
root.after(200, lambda: app.importieren([a for a in sys.argv[1:] if Path(a).exists()])) app = App(root)
root.mainloop() if dateien:
root.after(200, lambda: app.importieren(dateien))
dateien = []
root.mainloop()
if not app.neustart: # sonst: Design wurde umgeschaltet – Fenster neu aufbauen
break
if __name__ == "__main__": if __name__ == "__main__":
+2 -2
View File
@@ -74,9 +74,9 @@ class Einrichtungsassistent(w.Dialog):
self._img = w.make_icon(self, "app", 7) self._img = w.make_icon(self, "app", 7)
c.create_rectangle(0, self.HOEHE - 70, 160, self.HOEHE + 10, fill="#006060", outline="") 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))): 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_rectangle(x, y, x + 2 + i % 2, y + 2 + i % 2, fill="#ffffff", outline="")
c.create_image(77, self.HOEHE // 2 - 10, image=self._img) c.create_image(77, self.HOEHE // 2 - 10, image=self._img)
c.create_text(77, self.HOEHE - 38, text=w.APP_NAME.replace("-", "-\n", 1), fill=w.WHITE, font=w.FONT_BOLD, c.create_text(77, self.HOEHE - 38, text=w.APP_NAME.replace("-", "-\n", 1), fill="#ffffff", font=w.FONT_BOLD,
justify="center") justify="center")
def _leeren(self): def _leeren(self):
+38 -19
View File
@@ -558,10 +558,13 @@ class ProvisionsartenDialog(w.Dialog):
# =========================================================================== Abrechnungen # =========================================================================== Abrechnungen
class AbrechnungenDialog(w.Dialog): class AbrechnungenDialog(w.Dialog):
def __init__(self, master, db: Datenbank, aktuelle_id: int | None): def __init__(self, master, db: Datenbank, aktuelle_id: int | None, fuer_import: bool = False):
super().__init__(master, "Abrechnung öffnen", resizable=True) """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 self.db = db
tk.Label(self, text="Gespeicherte Abrechnungen:", anchor="w").pack(fill="x", padx=10, pady=(10, 2)) 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"), r, self.tree = w.scroll_tree(self, columns=("titel", "status", "geaendert", "anzahl", "dateien"),
show="headings", height=12, selectmode="browse") show="headings", height=12, selectmode="browse")
for c, t, b in (("titel", "Bezeichnung", 180), ("status", "Status", 110), ("geaendert", "Zuletzt geändert", 120), for c, t, b in (("titel", "Bezeichnung", 180), ("status", "Status", 110), ("geaendert", "Zuletzt geändert", 120),
@@ -572,9 +575,13 @@ class AbrechnungenDialog(w.Dialog):
self.tree.bind("<Double-1>", lambda e: self._oeffnen()) self.tree.bind("<Double-1>", lambda e: self._oeffnen())
k = tk.Frame(self, bg=w.FACE) k = tk.Frame(self, bg=w.FACE)
k.pack(fill="x", padx=10, pady=10) k.pack(fill="x", padx=10, pady=10)
w.knopf(k, "Öffnen", self._oeffnen, breite=10, default="active").pack(side="left") if fuer_import:
w.knopf(k, "Umbenennen...", self._umbenennen, breite=12).pack(side="left", padx=6) w.knopf(k, "Hierhin importieren", self._oeffnen, default="active").pack(side="left")
w.knopf(k, "Löschen", self._loeschen, breite=10).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") w.knopf(k, "Abbrechen", self.abbrechen, breite=10).pack(side="right")
self.bind("<Return>", lambda e: self._oeffnen()) self.bind("<Return>", lambda e: self._oeffnen())
self._laden(aktuelle_id) self._laden(aktuelle_id)
@@ -600,6 +607,10 @@ class AbrechnungenDialog(w.Dialog):
self.result = ("oeffnen", self._sel()) self.result = ("oeffnen", self._sel())
self.destroy() self.destroy()
def _neu(self):
self.result = ("neu",)
self.destroy()
def _umbenennen(self): def _umbenennen(self):
aid = self._sel() aid = self._sel()
if not aid: if not aid:
@@ -857,10 +868,15 @@ class ExportDialog(w.Dialog):
"""haendler: (id, name, anzahl, summe_cent)""" """haendler: (id, name, anzahl, summe_cent)"""
super().__init__(master, "Abrechnungen exportieren") super().__init__(master, "Abrechnungen exportieren")
self.settings = settings self.settings = settings
f = tk.Frame(self, bg=w.FACE) rumpf = tk.Frame(self, bg=w.FACE)
f.pack(fill="both", expand=True, padx=10, pady=10) 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") 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, r, self.tree = w.scroll_tree(f, columns=("name", "anzahl", "summe"), show="tree headings", height=12,
selectmode="extended") selectmode="extended")
self.tree.heading("#0", text="") self.tree.heading("#0", text="")
self.tree.column("#0", width=40, stretch=False) self.tree.column("#0", width=40, stretch=False)
@@ -880,7 +896,7 @@ class ExportDialog(w.Dialog):
w.knopf(kk, "Alle", lambda: self._alle(True)).pack(side="left") w.knopf(kk, "Alle", lambda: self._alle(True)).pack(side="left")
w.knopf(kk, "Keine", lambda: self._alle(False)).pack(side="left", padx=4) w.knopf(kk, "Keine", lambda: self._alle(False)).pack(side="left", padx=4)
g = w.gruppe(f, "Optionen") g = w.gruppe(rechts, "Optionen")
g.pack(fill="x", pady=(8, 0)) g.pack(fill="x", pady=(8, 0))
tk.Label(g, text="Bezeichnung im Dateinamen:").grid(row=0, column=0, sticky="w") tk.Label(g, text="Bezeichnung im Dateinamen:").grid(row=0, column=0, sticky="w")
self.v_titel = tk.StringVar(value=titel) self.v_titel = tk.StringVar(value=titel)
@@ -902,13 +918,14 @@ class ExportDialog(w.Dialog):
("Summenzeile unter der Tabelle", self.v_sum), ("Summenzeile unter der Tabelle", self.v_sum),
("Ordner nach dem Export öffnen", self.v_auf)), start=2): ("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.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\n" 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").grid( "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)) row=7, column=0, columnspan=3, sticky="w", pady=(4, 0))
g.columnconfigure(1, weight=1) g.columnconfigure(1, weight=1)
z = w.gruppe(f, "Zeitraum und Abrechnung") z = w.gruppe(rechts, "Zeitraum und Abrechnung")
z.pack(fill="x", pady=(8, 0)) z.pack(fill="x", before=g)
zf = tk.Frame(z, bg=w.FACE) zf = tk.Frame(z, bg=w.FACE)
zf.pack(fill="x") zf.pack(fill="x")
tk.Label(zf, text="Nur von:").pack(side="left") tk.Label(zf, text="Nur von:").pack(side="left")
@@ -923,11 +940,12 @@ class ExportDialog(w.Dialog):
self.v_basis = tk.StringVar(value=zr.BASEN[settings.get("export_basis", zr.AKTIVIERUNG)]) 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", ttk.Combobox(z, textvariable=self.v_basis, values=list(zr.BASEN.values()), state="readonly",
width=40).pack(anchor="w", pady=(4, 0)) 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").pack( tk.Label(z, text="(leer bzw. „Noch nicht abgerechnet“ = alle offenen Positionen; Datum als TT.MM.JJJJ)",
fill="x") fg=w.SHADOW, anchor="w", justify="left", wraplength=420).pack(fill="x")
self.v_kennz = tk.BooleanVar(value=True) self.v_kennz = tk.BooleanVar(value=True)
tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch " tk.Checkbutton(z, text="Exportierte Positionen als abgerechnet kennzeichnen (werden dann nicht noch "
"einmal abgerechnet)", variable=self.v_kennz, anchor="w").pack(fill="x", pady=(4, 0)) "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)) self.knopfleiste([("Exportieren", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 10))
def _vormonat(self): def _vormonat(self):
@@ -1003,10 +1021,11 @@ class EinstellungenDialog(w.Dialog):
g2 = w.gruppe(self, "Darstellung") g2 = w.gruppe(self, "Darstellung")
g2.pack(fill="x", padx=10, pady=(0, 10)) g2.pack(fill="x", padx=10, pady=(0, 10))
self.v_design = tk.StringVar(value=settings.get("design", "modern")) self.v_design = tk.StringVar(value=settings.get("design", "modern"))
tk.Radiobutton(g2, text="Modern (wie Windows 11)", variable=self.v_design, value="modern").pack(anchor="w") 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( tk.Radiobutton(g2, text="Klassisch (Windows 95, Legacy)", variable=self.v_design, value="win95").pack(
anchor="w") anchor="w")
tk.Label(g2, text="Wirkt beim nächsten Programmstart.", fg=w.SHADOW).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)) self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=(0, 10))
def _waehlen(self): def _waehlen(self):
+2 -1
View File
@@ -226,7 +226,8 @@ class Menueleiste(tk.Frame):
self.offener_knopf: tk.Label | None = None self.offener_knopf: tk.Label | None = None
def menue(self, label: str, underline: int | None = 0) -> Popup: def menue(self, label: str, underline: int | None = 0) -> Popup:
knopf = tk.Label(self, text=label, bg=w.FACE, padx=7, pady=2, bd=1, relief="flat", knopf = tk.Label(self, text=label, bg=w.FACE, padx=10 if w.MODERN else 7, pady=6 if w.MODERN else 2,
bd=1, relief="flat",
underline=-1 if underline is None else underline) underline=-1 if underline is None else underline)
knopf.pack(side="left") knopf.pack(side="left")
popup = Popup(self) popup = Popup(self)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+13
View File
@@ -80,6 +80,19 @@ def hwnd(fenster: tk.Misc) -> int | None:
return None return None
def dunkle_titelleiste(fenster: tk.Misc) -> None:
"""Windows-Titelleiste dunkel (Design „dunkel“, ab Windows 10 20H1)."""
h = hwnd(fenster)
if not h:
return
try:
import ctypes
an = ctypes.c_int(1)
ctypes.windll.dwmapi.DwmSetWindowAttribute(h, 20, ctypes.byref(an), ctypes.sizeof(an)) # IMMERSIVE_DARK_MODE
except (OSError, AttributeError):
pass
def stapel(*fenster: tk.Misc) -> list[int]: def stapel(*fenster: tk.Misc) -> list[int]:
"""Reihenfolge der übergebenen Fenster im Windows-Fensterstapel, oberstes zuerst (Indizes).""" """Reihenfolge der übergebenen Fenster im Windows-Fensterstapel, oberstes zuerst (Indizes)."""
ziele = [hwnd(f) for f in fenster] ziele = [hwnd(f) for f in fenster]
+245 -41
View File
@@ -22,17 +22,35 @@ INFO_BG = "#ffffe1"
SEL_BG, SEL_FG = NAVY, WHITE # Auswahl/Markierung SEL_BG, SEL_FG = NAVY, WHITE # Auswahl/Markierung
RAND = SHADOW # dünne Rahmenlinien (nur modern) RAND = SHADOW # dünne Rahmenlinien (nur modern)
DESIGN = "win95" DESIGN = "win95"
MODERN = False MODERN = False # „modern“ oder „dunkel“
DUNKEL = False
APP_NAME = "Provisions-Manager 95" # modern: ohne „95“ APP_NAME = "Provisions-Manager 95" # modern: ohne „95“
# Farbnamen: FACE = Fläche (Fenster, Karten, Dialoge), BG = Grund hinter den Karten, WHITE = Eingabefelder/Listen,
# DARK = Text, TEXT2 = Nebentext, SHADOW = leiser Text/Win95-Schatten, NAVY = Akzent, RAND/RAND2 = Linien,
# PRIMAER/AUF_PRIMAER = Hauptknopf, MANUELL = von Hand geänderte Beträge, *_SOFT = Grund der Status-Pills.
_PALETTEN = { _PALETTEN = {
"win95": dict(FACE="#c0c0c0", HILITE="#ffffff", LIGHT="#dfdfdf", SHADOW="#808080", DARK="#000000", "win95": dict(FACE="#c0c0c0", HILITE="#ffffff", LIGHT="#dfdfdf", SHADOW="#808080", DARK="#000000",
NAVY="#000080", TEAL="#008080", WHITE="#ffffff", INFO_BG="#ffffe1", SEL_BG="#000080", NAVY="#000080", TEAL="#008080", WHITE="#ffffff", INFO_BG="#ffffe1", SEL_BG="#000080",
SEL_FG="#ffffff", RAND="#808080"), SEL_FG="#ffffff", RAND="#808080", BG="#008080", RAND2="#808080", TEXT2="#000000",
"modern": dict(FACE="#f3f3f3", HILITE="#ffffff", LIGHT="#ebebeb", SHADOW="#8a8a8a", DARK="#1b1b1b", PRIMAER="#c0c0c0", AUF_PRIMAER="#000000", ZEBRA="#ffffff", MANUELL="#0000c0", ROT="#a00000",
NAVY="#005fb8", TEAL="#e6eef6", WHITE="#ffffff", INFO_BG="#ffffff", SEL_BG="#cce4f7", ROT_SOFT="#c0c0c0", GRUEN="#008000", GRUEN_SOFT="#c0c0c0", BLAU="#0000ff", BLAU_SOFT="#c0c0c0",
SEL_FG="#1b1b1b", RAND="#d1d1d1"), GOLD="#808000", GOLD_SOFT="#c0c0c0"),
# Hausfarben der Handy-Insel-Webtools (VVL-Tool): Lila, IBM Plex
"modern": dict(FACE="#ffffff", HILITE="#ffffff", LIGHT="#f1edf8", SHADOW="#6b6386", DARK="#211a38",
NAVY="#5e35b1", TEAL="#f1eef8", WHITE="#ffffff", INFO_BG="#ffffff", SEL_BG="#ede7fa",
SEL_FG="#2d1b69", RAND="#e5e0f0", BG="#f1eef8", RAND2="#d6cee8", TEXT2="#5b5372",
PRIMAER="#2d1b69", AUF_PRIMAER="#ffffff", ZEBRA="#faf8fd", MANUELL="#5e35b1", ROT="#b8223f",
ROT_SOFT="#fbe4e9", GRUEN="#11734d", GRUEN_SOFT="#def3ea", BLAU="#2a59c4", BLAU_SOFT="#e4ebfb",
GOLD="#8a6212", GOLD_SOFT="#f7eed8"),
"dunkel": dict(FACE="#1c1436", HILITE="#1c1436", LIGHT="#2a2049", SHADOW="#948daa", DARK="#ece8f6",
NAVY="#a88beb", TEAL="#120c22", WHITE="#221940", INFO_BG="#2a2049", SEL_BG="#3a2d66",
SEL_FG="#ece8f6", RAND="#312551", BG="#120c22", RAND2="#3f3167", TEXT2="#ada5c6",
PRIMAER="#c3adee", AUF_PRIMAER="#1c1436", ZEBRA="#201838", MANUELL="#c3adee", ROT="#f26982",
ROT_SOFT="#3a1e28", GRUEN="#4fc796", GRUEN_SOFT="#163025", BLAU="#7da0f0", BLAU_SOFT="#1e2743",
GOLD="#d8b058", GOLD_SOFT="#332a17"),
} }
globals().update(_PALETTEN["win95"])
IS_WIN = sys.platform.startswith("win") IS_WIN = sys.platform.startswith("win")
FONT_FAMILY = "MS Sans Serif" if IS_WIN else "DejaVu Sans" FONT_FAMILY = "MS Sans Serif" if IS_WIN else "DejaVu Sans"
@@ -40,26 +58,57 @@ FONT_SIZE = 8 if IS_WIN else 9
FONT = (FONT_FAMILY, FONT_SIZE) FONT = (FONT_FAMILY, FONT_SIZE)
FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold") FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold")
FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold") FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold")
FONT_MONO = ("Courier New" if IS_WIN else "DejaVu Sans Mono", FONT_SIZE + 1) MONO_FAMILY = "Courier New" if IS_WIN else "DejaVu Sans Mono"
FONT_MONO = (MONO_FAMILY, FONT_SIZE + 1)
FONT_KLEIN_FETT = FONT_TITEL = FONT_ZAHL = FONT_BOLD
def design_setzen(name: str) -> None: def design_setzen(name: str) -> None:
"""„modern“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply()).""" """„modern“ (hell), „dunkel“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply())."""
global DESIGN, MODERN, FONT_FAMILY, FONT_SIZE, APP_NAME global DESIGN, MODERN, DUNKEL, FONT_FAMILY, FONT_SIZE, APP_NAME, MONO_FAMILY
DESIGN = name if name in _PALETTEN else "modern" DESIGN = name if name in _PALETTEN else "modern"
MODERN = DESIGN == "modern" MODERN = DESIGN in ("modern", "dunkel")
DUNKEL = DESIGN == "dunkel"
APP_NAME = "Provisions-Manager" if MODERN else "Provisions-Manager 95" APP_NAME = "Provisions-Manager" if MODERN else "Provisions-Manager 95"
globals().update(_PALETTEN[DESIGN]) globals().update(_PALETTEN[DESIGN])
FONT_FAMILY = ("Segoe UI" if MODERN else "MS Sans Serif") if IS_WIN else "DejaVu Sans" FONT_FAMILY = ("Segoe UI" if MODERN else "MS Sans Serif") if IS_WIN else "DejaVu Sans"
FONT_SIZE = 9 if (MODERN or not IS_WIN) else 8 MONO_FAMILY = ("Consolas" if MODERN else "Courier New") if IS_WIN else "DejaVu Sans Mono"
FONT_SIZE = 10 if MODERN else (8 if IS_WIN else 9)
_schriften() _schriften()
def _schriften(): def _schriften():
global FONT, FONT_BOLD, FONT_BIG global FONT, FONT_BOLD, FONT_BIG, FONT_MONO, FONT_KLEIN_FETT, FONT_TITEL, FONT_ZAHL
FONT = (FONT_FAMILY, FONT_SIZE) FONT = (FONT_FAMILY, FONT_SIZE)
FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold") FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold")
FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold") FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold")
FONT_MONO = (MONO_FAMILY, FONT_SIZE + (0 if MODERN else 1))
FONT_KLEIN_FETT = (FONT_FAMILY, FONT_SIZE - 2, "bold") # Abschnitts-Überschriften (ABRECHNUNGEN …)
FONT_TITEL = (FONT_FAMILY, FONT_SIZE + 8, "bold") # Name der geöffneten Abrechnung
FONT_ZAHL = (MONO_FAMILY, FONT_SIZE + 6) if MODERN else (FONT_FAMILY, FONT_SIZE + 4, "bold") # Kennzahlen
SCHRIFTEN = Path(__file__).resolve().parent / "schriften"
def _schriften_laden() -> None:
"""IBM Plex (Hausschrift der Handy-Insel-Webtools) nur für dieses Programm laden – ohne Installation."""
import ctypes
import ctypes.util
dateien = sorted(SCHRIFTEN.glob("*.ttf"))
try:
if IS_WIN:
for f in dateien:
ctypes.windll.gdi32.AddFontResourceExW(str(f), 0x10, 0) # FR_PRIVATE
return
lib = ctypes.util.find_library("fontconfig")
if lib:
fc = ctypes.CDLL(lib)
fc.FcConfigAppFontAddFile.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
for f in dateien:
fc.FcConfigAppFontAddFile(None, str(f).encode())
except Exception:
pass # dann eben Segoe UI / Systemschrift
# --------------------------------------------------------------------------- Pixel-Icons (16x16) # --------------------------------------------------------------------------- Pixel-Icons (16x16)
PALETTE = { PALETTE = {
@@ -332,12 +381,15 @@ class Icons:
# --------------------------------------------------------------------------- Theme # --------------------------------------------------------------------------- Theme
def apply(root: tk.Tk) -> None: def apply(root: tk.Tk) -> None:
global FONT_FAMILY global FONT_FAMILY, MONO_FAMILY
if MODERN and not IS_WIN: # Linux: die erste vorhandene moderne Schrift if MODERN: # die erste vorhandene moderne Schrift, am liebsten IBM Plex
from tkinter import font as tkfont from tkinter import font as tkfont
_schriften_laden()
vorhanden = set(tkfont.families(root)) vorhanden = set(tkfont.families(root))
FONT_FAMILY = next((f for f in ("Segoe UI", "Noto Sans", "Cantarell", "Ubuntu", "DejaVu Sans") FONT_FAMILY = next((f for f in ("IBM Plex Sans", "Segoe UI", "Noto Sans", "Cantarell", "Ubuntu",
if f in vorhanden), FONT_FAMILY) "DejaVu Sans") if f in vorhanden), FONT_FAMILY)
MONO_FAMILY = next((f for f in ("IBM Plex Mono", "Consolas", "DejaVu Sans Mono") if f in vorhanden),
MONO_FAMILY)
_schriften() _schriften()
root.configure(bg=FACE) root.configure(bg=FACE)
o = root.option_add o = root.option_add
@@ -399,40 +451,61 @@ def apply(root: tk.Tk) -> None:
def _modern(root: tk.Tk, st: ttk.Style, zeile: int) -> None: def _modern(root: tk.Tk, st: ttk.Style, zeile: int) -> None:
"""Flach und hell: dünne graue Rahmen statt 3D-Kanten, Akzentfarbe beim Fokus.""" """Flach wie die Handy-Insel-Webtools: weiße Flächen, dünne Linien, Lila als Akzent."""
o = root.option_add o = root.option_add
for klasse in ("Entry", "Text", "Listbox", "Spinbox"): for klasse in ("Entry", "Text", "Listbox", "Spinbox"):
o(f"*{klasse}.relief", "flat") o(f"*{klasse}.relief", "flat")
o(f"*{klasse}.borderWidth", 3) o(f"*{klasse}.borderWidth", 4)
o(f"*{klasse}.highlightThickness", 1) o(f"*{klasse}.highlightThickness", 1)
o(f"*{klasse}.highlightBackground", RAND) o(f"*{klasse}.highlightBackground", RAND2)
o(f"*{klasse}.highlightColor", NAVY) o(f"*{klasse}.highlightColor", NAVY)
o(f"*{klasse}.background", WHITE)
o(f"*{klasse}.foreground", DARK)
o(f"*{klasse}.insertBackground", DARK)
o("*Entry.disabledBackground", LIGHT)
o("*Button.relief", "flat") o("*Button.relief", "flat")
o("*Button.borderWidth", 0) o("*Button.borderWidth", 0)
o("*Button.highlightThickness", 1) o("*Button.highlightThickness", 1)
o("*Button.highlightBackground", RAND) o("*Button.highlightBackground", RAND2)
o("*Button.background", WHITE) o("*Button.background", FACE)
o("*Button.activeBackground", SEL_BG) o("*Button.activeBackground", LIGHT)
o("*Button.padY", 3) o("*Button.padX", 12)
o("*Checkbutton.activeBackground", FACE) o("*Button.padY", 4)
o("*Radiobutton.activeBackground", FACE) o("*Button.cursor", "hand2")
for klasse in ("Checkbutton", "Radiobutton"):
o(f"*{klasse}.activeBackground", FACE)
o(f"*{klasse}.selectColor", WHITE)
o("*selectBackground", SEL_BG)
o("*selectForeground", SEL_FG)
o("*Menu.relief", "flat") o("*Menu.relief", "flat")
o("*TCombobox*Listbox.background", WHITE)
o("*TCombobox*Listbox.foreground", DARK)
o("*TCombobox*Listbox.selectBackground", SEL_BG)
o("*TCombobox*Listbox.selectForeground", SEL_FG)
st.theme_use("clam") st.theme_use("clam")
st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=FACE, selectbackground=SEL_BG, st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=FACE, selectbackground=SEL_BG,
selectforeground=SEL_FG, fieldbackground=WHITE, bordercolor=RAND, lightcolor=WHITE, selectforeground=SEL_FG, fieldbackground=WHITE, bordercolor=RAND, lightcolor=FACE,
darkcolor=RAND, focuscolor=NAVY) darkcolor=RAND, focuscolor=NAVY, insertcolor=DARK)
st.configure("Treeview", background=WHITE, fieldbackground=WHITE, rowheight=zeile + 4, borderwidth=0, font=FONT) st.configure("Treeview", background=WHITE, fieldbackground=WHITE, foreground=DARK, rowheight=zeile + 10,
borderwidth=0, font=FONT)
st.map("Treeview", background=[("selected", SEL_BG)], foreground=[("selected", SEL_FG)]) st.map("Treeview", background=[("selected", SEL_BG)], foreground=[("selected", SEL_FG)])
st.configure("Treeview.Heading", background=WHITE, relief="flat", borderwidth=1, font=FONT, padding=(6, 3), st.configure("Treeview.Heading", background=FACE, foreground=SHADOW, relief="flat", borderwidth=1,
bordercolor=RAND, lightcolor=WHITE, darkcolor=RAND) font=FONT_KLEIN_FETT, padding=(10, 7), bordercolor=RAND, lightcolor=FACE, darkcolor=RAND)
st.map("Treeview.Heading", background=[("active", LIGHT)], relief=[("pressed", "flat")]) st.map("Treeview.Heading", background=[("active", LIGHT)], relief=[("pressed", "flat")])
st.configure("TScrollbar", background=LIGHT, troughcolor=FACE, arrowcolor=SHADOW, bordercolor=FACE, # Seitenleiste: ohne Kopf, hohe Zeilen, Auswahl lila wie im VVL-Tool
lightcolor=LIGHT, darkcolor=LIGHT, relief="flat", arrowsize=12, width=12) st.configure("Seite.Treeview", background=FACE, fieldbackground=FACE, rowheight=zeile + 16, font=FONT,
st.map("TScrollbar", background=[("active", RAND)]) borderwidth=0)
st.configure("TCombobox", fieldbackground=WHITE, background=WHITE, arrowcolor=DARK, padding=3, st.layout("Seite.Treeview", [("Treeview.treearea", {"sticky": "nswe"})]) # ohne Rahmen
bordercolor=RAND, lightcolor=WHITE, darkcolor=WHITE) st.map("Seite.Treeview", background=[("selected", SEL_BG)], foreground=[("selected", NAVY)])
st.configure("TScrollbar", background=RAND2, troughcolor=FACE, arrowcolor=SHADOW, bordercolor=FACE,
lightcolor=RAND2, darkcolor=RAND2, relief="flat", arrowsize=10, width=10, gripcount=0)
st.map("TScrollbar", background=[("active", SHADOW)])
st.configure("TCombobox", fieldbackground=WHITE, background=FACE, foreground=DARK, arrowcolor=DARK, padding=4,
bordercolor=RAND2, lightcolor=WHITE, darkcolor=WHITE)
st.map("TCombobox", fieldbackground=[("readonly", WHITE)], selectbackground=[("readonly", WHITE)], st.map("TCombobox", fieldbackground=[("readonly", WHITE)], selectbackground=[("readonly", WHITE)],
selectforeground=[("readonly", DARK)], bordercolor=[("focus", NAVY)]) selectforeground=[("readonly", DARK)], bordercolor=[("focus", NAVY)])
st.configure("TPanedwindow", background=BG)
st.configure("Sash", sashthickness=6, background=BG)
# --------------------------------------------------------------------------- Wort löschen # --------------------------------------------------------------------------- Wort löschen
@@ -566,12 +639,19 @@ class Statusleiste(tk.Frame):
self.vars[key].set(text) self.vars[key].set(text)
def knopf(master, text: str, command=None, icon: tk.PhotoImage | None = None, breite: int | None = None, **kw): def knopf(master, text: str, command=None, icon: tk.PhotoImage | None = None, breite: int | None = None,
primaer: bool = False, **kw):
"""primaer: der eine Hauptknopf einer Fläche (modern dunkel-lila gefüllt, Win95 mit dickem Rahmen)."""
if MODERN: if MODERN:
if primaer:
kw = dict(bg=PRIMAER, fg=AUF_PRIMAER, activebackground=NAVY, activeforeground=AUF_PRIMAER,
highlightbackground=PRIMAER, font=FONT_BOLD) | kw
b = tk.Button(master, text=text, command=command, **kw) # Aussehen aus apply()/_modern() b = tk.Button(master, text=text, command=command, **kw) # Aussehen aus apply()/_modern()
else: else:
b = tk.Button(master, text=text, command=command, bd=2, relief="raised", bg=FACE, b = tk.Button(master, text=text, command=command, bd=2, relief="raised", bg=FACE,
activebackground=FACE, **kw) activebackground=FACE, **kw)
if primaer:
b.configure(default="active")
if icon is not None: if icon is not None:
b.configure(image=icon, compound="left", padx=6) b.configure(image=icon, compound="left", padx=6)
if breite: if breite:
@@ -579,6 +659,115 @@ def knopf(master, text: str, command=None, icon: tk.PhotoImage | None = None, br
return b return b
class Karte(tk.Frame):
"""Fläche, die Inhalte zusammenfasst (Kennzahl, Tabelle): modern weiß mit dünner Linie."""
def __init__(self, master, **kw):
if MODERN:
kw = dict(bg=FACE, bd=0, highlightthickness=1, highlightbackground=RAND, highlightcolor=RAND) | kw
else:
kw = dict(bg=FACE, bd=2, relief="raised") | kw
super().__init__(master, **kw)
def titelleiste(master, text: str = "") -> tuple[tk.Frame, tk.Label]:
"""Win95: blaue Titelleiste oben in einer Karte (wie bei Fenster); rechts passen kleine Knöpfe hinein."""
f = tk.Frame(master, bg=NAVY)
f.pack(fill="x", padx=1, pady=1)
lbl = tk.Label(f, text=text, bg=NAVY, fg=WHITE, font=FONT_BOLD, anchor="w", padx=3)
lbl.pack(side="left", pady=1)
return f, lbl
def abschnitt(master, text: str, **kw) -> tk.Label:
"""Kleine Überschrift (modern in Großbuchstaben und grau, wie die Tabellenköpfe der Webtools)."""
if MODERN:
return tk.Label(master, text=text.upper(), fg=SHADOW, font=FONT_KLEIN_FETT, anchor="w", **kw)
return tk.Label(master, text=text, font=FONT_BOLD, anchor="w", **kw)
def _rund(c: tk.Canvas, x0, y0, x1, y1, r, **kw):
"""Abgerundetes Rechteck auf einem Canvas."""
r = min(r, (x1 - x0) / 2, (y1 - y0) / 2)
pkt = [x0 + r, y0, x1 - r, y0, x1, y0, x1, y0 + r, x1, y1 - r, x1, y1, x1 - r, y1, x0 + r, y1, x0, y1,
x0, y1 - r, x0, y0 + r, x0, y0]
return c.create_polygon(pkt, smooth=True, **kw)
class Pill(tk.Canvas):
"""Umschalter für Filter („Alle“, „Von Hand geändert“ …): modern abgerundet, Win95 als Knopf,
der eingedrückt bleibt. command bekommt nichts übergeben; aktiv wird von außen gesetzt."""
def __init__(self, master, text: str, command=None, aktiv: bool = False, **kw):
from tkinter import font as tkfont
f = tkfont.Font(root=master, font=FONT)
breite, hoehe = f.measure(text) + (24 if MODERN else 16), f.metrics("linespace") + (10 if MODERN else 8)
bg = kw.pop("bg", master.cget("bg"))
super().__init__(master, width=breite, height=hoehe, bg=bg, highlightthickness=0, bd=0, takefocus=1,
cursor="hand2" if MODERN else "", **kw)
self.text, self.command, self.aktiv = text, command, aktiv
self._zeichnen()
self.bind("<Button-1>", lambda e: self._ausloesen())
self.bind("<space>", lambda e: self._ausloesen())
self.bind("<Return>", lambda e: self._ausloesen())
def _ausloesen(self):
self.focus_set()
if self.command:
self.command()
def setzen(self, aktiv: bool):
if aktiv != self.aktiv:
self.aktiv = aktiv
self._zeichnen()
def _zeichnen(self):
self.delete("all")
b, h = int(self["width"]), int(self["height"])
if MODERN:
fuell, linie, fg = (PRIMAER, PRIMAER, AUF_PRIMAER) if self.aktiv else (FACE, RAND2, TEXT2)
_rund(self, 1, 1, b - 2, h - 2, h, fill=fuell, outline=linie)
else:
oben, unten = (SHADOW, HILITE) if self.aktiv else (HILITE, SHADOW)
self.create_rectangle(0, 0, b - 1, h - 1, fill=LIGHT if self.aktiv else FACE, outline=DARK)
self.create_line(1, h - 2, 1, 1, b - 2, 1, fill=oben)
self.create_line(1, h - 2, b - 2, h - 2, b - 2, 1, fill=unten)
fg = DARK
self.create_text(b // 2 + (1 if self.aktiv and not MODERN else 0), h // 2, text=self.text, fill=fg, font=FONT)
def punkt(master, farbe: str, d: int = 9) -> tk.PhotoImage:
"""Farbiger Punkt (Status in Listen); Ecken bleiben durchsichtig."""
img = tk.PhotoImage(master=master, width=d + 4, height=d)
r = d / 2
# Win95: grauer Ring, damit der Punkt auf weißem Grund und auf der blauen Auswahl sichtbar ist
for f, rr in ((farbe, r),) if MODERN else ((SHADOW, r), (farbe, r - 1)):
for y in range(d):
xs = [x for x in range(d) if (x + .5 - r) ** 2 + (y + .5 - r) ** 2 <= rr * rr]
if xs:
img.put(f, to=(xs[0], y, xs[-1] + 1, y + 1))
return img
def logo(master, groesse: int = 34) -> tk.Canvas:
"""Logo-Kachel wie in den Webtools: Lila-Verlauf, abgerundet, weißes %."""
c = tk.Canvas(master, width=groesse, height=groesse, bg=master.cget("bg"), highlightthickness=0, bd=0)
img = tk.PhotoImage(master=master, width=groesse, height=groesse)
a, e = (0x2d, 0x1b, 0x69), (0x5e, 0x35, 0xb1)
r = groesse * 0.28
for y in range(groesse):
for x in range(groesse):
dx = max(r - x - .5, x + .5 - (groesse - r), 0)
dy = max(r - y - .5, y + .5 - (groesse - r), 0)
if dx * dx + dy * dy <= r * r:
t = (x + y) / (2 * groesse - 2)
img.put("#%02x%02x%02x" % tuple(round(a[i] + (e[i] - a[i]) * t) for i in range(3)), (x, y))
c.create_image(0, 0, image=img, anchor="nw")
c.bild = img # type: ignore[attr-defined] # Referenz halten
c.create_text(groesse / 2, groesse / 2, text="%", fill="#ffffff", font=(FONT_FAMILY, int(groesse * .4), "bold"))
return c
class ToolbarButton(tk.Label): class ToolbarButton(tk.Label):
"""Flacher Werkzeugleisten-Knopf (Office-95-Stil): hebt sich beim Überfahren, senkt sich beim Klick.""" """Flacher Werkzeugleisten-Knopf (Office-95-Stil): hebt sich beim Überfahren, senkt sich beim Klick."""
@@ -660,15 +849,28 @@ def gruppe(master, titel: str, **kw) -> tk.LabelFrame:
return tk.LabelFrame(master, text=titel, bd=2, relief="groove", bg=FACE, padx=6, pady=4, **kw) return tk.LabelFrame(master, text=titel, bd=2, relief="groove", bg=FACE, padx=6, pady=4, **kw)
def scroll_tree(master, **kw) -> tuple[tk.Frame, ttk.Treeview]: def _bei_bedarf(leiste: ttk.Scrollbar):
rahmen = Sunken(master, bg=WHITE) """Bildlaufleiste nur zeigen, wenn nicht alles hineinpasst."""
def setzen(lo, hi):
leiste.grid() if float(lo) > 0 or float(hi) < 1 else leiste.grid_remove()
leiste.set(lo, hi)
return setzen
def scroll_tree(master, auto: bool = False, **kw) -> tuple[tk.Frame, ttk.Treeview]:
"""Treeview mit Bildlaufleisten (modern nur bei Bedarf). auto: keine waagrechte Leiste (Seitenleiste)."""
rahmen = Sunken(master, bg=WHITE if MODERN else FACE) # Win95: graue Ecke zwischen den Leisten
tree = ttk.Treeview(rahmen, **kw) tree = ttk.Treeview(rahmen, **kw)
vs = ttk.Scrollbar(rahmen, orient="vertical", command=tree.yview) vs = ttk.Scrollbar(rahmen, orient="vertical", command=tree.yview)
hs = ttk.Scrollbar(rahmen, orient="horizontal", command=tree.xview)
tree.configure(yscrollcommand=vs.set, xscrollcommand=hs.set)
tree.grid(row=0, column=0, sticky="nsew") tree.grid(row=0, column=0, sticky="nsew")
vs.grid(row=0, column=1, sticky="ns") vs.grid(row=0, column=1, sticky="ns")
hs.grid(row=1, column=0, sticky="ew") if auto and MODERN:
rahmen.configure(highlightthickness=0, bg=FACE)
if not auto:
hs = ttk.Scrollbar(rahmen, orient="horizontal", command=tree.xview)
hs.grid(row=1, column=0, sticky="ew")
tree.configure(xscrollcommand=_bei_bedarf(hs) if MODERN else hs.set)
tree.configure(yscrollcommand=_bei_bedarf(vs) if auto or MODERN else vs.set)
rahmen.rowconfigure(0, weight=1) rahmen.rowconfigure(0, weight=1)
rahmen.columnconfigure(0, weight=1) rahmen.columnconfigure(0, weight=1)
return rahmen, tree return rahmen, tree
@@ -747,6 +949,8 @@ class Dialog(tk.Toplevel):
self.geometry(f"{w}x{h}+{max(0, x)}+{max(0, y)}") # Größe + Position zusammen (siehe Tooltip) self.geometry(f"{w}x{h}+{max(0, x)}+{max(0, y)}") # Größe + Position zusammen (siehe Tooltip)
self.deiconify() self.deiconify()
self.update_idletasks() self.update_idletasks()
if DUNKEL:
win32.dunkle_titelleiste(self)
if KLASSISCH and win32.IS_WIN: if KLASSISCH and win32.IS_WIN:
# eigener Taskleisten-Eintrag; wird erst nach kurzem Aus-/Einblenden wirksam # eigener Taskleisten-Eintrag; wird erst nach kurzem Aus-/Einblenden wirksam
if win32.taskleisteneintrag(self): if win32.taskleisteneintrag(self):
@@ -796,7 +1000,7 @@ class Dialog(tk.Toplevel):
def knopfleiste(self, knoepfe: list[tuple[str, object]], default: int = 0) -> tk.Frame: def knopfleiste(self, knoepfe: list[tuple[str, object]], default: int = 0) -> tk.Frame:
f = tk.Frame(self, bg=FACE) f = tk.Frame(self, bg=FACE)
for i, (text, cmd) in enumerate(knoepfe): for i, (text, cmd) in enumerate(knoepfe):
b = knopf(f, text, cmd, breite=10) b = knopf(f, text, cmd, breite=10, primaer=MODERN and i == default)
if i == default: if i == default:
b.configure(default="active") b.configure(default="active")
b.pack(side="left", padx=4) b.pack(side="left", padx=4)
+63
View File
@@ -1129,3 +1129,66 @@ def _alle(widget):
for kind in widget.winfo_children(): for kind in widget.winfo_children():
yield kind yield kind
yield from _alle(kind) yield from _alle(kind)
def test_abrechnungen_links_und_import_fragt_nach_ziel(lauf_modern, home, monkeypatch):
from provtool.gui import win95 as w
from provtool.gui.dialogs import AbrechnungenDialog, ImportBericht
monkeypatch.setattr(w, "frage", lambda *a, **k: True) # „bereits importiert – trotzdem?“ (Testdatei ist schon drin)
lauf = lauf_modern
app = lauf.app
alt = app.aid
neu = app.db.abrechnung_neu("November 2026")
app._aktualisieren()
assert set(app.atree.get_children()) == {str(alt), str(neu)}, "Abrechnungen fehlen links"
app.atree.selection_set(str(neu))
lauf.root.update()
assert app.aid == neu and not app.positionen, "Klick links öffnet die Abrechnung nicht"
app.atree.selection_set(str(alt))
lauf.root.update()
def ziel_waehlen():
d = lauf.dialog(AbrechnungenDialog)
d.tree.selection_set(str(neu))
d._oeffnen()
lauf.ablauf([
(IMMER, lambda: app.importieren([str(home / "freenet.csv")]), "Import"),
(lambda: lauf.dialog(AbrechnungenDialog) is not None, ziel_waehlen, "Ziel: November"),
(lambda: lauf.dialog(ImportBericht) is not None, lambda: lauf.dialog(ImportBericht).abbrechen(), "Bericht zu"),
])
assert not lauf.uebrig, lauf.diagnose()
assert app.aid == neu and len(app.positionen) == 3, "Import nicht in der gewählten Abrechnung"
assert app.atree.selection() == (str(neu),)
lauf.pruefen()
def test_filter_kennzahlen_und_hell_dunkel(lauf_modern):
"""Filter-Knöpfe über der Tabelle, Kennzahl-Kacheln und der Hell/Dunkel-Schalter in der Kopfzeile."""
from provtool import config
from provtool.db import EIGEN
lauf = lauf_modern
app = lauf.app
alle = [p for p in app.positionen if p["haendler_id"] != EIGEN]
assert app.kpi["pos"][0].cget("text") == str(len(alle))
p = alle[0]
app.db.betrag_setzen(p["id"], 123)
app._aktualisieren()
app.pills["manuell"]._ausloesen() # wie ein Klick (event_generate kommt unter Wine nicht an)
lauf.root.update()
manuell = [x for x in app.positionen if x["manuell"] and x["haendler_id"] != EIGEN]
assert len(manuell) == 1
zeilen = [i for g in app.ptree.get_children() for i in app.ptree.get_children(g)]
assert len(zeilen) == len(manuell), "Filter „Von Hand geändert“ wirkt nicht"
assert app.kpi["pos"][0].cget("text") == str(len(manuell))
assert app.pills["manuell"].aktiv and not app.pills["alle"].aktiv
app._filter_setzen("alle")
assert len([i for g in app.ptree.get_children() for i in app.ptree.get_children(g)]) == len(alle)
app.design_umschalten() # schließt das Fenster; main() baut es danach dunkel neu auf
for _ in range(50):
if app._beendet:
break
lauf.root.update()
assert app.neustart and app._beendet
assert config.load()["design"] == "dunkel"