Win95-Titelleiste und -Menüleiste, Einrichtungs-Assistent, Rundgang, Tipp des Tages
- selbst gezeichneter Fensterrahmen (verschieben, maximieren, minimieren, Größe ändern), abschaltbar - Dialoge mit Win95-Titelleiste; vorherige Modalsperre wird nach Rückfragen wiederhergestellt - Assistent: Speicherort, Unterhändler (Nummern aus Abrechnung vorschlagen), erste Abrechnung - Rundgang mit Hinweis-Sprechblasen, Tipp des Tages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a837ddc4d4
commit
4cfe0e6c47
7 files changed
+1125
-40
No files matched your search
@@ -5,6 +5,17 @@ von **freenet** (CSV/Excel) und **Vodafone/Euronics** (Excel) ein, ordnet die Po
|
|||||||
Unterhändlern zu, erlaubt freie Anpassung der Auszahlung und exportiert je Händler eine
|
Unterhändlern zu, erlaubt freie Anpassung der Auszahlung und exportiert je Händler eine
|
||||||
fertige Excel-Abrechnung. Optik: Windows 95.
|
fertige Excel-Abrechnung. Optik: Windows 95.
|
||||||
|
|
||||||
|
## Erster Start
|
||||||
|
|
||||||
|
Beim ersten Start führt ein **Einrichtungs-Assistent** durch Speicherort, Unterhändler
|
||||||
|
(inkl. „Nummern aus Abrechnung vorschlagen“) und die erste Abrechnung. Danach zeigt ein
|
||||||
|
**Rundgang** mit Hinweis-Sprechblasen die wichtigsten Stellen. Beides sowie den
|
||||||
|
**Tipp des Tages** gibt es jederzeit im Menü **?**.
|
||||||
|
|
||||||
|
Die Windows-95-Titelleiste ist selbst gezeichnet (Windows lässt die echte nicht umfärben).
|
||||||
|
Falls sie auf einem PC Probleme macht: *Datei ▸ Einstellungen ▸ Darstellung* – dann wird die
|
||||||
|
normale Windows-Titelleiste verwendet.
|
||||||
|
|
||||||
## Bedienung in Kürze
|
## Bedienung in Kürze
|
||||||
|
|
||||||
1. **Händler anlegen** (`Strg+H`): Name, Standard-Auszahlung in % und die *Kennungen*:
|
1. **Händler anlegen** (`Strg+H`): Name, Standard-Auszahlung in % und die *Kennungen*:
|
||||||
@@ -75,6 +86,8 @@ Aufbau:
|
|||||||
| `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 im Win95-Stil) |
|
||||||
|
| `provtool/gui/rahmen.py` | selbst gezeichnete Titelleiste, Menüleiste, Größenänderung |
|
||||||
|
| `provtool/gui/assistent.py` | Einrichtungs-Assistent, Rundgang, Tipp des Tages |
|
||||||
|
|
||||||
**Datenschutz:** Echte Abrechnungen enthalten Kundendaten und gehören nicht ins Repository
|
**Datenschutz:** Echte Abrechnungen enthalten Kundendaten und gehören nicht ins Repository
|
||||||
(`.gitignore` schließt `*.csv`, `*.xlsx` im Hauptordner und `beispieldaten/` aus).
|
(`.gitignore` schließt `*.csv`, `*.xlsx` im Hauptordner und `beispieldaten/` aus).
|
||||||
|
|||||||
@@ -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.0.0"
|
__version__ = "1.1.0"
|
||||||
APP_NAME = "Provisions-Manager 95"
|
APP_NAME = "Provisions-Manager 95"
|
||||||
APP_ORG = "Handy-Insel Miesbach"
|
APP_ORG = "Handy-Insel Miesbach"
|
||||||
+116
-37
@@ -15,7 +15,9 @@ from tkinter import filedialog
|
|||||||
from provtool import APP_NAME, config
|
from provtool import APP_NAME, config
|
||||||
from provtool import export as exp
|
from provtool import export as exp
|
||||||
from provtool.db import UNZUGEORDNET, Datenbank
|
from provtool.db import UNZUGEORDNET, Datenbank
|
||||||
|
from provtool.gui import assistent as ass
|
||||||
from provtool.gui import dialogs as dlg
|
from provtool.gui import dialogs as dlg
|
||||||
|
from provtool.gui import rahmen as rh
|
||||||
from provtool.gui import win95 as w
|
from provtool.gui import win95 as w
|
||||||
from provtool.importer import ANBIETER, ImportFehler, lies_datei
|
from provtool.importer import ANBIETER, ImportFehler, lies_datei
|
||||||
from provtool.money import fmt, fmt_percent, parse_cent
|
from provtool.money import fmt, fmt_percent, parse_cent
|
||||||
@@ -67,30 +69,40 @@ class App:
|
|||||||
self.icons = w.Icons(root)
|
self.icons = w.Icons(root)
|
||||||
root._app_icon = self.icons("app", 2) # type: ignore[attr-defined]
|
root._app_icon = self.icons("app", 2) # type: ignore[attr-defined]
|
||||||
root.iconphoto(True, self.icons("app", 2), self.icons("app", 1))
|
root.iconphoto(True, self.icons("app", 2), self.icons("app", 1))
|
||||||
root.title(APP_NAME)
|
|
||||||
root.geometry(self.settings.get("fenster", "1200x720"))
|
|
||||||
root.minsize(900, 520)
|
|
||||||
root.protocol("WM_DELETE_WINDOW", self.beenden)
|
root.protocol("WM_DELETE_WINDOW", self.beenden)
|
||||||
root.report_callback_exception = self._fehler_handler
|
root.report_callback_exception = self._fehler_handler
|
||||||
|
klassisch = bool(self.settings.get("klassischer_rahmen", True))
|
||||||
|
w.KLASSISCH = klassisch
|
||||||
|
self.rahmen = rh.Hauptrahmen(root, APP_NAME, self.icons("app"), self.beenden, klassisch=klassisch)
|
||||||
|
self.rahmen.set_titel(APP_NAME)
|
||||||
|
self.inhalt = self.rahmen.inhalt
|
||||||
|
|
||||||
self._menue()
|
self._menue()
|
||||||
self._toolbar()
|
self._toolbar()
|
||||||
self._body()
|
self._body()
|
||||||
self.status = w.Statusleiste(root, [("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)
|
||||||
|
if griff:
|
||||||
|
griff.pack(side="right", anchor="se")
|
||||||
self.status.pack(side="bottom", fill="x")
|
self.status.pack(side="bottom", fill="x")
|
||||||
self._tasten()
|
self._tasten()
|
||||||
|
self.rahmen.geometrie_setzen(self.settings.get("fenster"), self.settings.get("maximiert", False))
|
||||||
|
|
||||||
abr = self.db.abrechnungen()
|
abr = self.db.abrechnungen()
|
||||||
if abr:
|
if abr:
|
||||||
self.oeffnen(abr[0]["id"])
|
self.oeffnen(abr[0]["id"])
|
||||||
else:
|
else:
|
||||||
self._aktualisieren()
|
self._aktualisieren()
|
||||||
root.after(300, self._willkommen)
|
if not self.settings.get("assistent_erledigt") and not self.db.haendler_liste():
|
||||||
|
root.after(400, self.assistent)
|
||||||
|
elif self.settings.get("tipps_beim_start", True):
|
||||||
|
root.after(500, self.tipp_des_tages)
|
||||||
|
|
||||||
# ================================================================= Aufbau
|
# ================================================================= Aufbau
|
||||||
def _menue(self):
|
def _menue(self):
|
||||||
m = tk.Menu(self.root, tearoff=0)
|
self.menueleiste = rh.Menueleiste(self.inhalt, self.root)
|
||||||
d = tk.Menu(m, tearoff=0)
|
self.menueleiste.pack(side="top", fill="x")
|
||||||
|
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)
|
||||||
d.add_separator()
|
d.add_separator()
|
||||||
@@ -103,32 +115,32 @@ class App:
|
|||||||
d.add_command(label="Einstellungen…", command=self.einstellungen)
|
d.add_command(label="Einstellungen…", command=self.einstellungen)
|
||||||
d.add_separator()
|
d.add_separator()
|
||||||
d.add_command(label="Beenden", accelerator="Alt+F4", command=self.beenden)
|
d.add_command(label="Beenden", accelerator="Alt+F4", command=self.beenden)
|
||||||
m.add_cascade(label="Datei", menu=d, underline=0)
|
|
||||||
|
|
||||||
b = tk.Menu(m, tearoff=0)
|
b = self.menueleiste.menue("Bearbeiten")
|
||||||
b.add_command(label="Betrag ändern…", accelerator="F2", command=self.betrag_bearbeiten)
|
b.add_command(label="Betrag ändern…", accelerator="F2", command=self.betrag_bearbeiten)
|
||||||
b.add_command(label="Auszahlung in Prozent…", accelerator="F4", command=self.prozent)
|
b.add_command(label="Auszahlung in Prozent…", accelerator="F4", command=self.prozent)
|
||||||
b.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen)
|
b.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen)
|
||||||
b.add_separator()
|
b.add_separator()
|
||||||
b.add_command(label="Alle Beträge neu berechnen", command=self.neu_berechnen)
|
b.add_command(label="Alle Beträge neu berechnen", command=self.neu_berechnen)
|
||||||
m.add_cascade(label="Bearbeiten", menu=b, underline=0)
|
|
||||||
|
|
||||||
h = tk.Menu(m, tearoff=0)
|
h = self.menueleiste.menue("Händler")
|
||||||
h.add_command(label="Händler verwalten…", accelerator="Strg+H", command=self.haendler)
|
h.add_command(label="Händler verwalten…", accelerator="Strg+H", command=self.haendler)
|
||||||
h.add_command(label="Kennung des Eintrags zuordnen…", command=self.kennung_zuordnen_auswahl)
|
h.add_command(label="Kennung des Eintrags zuordnen…", command=self.kennung_zuordnen_auswahl)
|
||||||
h.add_command(label="Auswahl fest einem Händler zuweisen…", command=self.haendler_zuweisen)
|
h.add_command(label="Auswahl fest einem Händler zuweisen…", command=self.haendler_zuweisen)
|
||||||
h.add_command(label="Auswahl automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True))
|
h.add_command(label="Auswahl automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True))
|
||||||
m.add_cascade(label="Händler", menu=h, underline=0)
|
|
||||||
|
|
||||||
hi = tk.Menu(m, tearoff=0)
|
hi = self.menueleiste.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="Tipp des Tages…", command=self.tipp_des_tages)
|
||||||
|
hi.add_separator()
|
||||||
|
hi.add_command(label="Einrichtungs-Assistent…", command=self.assistent)
|
||||||
hi.add_separator()
|
hi.add_separator()
|
||||||
hi.add_command(label=f"Info über {APP_NAME}…", command=lambda: dlg.InfoDialog(self.root).zeigen())
|
hi.add_command(label=f"Info über {APP_NAME}…", command=lambda: dlg.InfoDialog(self.root).zeigen())
|
||||||
m.add_cascade(label="?", menu=hi)
|
|
||||||
self.root.configure(menu=m)
|
|
||||||
|
|
||||||
def _toolbar(self):
|
def _toolbar(self):
|
||||||
tb = tk.Frame(self.root, bg=w.FACE, bd=1, relief="raised")
|
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=1, relief="raised")
|
||||||
|
self.tb: dict[str, w.ToolbarButton] = {}
|
||||||
tb.pack(side="top", fill="x")
|
tb.pack(side="top", fill="x")
|
||||||
for text, icon, cmd, tip in (
|
for text, icon, cmd, tip in (
|
||||||
("Neu", "neu", self.neu, "Neue Abrechnung anlegen (Strg+N)"),
|
("Neu", "neu", self.neu, "Neue Abrechnung anlegen (Strg+N)"),
|
||||||
@@ -144,7 +156,8 @@ class App:
|
|||||||
if text is None:
|
if text is None:
|
||||||
w.trenner(tb).pack(side="left", fill="y", padx=4, pady=3)
|
w.trenner(tb).pack(side="left", fill="y", padx=4, pady=3)
|
||||||
continue
|
continue
|
||||||
w.ToolbarButton(tb, text, self.icons(icon), cmd, tip).pack(side="left", padx=1, pady=1)
|
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()
|
self.v_suche = tk.StringVar()
|
||||||
sf = tk.Frame(tb, bg=w.FACE)
|
sf = tk.Frame(tb, bg=w.FACE)
|
||||||
sf.pack(side="right", padx=8)
|
sf.pack(side="right", padx=8)
|
||||||
@@ -153,9 +166,10 @@ class App:
|
|||||||
e.pack(side="left", padx=4)
|
e.pack(side="left", padx=4)
|
||||||
self.v_suche.trace_add("write", lambda *_: self._tabelle())
|
self.v_suche.trace_add("write", lambda *_: self._tabelle())
|
||||||
self._suchfeld = e
|
self._suchfeld = e
|
||||||
|
self._suchrahmen = sf
|
||||||
|
|
||||||
def _body(self):
|
def _body(self):
|
||||||
desk = tk.Frame(self.root, bg=w.TEAL)
|
desk = tk.Frame(self.inhalt, bg=w.TEAL)
|
||||||
desk.pack(side="top", fill="both", expand=True)
|
desk.pack(side="top", fill="both", expand=True)
|
||||||
pw = tk.PanedWindow(desk, orient="horizontal", bg=w.TEAL, sashwidth=6, bd=0)
|
pw = tk.PanedWindow(desk, orient="horizontal", bg=w.TEAL, sashwidth=6, bd=0)
|
||||||
pw.pack(fill="both", expand=True, padx=6, pady=6)
|
pw.pack(fill="both", expand=True, padx=6, pady=6)
|
||||||
@@ -202,7 +216,8 @@ class App:
|
|||||||
|
|
||||||
leiste = tk.Frame(self.f_p.inhalt, bg=w.FACE)
|
leiste = tk.Frame(self.f_p.inhalt, bg=w.FACE)
|
||||||
leiste.pack(fill="x", pady=(4, 0))
|
leiste.pack(fill="x", pady=(4, 0))
|
||||||
w.knopf(leiste, "Prozent…", self.prozent, self.icons("prozent")).pack(side="left")
|
self.b_prozent = w.knopf(leiste, "Prozent…", self.prozent, self.icons("prozent"))
|
||||||
|
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, self.icons("rechner")).pack(side="left", padx=4)
|
||||||
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)
|
self.v_alle_auf = tk.BooleanVar(value=True)
|
||||||
@@ -235,12 +250,12 @@ class App:
|
|||||||
if self.aid and self.db.abrechnung(self.aid):
|
if self.aid and self.db.abrechnung(self.aid):
|
||||||
self.positionen = self.db.positionen(self.aid)
|
self.positionen = self.db.positionen(self.aid)
|
||||||
titel = self.db.abrechnung(self.aid)["titel"]
|
titel = self.db.abrechnung(self.aid)["titel"]
|
||||||
self.root.title(f"{APP_NAME} - [{titel}]")
|
self.rahmen.set_titel(f"{APP_NAME} - [{titel}]")
|
||||||
self.status.set("info", f" Abrechnung: {titel}")
|
self.status.set("info", f" Abrechnung: {titel}")
|
||||||
else:
|
else:
|
||||||
self.aid = None
|
self.aid = None
|
||||||
self.positionen = []
|
self.positionen = []
|
||||||
self.root.title(APP_NAME)
|
self.rahmen.set_titel(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._haendlerliste(behalte_auswahl)
|
self._haendlerliste(behalte_auswahl)
|
||||||
self._tabelle()
|
self._tabelle()
|
||||||
@@ -746,23 +761,33 @@ class App:
|
|||||||
self.ansicht = ALLE
|
self.ansicht = ALLE
|
||||||
self._aktualisieren()
|
self._aktualisieren()
|
||||||
|
|
||||||
def einstellungen(self):
|
def db_wechseln(self, pfad: str | None):
|
||||||
neu = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen()
|
"""Öffnet (oder legt an) eine andere Datenbank. pfad=None: Standard-Speicherort."""
|
||||||
if neu and Path(neu) != self.db.path:
|
ziel = Path(pfad) if pfad else config.app_dir() / "provisionen.db"
|
||||||
try:
|
db = Datenbank(ziel) # wirft bei Fehler – alte DB bleibt dann offen
|
||||||
db = Datenbank(neu)
|
|
||||||
except Exception as exc:
|
|
||||||
w.fehler(self.root, "Einstellungen", f"Datenbank konnte nicht geöffnet werden:\n{exc}")
|
|
||||||
return
|
|
||||||
self.db.close()
|
self.db.close()
|
||||||
self.db = db
|
self.db = db
|
||||||
self.settings["db_pfad"] = neu
|
self.settings["db_pfad"] = str(ziel) if pfad else None
|
||||||
config.save(self.settings)
|
config.save(self.settings)
|
||||||
abr = self.db.abrechnungen()
|
abr = self.db.abrechnungen()
|
||||||
self.aid = abr[0]["id"] if abr else None
|
self.aid = abr[0]["id"] if abr else None
|
||||||
self.ansicht = ALLE
|
self.ansicht = ALLE
|
||||||
self._aktualisieren()
|
self._aktualisieren()
|
||||||
|
|
||||||
|
def einstellungen(self):
|
||||||
|
res = dlg.EinstellungenDialog(self.root, self.settings, str(self.db.path)).zeigen()
|
||||||
|
if not res:
|
||||||
|
return
|
||||||
|
if res["klassisch"] != bool(self.settings.get("klassischer_rahmen", True)):
|
||||||
|
self.settings["klassischer_rahmen"] = res["klassisch"]
|
||||||
|
config.save(self.settings)
|
||||||
|
w.info(self.root, "Einstellungen", "Die Änderung der Titelleiste wird beim nächsten Programmstart wirksam.")
|
||||||
|
if res["pfad"] and Path(res["pfad"]) != self.db.path:
|
||||||
|
try:
|
||||||
|
self.db_wechseln(res["pfad"])
|
||||||
|
except Exception as exc:
|
||||||
|
w.fehler(self.root, "Einstellungen", f"Datenbank konnte nicht geöffnet werden:\n{exc}")
|
||||||
|
|
||||||
def hilfe(self):
|
def hilfe(self):
|
||||||
w.info(self.root, "Kurzanleitung",
|
w.info(self.root, "Kurzanleitung",
|
||||||
"1. Händler anlegen (Strg+H): Name, Standard-Auszahlung in % und die Kennungen –\n"
|
"1. Händler anlegen (Strg+H): Name, Standard-Auszahlung in % und die Kennungen –\n"
|
||||||
@@ -779,12 +804,65 @@ class App:
|
|||||||
"5. Exportieren (Strg+E): je Händler eine Excel-Datei im Abrechnungsformat.\n\n"
|
"5. Exportieren (Strg+E): je Händler eine Excel-Datei im Abrechnungsformat.\n\n"
|
||||||
"Alles wird automatisch in der Datenbank gespeichert.")
|
"Alles wird automatisch in der Datenbank gespeichert.")
|
||||||
|
|
||||||
def _willkommen(self):
|
def assistent(self):
|
||||||
if not self.db.haendler_liste():
|
res = ass.Einrichtungsassistent(self).zeigen()
|
||||||
if w.frage(self.root, "Willkommen",
|
self.settings["assistent_erledigt"] = True
|
||||||
f"Willkommen beim {APP_NAME}!\n\nEs sind noch keine Händler angelegt.\n"
|
config.save(self.settings)
|
||||||
"Jetzt die Unterhändler mit ihren Kennungen anlegen?"):
|
self._aktualisieren()
|
||||||
self.haendler()
|
if not res:
|
||||||
|
return
|
||||||
|
if res["titel"]:
|
||||||
|
self.aid = self.db.abrechnung_neu(res["titel"])
|
||||||
|
self.ansicht = ALLE
|
||||||
|
self._aktualisieren()
|
||||||
|
if res["dateien"]:
|
||||||
|
self.importieren(res["dateien"])
|
||||||
|
if res["rundgang"]:
|
||||||
|
self.root.after(200, self.rundgang)
|
||||||
|
|
||||||
|
def rundgang(self):
|
||||||
|
tb = self.tb
|
||||||
|
schritte = [
|
||||||
|
(self.rahmen.titelleiste or self.menueleiste, "Willkommen zum Rundgang!",
|
||||||
|
"In ein paar Schritten sehen Sie, wo Sie was finden.\n\nWeiter geht es mit „Weiter >“ oder der "
|
||||||
|
"Eingabetaste, beenden können Sie jederzeit mit Esc."),
|
||||||
|
(tb["Händler"], "Händler",
|
||||||
|
"Hier legen Sie Ihre Unterhändler an: Name, Auszahlung in % und woran ihre Positionen erkannt werden "
|
||||||
|
"(freenet-Unterhändlernummer, Vodafone-VO-Nr. oder Kundenname in GROSSBUCHSTABEN)."),
|
||||||
|
(tb["Neu"], "Neue Abrechnung",
|
||||||
|
"Für jeden Abrechnungszeitraum – meist ein Monat – legen Sie eine neue Abrechnung an. "
|
||||||
|
"Mit „Öffnen“ holen Sie frühere Abrechnungen wieder hervor."),
|
||||||
|
(tb["Import"], "Dateien importieren",
|
||||||
|
"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."),
|
||||||
|
(self.f_h, "Händlerliste",
|
||||||
|
"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. "
|
||||||
|
"Das wird nicht exportiert."),
|
||||||
|
(self.f_p, "Positionen prüfen",
|
||||||
|
"Rechts stehen die Positionen des gewählten Händlers, gruppiert nach Provisionsart.\n\n"
|
||||||
|
"• Doppelklick auf eine Position: Betrag frei ändern\n"
|
||||||
|
"• Rechtsklick: Auszahlung 0 €, Händler zuweisen, …\n"
|
||||||
|
"• Blau = von Hand geändert, Rot = durch Prozent-Regel geändert"),
|
||||||
|
(self.b_prozent, "Auszahlung in Prozent",
|
||||||
|
"Graue 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."),
|
||||||
|
(tb["Export"], "Exportieren",
|
||||||
|
"Zum Schluss erstellt „Export“ für jeden Händler eine Excel-Datei im gewohnten Abrechnungsformat."),
|
||||||
|
(self.status, "Summen",
|
||||||
|
"Unten sehen Sie immer die Summe der angezeigten bzw. markierten Positionen: "
|
||||||
|
"Provision laut Anbieter und Ihre Auszahlung."),
|
||||||
|
(self._suchrahmen, "Suchen",
|
||||||
|
"Rufnummer, Name oder Tarif eingeben – die Liste filtert sofort. Tastenkürzel: Strg+F."),
|
||||||
|
(self.menueleiste.knoepfe["?"], "Das war's!",
|
||||||
|
"Diesen Rundgang, den Einrichtungs-Assistenten und die Tipps finden Sie jederzeit hier im Menü „?“.\n\n"
|
||||||
|
"Viel Erfolg mit dem Provisions-Manager 95!"),
|
||||||
|
]
|
||||||
|
self._rundgang = ass.Rundgang(self, schritte)
|
||||||
|
|
||||||
|
def tipp_des_tages(self):
|
||||||
|
ass.TippDesTages(self).zeigen()
|
||||||
|
config.save(self.settings)
|
||||||
|
|
||||||
def _fehler_handler(self, exc, val, tb):
|
def _fehler_handler(self, exc, val, tb):
|
||||||
text = "".join(traceback.format_exception(exc, val, tb))
|
text = "".join(traceback.format_exception(exc, val, tb))
|
||||||
@@ -799,7 +877,8 @@ class App:
|
|||||||
|
|
||||||
def beenden(self):
|
def beenden(self):
|
||||||
try:
|
try:
|
||||||
self.settings["fenster"] = self.root.geometry()
|
self.settings["fenster"] = self.rahmen.normale_geometrie()
|
||||||
|
self.settings["maximiert"] = self.rahmen.maximiert
|
||||||
config.save(self.settings)
|
config.save(self.settings)
|
||||||
self.db.close()
|
self.db.close()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -0,0 +1,569 @@
|
|||||||
|
"""Ersteinrichtung im Stil der Windows-95-Setup-Assistenten, Programm-Rundgang und „Tipp des Tages“."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import tkinter as tk
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from tkinter import filedialog, ttk
|
||||||
|
|
||||||
|
from provtool import APP_NAME
|
||||||
|
from provtool.db import CAPS_ANBIETER, CAPS_WERT, Haendler, ist_grossgeschrieben
|
||||||
|
from provtool.gui import win95 as w
|
||||||
|
from provtool.importer import lies_datei
|
||||||
|
from provtool.money import fmt, parse_percent
|
||||||
|
|
||||||
|
DATEITYPEN = [("Abrechnungen (CSV, Excel)", "*.csv *.xlsx *.xlsm *.xls *.txt"), ("Alle Dateien", "*.*")]
|
||||||
|
|
||||||
|
|
||||||
|
def _nummern(text: str) -> list[str]:
|
||||||
|
"""'11111102 (6 Pos.), 27518999' → ['11111102', '27518999']"""
|
||||||
|
out = []
|
||||||
|
for teil in text.split(","):
|
||||||
|
m = re.match(r"\s*([A-Za-z0-9]+)", teil)
|
||||||
|
if m:
|
||||||
|
out.append(m.group(1))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================== Assistent
|
||||||
|
|
||||||
|
class Einrichtungsassistent(w.Dialog):
|
||||||
|
BREITE, HOEHE = 470, 340
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app.root, "Provisions-Manager 95 – Einrichtung")
|
||||||
|
self.app = app
|
||||||
|
self.dateien: list[str] = []
|
||||||
|
self.vorschlaege: dict[str, list[str]] = {"freenet": [], "vodafone": []}
|
||||||
|
self.caps_anzahl = 0
|
||||||
|
|
||||||
|
haupt = tk.Frame(self, bg=w.FACE)
|
||||||
|
haupt.pack(fill="both", expand=True, padx=10, pady=10)
|
||||||
|
bild = tk.Canvas(haupt, width=150, height=self.HOEHE, bg=w.TEAL, bd=2, relief="sunken",
|
||||||
|
highlightthickness=0)
|
||||||
|
bild.pack(side="left", fill="y")
|
||||||
|
self._bild(bild)
|
||||||
|
self.seite = tk.Frame(haupt, bg=w.FACE, width=self.BREITE, height=self.HOEHE)
|
||||||
|
self.seite.pack(side="left", fill="both", expand=True, padx=(14, 0))
|
||||||
|
self.seite.pack_propagate(False)
|
||||||
|
|
||||||
|
w.trenner(self, vertikal=False).pack(fill="x", padx=10)
|
||||||
|
k = tk.Frame(self, bg=w.FACE)
|
||||||
|
k.pack(fill="x", padx=10, pady=10)
|
||||||
|
self.b_abbr = w.knopf(k, "Abbrechen", self.abbrechen, breite=10)
|
||||||
|
self.b_abbr.pack(side="right")
|
||||||
|
self.b_weiter = w.knopf(k, "Weiter >", self._weiter, breite=10, default="active")
|
||||||
|
self.b_weiter.pack(side="right", padx=(0, 10))
|
||||||
|
self.b_zurueck = w.knopf(k, "< Zurück", self._zurueck, breite=10)
|
||||||
|
self.b_zurueck.pack(side="right")
|
||||||
|
self.bind("<Return>", lambda e: self._weiter() if not isinstance(e.widget, tk.Entry) else None)
|
||||||
|
|
||||||
|
self.v_titel = tk.StringVar(value=app._vorschlag_titel())
|
||||||
|
self.v_abrechnung = tk.BooleanVar(value=True)
|
||||||
|
self.v_rundgang = tk.BooleanVar(value=True)
|
||||||
|
self.v_db_eigen = tk.BooleanVar(value=bool(app.settings.get("db_pfad")))
|
||||||
|
self.v_db_pfad = tk.StringVar(value=str(app.db.path))
|
||||||
|
|
||||||
|
self.seiten = [self._s_willkommen, self._s_datenbank, self._s_haendler, self._s_abrechnung, self._s_fertig]
|
||||||
|
self.nr = 0
|
||||||
|
self._zeige(0)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Rahmen
|
||||||
|
def _bild(self, c: tk.Canvas):
|
||||||
|
self._img = w.make_icon(self, "app", 7)
|
||||||
|
c.create_rectangle(0, self.HOEHE - 70, 160, self.HOEHE + 10, fill="#006060", outline="")
|
||||||
|
for i, (x, y) in enumerate(((20, 30), (120, 55), (35, 120), (128, 150), (60, 20))):
|
||||||
|
c.create_rectangle(x, y, x + 2 + i % 2, y + 2 + i % 2, fill=w.WHITE, outline="")
|
||||||
|
c.create_image(77, self.HOEHE // 2 - 10, image=self._img)
|
||||||
|
c.create_text(77, self.HOEHE - 38, text="Provisions-\nManager 95", fill=w.WHITE, font=w.FONT_BOLD,
|
||||||
|
justify="center")
|
||||||
|
|
||||||
|
def _leeren(self):
|
||||||
|
for kind in self.seite.winfo_children():
|
||||||
|
kind.destroy()
|
||||||
|
|
||||||
|
def _ueberschrift(self, text: str):
|
||||||
|
tk.Label(self.seite, text=text, font=w.FONT_BIG, anchor="w", justify="left",
|
||||||
|
wraplength=self.BREITE).pack(fill="x", pady=(0, 10))
|
||||||
|
|
||||||
|
def _text(self, text: str, master=None, **kw):
|
||||||
|
lbl = tk.Label(master or self.seite, text=text, anchor="w", justify="left", wraplength=self.BREITE, **kw)
|
||||||
|
lbl.pack(fill="x", pady=(0, 6))
|
||||||
|
return lbl
|
||||||
|
|
||||||
|
def _zeige(self, nr: int):
|
||||||
|
self.nr = nr
|
||||||
|
self._leeren()
|
||||||
|
self.seiten[nr]()
|
||||||
|
self.b_zurueck.configure(state="normal" if nr > 0 else "disabled")
|
||||||
|
self.b_weiter.configure(text="Fertig stellen" if nr == len(self.seiten) - 1 else "Weiter >")
|
||||||
|
if self.titelleiste:
|
||||||
|
self.titelleiste.set_titel(f"Provisions-Manager 95 – Einrichtung (Schritt {nr + 1} von {len(self.seiten)})")
|
||||||
|
|
||||||
|
def _weiter(self):
|
||||||
|
pruefer = getattr(self, f"_pruefe_{self.nr}", None)
|
||||||
|
if pruefer and not pruefer():
|
||||||
|
return
|
||||||
|
if self.nr == len(self.seiten) - 1:
|
||||||
|
self.result = dict(titel=self.v_titel.get().strip() if self.v_abrechnung.get() else "",
|
||||||
|
dateien=list(self.dateien) if self.v_abrechnung.get() else [],
|
||||||
|
rundgang=self.v_rundgang.get())
|
||||||
|
self.destroy()
|
||||||
|
return
|
||||||
|
self._zeige(self.nr + 1)
|
||||||
|
|
||||||
|
def _zurueck(self):
|
||||||
|
if self.nr > 0:
|
||||||
|
self._zeige(self.nr - 1)
|
||||||
|
|
||||||
|
def abbrechen(self):
|
||||||
|
if w.frage(self, "Einrichtung abbrechen",
|
||||||
|
"Möchten Sie die Einrichtung wirklich abbrechen?\n\n"
|
||||||
|
"Bereits angelegte Händler bleiben gespeichert. Den Assistenten finden Sie jederzeit "
|
||||||
|
"unter ? ▸ Einrichtungs-Assistent.", default_ja=False):
|
||||||
|
self.result = None
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Seite 1
|
||||||
|
def _s_willkommen(self):
|
||||||
|
self._ueberschrift(f"Willkommen beim {APP_NAME}!")
|
||||||
|
self._text("Dieser Assistent richtet das Programm in wenigen Schritten ein:")
|
||||||
|
for punkt in ("Speicherort der Daten festlegen",
|
||||||
|
"Unterhändler mit ihren Kennungen anlegen",
|
||||||
|
"die erste Abrechnung anlegen und Dateien importieren"):
|
||||||
|
self._text(f" • {punkt}")
|
||||||
|
self._text("\nDanach zeigt Ihnen ein kurzer Rundgang, wo Sie was finden.\n\n"
|
||||||
|
"Alle Angaben lassen sich später jederzeit ändern.")
|
||||||
|
self._text("Klicken Sie auf „Weiter“, um fortzufahren.", fg=w.NAVY)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Seite 2
|
||||||
|
def _s_datenbank(self):
|
||||||
|
self._ueberschrift("Wo sollen die Daten gespeichert werden?")
|
||||||
|
self._text("In der Datenbank liegen Ihre Händler, die Auszahlungsregeln und alle Abrechnungen "
|
||||||
|
"mit Ihren Änderungen.")
|
||||||
|
f = tk.Frame(self.seite, bg=w.FACE)
|
||||||
|
f.pack(fill="x", pady=6)
|
||||||
|
from provtool import config
|
||||||
|
standard = str(config.app_dir() / "provisionen.db")
|
||||||
|
tk.Radiobutton(f, text="Auf diesem Computer (empfohlen)", variable=self.v_db_eigen, value=False,
|
||||||
|
command=lambda: self.v_db_pfad.set(standard)).pack(anchor="w")
|
||||||
|
tk.Label(f, text=standard, fg=w.SHADOW, anchor="w", wraplength=self.BREITE - 30,
|
||||||
|
justify="left").pack(anchor="w", padx=22)
|
||||||
|
tk.Radiobutton(f, text="An einem eigenen Ort, z. B. auf einem Netzlaufwerk:", variable=self.v_db_eigen,
|
||||||
|
value=True).pack(anchor="w", pady=(10, 0))
|
||||||
|
zeile = tk.Frame(f, bg=w.FACE)
|
||||||
|
zeile.pack(fill="x", padx=22, pady=2)
|
||||||
|
e = tk.Entry(zeile, textvariable=self.v_db_pfad)
|
||||||
|
e.pack(side="left", fill="x", expand=True)
|
||||||
|
e.bind("<Key>", lambda ev: self.v_db_eigen.set(True))
|
||||||
|
w.knopf(zeile, "Durchsuchen…", self._db_waehlen).pack(side="left", padx=(4, 0))
|
||||||
|
self._text("\nTipp: Auf einem Netzlaufwerk können mehrere PCs dieselben Händler nutzen – "
|
||||||
|
"bitte aber nicht gleichzeitig damit arbeiten.", fg=w.SHADOW)
|
||||||
|
|
||||||
|
def _db_waehlen(self):
|
||||||
|
p = filedialog.asksaveasfilename(parent=self, title="Speicherort der Datenbank", defaultextension=".db",
|
||||||
|
initialfile="provisionen.db", confirmoverwrite=False,
|
||||||
|
filetypes=[("Datenbank", "*.db"), ("Alle Dateien", "*.*")])
|
||||||
|
if p:
|
||||||
|
self.v_db_pfad.set(p)
|
||||||
|
self.v_db_eigen.set(True)
|
||||||
|
|
||||||
|
def _pruefe_1(self) -> bool:
|
||||||
|
from provtool import config
|
||||||
|
ziel = self.v_db_pfad.get().strip() if self.v_db_eigen.get() else str(config.app_dir() / "provisionen.db")
|
||||||
|
if not ziel:
|
||||||
|
w.warnung(self, "Speicherort", "Bitte einen Speicherort angeben.")
|
||||||
|
return False
|
||||||
|
if Path(ziel) != Path(self.app.db.path):
|
||||||
|
try:
|
||||||
|
self.app.db_wechseln(ziel if self.v_db_eigen.get() else None)
|
||||||
|
except Exception as exc:
|
||||||
|
w.fehler(self, "Speicherort", f"Die Datenbank konnte nicht angelegt werden:\n{exc}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Seite 3
|
||||||
|
def _s_haendler(self):
|
||||||
|
db = self.app.db
|
||||||
|
tk.Label(self.seite, text="Unterhändler anlegen", font=w.FONT_BIG, anchor="w").pack(fill="x")
|
||||||
|
self._text("Für jeden Unterhändler: Name, Auszahlung und woran seine Positionen zu erkennen sind. "
|
||||||
|
"Ihre eigene Filiale legen Sie nicht an.", fg=w.SHADOW)
|
||||||
|
|
||||||
|
g = tk.Frame(self.seite, bg=w.FACE)
|
||||||
|
g.pack(fill="x")
|
||||||
|
self.v_name, self.v_proz = tk.StringVar(), tk.StringVar(value="100")
|
||||||
|
self.v_fn, self.v_vf, self.v_caps = tk.StringVar(), tk.StringVar(), tk.BooleanVar()
|
||||||
|
tk.Label(g, text="Name / Firma:").grid(row=0, column=0, sticky="w", pady=1)
|
||||||
|
e_name = tk.Entry(g, textvariable=self.v_name)
|
||||||
|
e_name.grid(row=0, column=1, columnspan=2, sticky="we", pady=1)
|
||||||
|
tk.Label(g, text="Auszahlung:").grid(row=1, column=0, sticky="w", pady=1)
|
||||||
|
pf = tk.Frame(g, bg=w.FACE)
|
||||||
|
pf.grid(row=1, column=1, sticky="w")
|
||||||
|
tk.Entry(pf, textvariable=self.v_proz, width=6, justify="right").pack(side="left")
|
||||||
|
tk.Label(pf, text="% der Provision").pack(side="left", padx=4)
|
||||||
|
tk.Label(g, text="freenet Unterhändler-Nr.:").grid(row=2, column=0, sticky="w", pady=1)
|
||||||
|
self.cb_fn = ttk.Combobox(g, textvariable=self.v_fn, values=self.vorschlaege["freenet"])
|
||||||
|
self.cb_fn.grid(row=2, column=1, columnspan=2, sticky="we", pady=1)
|
||||||
|
tk.Label(g, text="Vodafone VO-Nr.:").grid(row=3, column=0, sticky="w", pady=1)
|
||||||
|
self.cb_vf = ttk.Combobox(g, textvariable=self.v_vf, values=self.vorschlaege["vodafone"])
|
||||||
|
self.cb_vf.grid(row=3, column=1, columnspan=2, sticky="we", pady=1)
|
||||||
|
caps_besitzer = db.kennung_map().get((CAPS_ANBIETER, CAPS_WERT))
|
||||||
|
caps_text = "Vodafone-Kunden mit Nachnamen in GROSSBUCHSTABEN gehören zu diesem Händler"
|
||||||
|
if self.caps_anzahl:
|
||||||
|
caps_text += f" ({self.caps_anzahl} Pos. in der Datei)"
|
||||||
|
cb = tk.Checkbutton(g, text=caps_text, variable=self.v_caps, wraplength=self.BREITE - 30, justify="left")
|
||||||
|
cb.grid(row=4, column=0, columnspan=3, sticky="w", pady=(2, 0))
|
||||||
|
if caps_besitzer:
|
||||||
|
cb.configure(state="disabled", text=f"GROSSBUCHSTABEN-Regel: bereits bei „{db.haendler(caps_besitzer).name}“")
|
||||||
|
g.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
k = tk.Frame(self.seite, bg=w.FACE)
|
||||||
|
k.pack(fill="x", pady=(4, 4))
|
||||||
|
w.knopf(k, "Händler hinzufügen", self._haendler_dazu, self.app.icons("haendler")).pack(side="left")
|
||||||
|
w.knopf(k, "Nummern aus Abrechnung vorschlagen…", self._vorschlagen).pack(side="left", padx=6)
|
||||||
|
|
||||||
|
tk.Label(self.seite, text="Bereits angelegt:", anchor="w").pack(fill="x")
|
||||||
|
lf = tk.Frame(self.seite, bg=w.FACE)
|
||||||
|
lf.pack(fill="both", expand=True)
|
||||||
|
self.lb = tk.Listbox(lf, height=4, activestyle="none")
|
||||||
|
self.lb.pack(side="left", fill="both", expand=True)
|
||||||
|
w.knopf(lf, "Entfernen", self._haendler_weg).pack(side="left", anchor="n", padx=(6, 0))
|
||||||
|
self._liste()
|
||||||
|
e_name.focus_set()
|
||||||
|
|
||||||
|
def _liste(self):
|
||||||
|
self.lb.delete(0, "end")
|
||||||
|
self._lb_ids = []
|
||||||
|
for h in self.app.db.haendler_liste():
|
||||||
|
teile = []
|
||||||
|
for anb, wert in self.app.db.kennungen(h.id):
|
||||||
|
teile.append("GROSSBUCHSTABEN" if anb == CAPS_ANBIETER else f"{anb} {wert}")
|
||||||
|
proz = f"{h.standard_prozent:g} %".replace(".", ",")
|
||||||
|
self.lb.insert("end", f"{h.name} – {proz} – {', '.join(teile) or 'keine Kennung'}")
|
||||||
|
self._lb_ids.append(h.id)
|
||||||
|
|
||||||
|
def _haendler_dazu(self) -> bool:
|
||||||
|
name = self.v_name.get().strip()
|
||||||
|
if not name:
|
||||||
|
w.warnung(self, "Händler", "Bitte einen Namen eingeben.")
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
proz = parse_percent(self.v_proz.get())
|
||||||
|
except ValueError:
|
||||||
|
w.warnung(self, "Händler", "Die Auszahlung muss eine Zahl sein, z. B. 100 oder 85.")
|
||||||
|
return False
|
||||||
|
kenn = [("freenet", n) for n in _nummern(self.v_fn.get())]
|
||||||
|
kenn += [("vodafone", n) for n in _nummern(self.v_vf.get())]
|
||||||
|
if self.v_caps.get():
|
||||||
|
kenn.append((CAPS_ANBIETER, CAPS_WERT))
|
||||||
|
if not kenn and not w.frage(self, "Händler", f"Für „{name}“ ist keine Kennung angegeben – ihm werden "
|
||||||
|
"daher keine Positionen zugeordnet.\n\nTrotzdem anlegen?"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.app.db.haendler_speichern(Haendler(0, name, standard_prozent=proz), kenn)
|
||||||
|
except Exception as exc:
|
||||||
|
msg = str(exc)
|
||||||
|
if "UNIQUE" in msg:
|
||||||
|
msg = f"Es gibt bereits einen Händler „{name}“."
|
||||||
|
w.fehler(self, "Händler", msg)
|
||||||
|
return False
|
||||||
|
for v in (self.v_name, self.v_fn, self.v_vf):
|
||||||
|
v.set("")
|
||||||
|
self.v_proz.set("100")
|
||||||
|
self._zeige(self.nr) # neu aufbauen (GROSSBUCHSTABEN-Regel evtl. vergeben)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _haendler_weg(self):
|
||||||
|
sel = self.lb.curselection()
|
||||||
|
if sel:
|
||||||
|
self.app.db.haendler_loeschen(self._lb_ids[sel[0]])
|
||||||
|
self._zeige(self.nr)
|
||||||
|
|
||||||
|
def _vorschlagen(self):
|
||||||
|
dateien = filedialog.askopenfilenames(parent=self, title="Abrechnung zum Auslesen der Nummern wählen",
|
||||||
|
filetypes=DATEITYPEN)
|
||||||
|
if not dateien:
|
||||||
|
return
|
||||||
|
anzahl: dict[str, Counter] = defaultdict(Counter)
|
||||||
|
summe: dict[tuple[str, str], int] = defaultdict(int)
|
||||||
|
caps = 0
|
||||||
|
fehler = []
|
||||||
|
for f in dateien:
|
||||||
|
try:
|
||||||
|
erg = lies_datei(f)
|
||||||
|
except Exception as exc:
|
||||||
|
fehler.append(f"{Path(f).name}: {exc}")
|
||||||
|
continue
|
||||||
|
if f not in self.dateien:
|
||||||
|
self.dateien.append(f)
|
||||||
|
for p in erg.positionen:
|
||||||
|
anzahl[p.anbieter][p.kennung] += 1
|
||||||
|
summe[(p.anbieter, p.kennung)] += p.betrag_cent
|
||||||
|
if p.anbieter == "vodafone" and ist_grossgeschrieben(p.name):
|
||||||
|
caps += 1
|
||||||
|
vergeben = self.app.db.kennung_map()
|
||||||
|
for anb in ("freenet", "vodafone"):
|
||||||
|
self.vorschlaege[anb] = [f"{k} ({n} Pos., {fmt(summe[(anb, k)])})"
|
||||||
|
for k, n in anzahl[anb].most_common() if k and (anb, k) not in vergeben]
|
||||||
|
self.caps_anzahl = caps
|
||||||
|
self._zeige(self.nr)
|
||||||
|
if fehler:
|
||||||
|
w.warnung(self, "Nummern vorschlagen", "Nicht lesbar:\n" + "\n".join(fehler))
|
||||||
|
else:
|
||||||
|
w.info(self, "Nummern vorschlagen",
|
||||||
|
"Die gefundenen Nummern stehen jetzt in den Auswahllisten (▼) bei freenet und Vodafone – "
|
||||||
|
"sortiert nach Anzahl der Positionen.\n\nDie Nummer mit den meisten Positionen ist meist "
|
||||||
|
"Ihre eigene Filiale.\n\nDie Dateien werden im nächsten Schritt gleich mit importiert.")
|
||||||
|
|
||||||
|
def _pruefe_2(self) -> bool:
|
||||||
|
if self.v_name.get().strip():
|
||||||
|
if w.frage(self, "Händler", f"„{self.v_name.get().strip()}“ wurde noch nicht hinzugefügt.\n\n"
|
||||||
|
"Jetzt hinzufügen?"):
|
||||||
|
return self._haendler_dazu() and False # Seite bleibt offen, Liste ist aktualisiert
|
||||||
|
if not self.app.db.haendler_liste():
|
||||||
|
return w.frage(self, "Händler", "Es ist noch kein Händler angelegt.\n\n"
|
||||||
|
"Trotzdem weiter? (Händler lassen sich später unter „Händler“ anlegen.)",
|
||||||
|
default_ja=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Seite 4
|
||||||
|
def _s_abrechnung(self):
|
||||||
|
self._ueberschrift("Die erste Abrechnung")
|
||||||
|
self._text("Pro Abrechnungszeitraum (meist ein Monat) legen Sie eine Abrechnung an und importieren "
|
||||||
|
"dort alle Dateien dieses Zeitraums – z. B. alle freenet-Wochenlisten und die Vodafone-Datei.")
|
||||||
|
tk.Checkbutton(self.seite, text="Jetzt eine Abrechnung anlegen", variable=self.v_abrechnung,
|
||||||
|
command=self._s_abrechnung_status).pack(anchor="w")
|
||||||
|
f = tk.Frame(self.seite, bg=w.FACE)
|
||||||
|
f.pack(fill="both", expand=True, padx=(22, 0))
|
||||||
|
z = tk.Frame(f, bg=w.FACE)
|
||||||
|
z.pack(fill="x", pady=4)
|
||||||
|
tk.Label(z, text="Bezeichnung:").pack(side="left")
|
||||||
|
self._e_titel = tk.Entry(z, textvariable=self.v_titel, width=28)
|
||||||
|
self._e_titel.pack(side="left", padx=6)
|
||||||
|
tk.Label(f, text="Diese Dateien gleich importieren (optional):", anchor="w").pack(fill="x", pady=(6, 2))
|
||||||
|
lf = tk.Frame(f, bg=w.FACE)
|
||||||
|
lf.pack(fill="both", expand=True)
|
||||||
|
self.lb_d = tk.Listbox(lf, height=5)
|
||||||
|
self.lb_d.pack(side="left", fill="both", expand=True)
|
||||||
|
kf = tk.Frame(lf, bg=w.FACE)
|
||||||
|
kf.pack(side="left", anchor="n", padx=(6, 0))
|
||||||
|
self._b_dazu = w.knopf(kf, "Hinzufügen…", self._datei_dazu, breite=11)
|
||||||
|
self._b_dazu.pack(pady=(0, 4))
|
||||||
|
self._b_weg = w.knopf(kf, "Entfernen", self._datei_weg, breite=11)
|
||||||
|
self._b_weg.pack()
|
||||||
|
for d in self.dateien:
|
||||||
|
self.lb_d.insert("end", Path(d).name)
|
||||||
|
self._s_abrechnung_status()
|
||||||
|
|
||||||
|
def _s_abrechnung_status(self):
|
||||||
|
st = "normal" if self.v_abrechnung.get() else "disabled"
|
||||||
|
for wdg in (self._e_titel, self.lb_d, self._b_dazu, self._b_weg):
|
||||||
|
wdg.configure(state=st)
|
||||||
|
|
||||||
|
def _datei_dazu(self):
|
||||||
|
for f in filedialog.askopenfilenames(parent=self, title="Dateien zum Import wählen", filetypes=DATEITYPEN):
|
||||||
|
if f not in self.dateien:
|
||||||
|
self.dateien.append(f)
|
||||||
|
self.lb_d.insert("end", Path(f).name)
|
||||||
|
|
||||||
|
def _datei_weg(self):
|
||||||
|
sel = self.lb_d.curselection()
|
||||||
|
if sel:
|
||||||
|
del self.dateien[sel[0]]
|
||||||
|
self.lb_d.delete(sel[0])
|
||||||
|
|
||||||
|
def _pruefe_3(self) -> bool:
|
||||||
|
if self.v_abrechnung.get() and not self.v_titel.get().strip():
|
||||||
|
w.warnung(self, "Abrechnung", "Bitte eine Bezeichnung eingeben, z. B. „Oktober 2026“.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Seite 5
|
||||||
|
def _s_fertig(self):
|
||||||
|
self._ueberschrift("Fertig!")
|
||||||
|
n = len(self.app.db.haendler_liste())
|
||||||
|
self._text(f"Der {APP_NAME} ist eingerichtet.")
|
||||||
|
self._text(f" • {n} Händler angelegt")
|
||||||
|
if self.v_abrechnung.get():
|
||||||
|
self._text(f" • Abrechnung „{self.v_titel.get().strip()}“"
|
||||||
|
+ (f" mit {len(self.dateien)} Datei(en)" if self.dateien else " (noch ohne Dateien)"))
|
||||||
|
self._text("\nSo geht es danach jeden Monat weiter:\n"
|
||||||
|
" Neu → Import → prüfen & anpassen → Export")
|
||||||
|
tk.Checkbutton(self.seite, text="Kurzen Rundgang durch das Programm starten", variable=self.v_rundgang
|
||||||
|
).pack(anchor="w", pady=(12, 0))
|
||||||
|
self._text("\nKlicken Sie auf „Fertig stellen“.", fg=w.NAVY)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================== Rundgang
|
||||||
|
|
||||||
|
class Rundgang:
|
||||||
|
"""Führt mit gelben Hinweis-Sprechblasen durch die Oberfläche und rahmt das jeweilige Element rot ein."""
|
||||||
|
|
||||||
|
def __init__(self, app, schritte: list[tuple]):
|
||||||
|
"""schritte: (widget_oder_funktion, titel, text)"""
|
||||||
|
self.app, self.root, self.schritte = app, app.root, schritte
|
||||||
|
self.nr = 0
|
||||||
|
self.blase: tk.Toplevel | None = None
|
||||||
|
self.rahmen: list[tk.Toplevel] = []
|
||||||
|
self._zeige(0)
|
||||||
|
|
||||||
|
def _weg(self):
|
||||||
|
for r in self.rahmen:
|
||||||
|
r.destroy()
|
||||||
|
self.rahmen = []
|
||||||
|
if self.blase is not None:
|
||||||
|
try:
|
||||||
|
self.blase.grab_release()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
self.blase.destroy()
|
||||||
|
self.blase = None
|
||||||
|
|
||||||
|
def beenden(self):
|
||||||
|
self._weg()
|
||||||
|
|
||||||
|
def _markieren(self, x, y, bw, bh, dicke=3, farbe="#ff0000"):
|
||||||
|
for gx, gy, gw, gh in ((x - dicke, y - dicke, bw + 2 * dicke, dicke), (x - dicke, y + bh, bw + 2 * dicke, dicke),
|
||||||
|
(x - dicke, y, dicke, bh), (x + bw, y, dicke, bh)):
|
||||||
|
t = tk.Toplevel(self.root, bg=farbe)
|
||||||
|
t.overrideredirect(True)
|
||||||
|
t.transient(self.root)
|
||||||
|
t.geometry(f"{max(1, gw)}x{max(1, gh)}+{gx}+{gy}")
|
||||||
|
self.rahmen.append(t)
|
||||||
|
|
||||||
|
def _zeige(self, nr: int):
|
||||||
|
self._weg()
|
||||||
|
self.nr = nr
|
||||||
|
ziel, titel, text = self.schritte[nr]
|
||||||
|
if callable(ziel) and not isinstance(ziel, tk.Misc):
|
||||||
|
ziel = ziel()
|
||||||
|
self.root.update_idletasks()
|
||||||
|
x, y = ziel.winfo_rootx(), ziel.winfo_rooty()
|
||||||
|
bw, bh = ziel.winfo_width(), ziel.winfo_height()
|
||||||
|
self._markieren(x, y, bw, bh)
|
||||||
|
|
||||||
|
b = self.blase = tk.Toplevel(self.root, bg=w.DARK)
|
||||||
|
b.withdraw()
|
||||||
|
b.overrideredirect(True)
|
||||||
|
b.transient(self.root)
|
||||||
|
innen = tk.Frame(b, bg=w.INFO_BG, padx=10, pady=8)
|
||||||
|
innen.pack(padx=1, pady=1)
|
||||||
|
kopf = tk.Frame(innen, bg=w.INFO_BG)
|
||||||
|
kopf.pack(fill="x")
|
||||||
|
self._ic = w.make_icon(b, "birne")
|
||||||
|
tk.Label(kopf, image=self._ic, bg=w.INFO_BG).pack(side="left")
|
||||||
|
tk.Label(kopf, text=titel, bg=w.INFO_BG, font=w.FONT_BOLD).pack(side="left", padx=6)
|
||||||
|
tk.Label(innen, text=text, bg=w.INFO_BG, justify="left", anchor="w", wraplength=330).pack(fill="x", pady=(6, 8))
|
||||||
|
fuss = tk.Frame(innen, bg=w.INFO_BG)
|
||||||
|
fuss.pack(fill="x")
|
||||||
|
tk.Label(fuss, text=f"Schritt {nr + 1} von {len(self.schritte)}", bg=w.INFO_BG,
|
||||||
|
fg=w.SHADOW).pack(side="left")
|
||||||
|
letzter = nr == len(self.schritte) - 1
|
||||||
|
w.knopf(fuss, "Fertig" if letzter else "Weiter >", self._weiter, default="active").pack(side="right")
|
||||||
|
zb = w.knopf(fuss, "< Zurück", self._zurueck)
|
||||||
|
zb.pack(side="right", padx=4)
|
||||||
|
if nr == 0:
|
||||||
|
zb.configure(state="disabled")
|
||||||
|
if not letzter:
|
||||||
|
w.knopf(fuss, "Beenden", self.beenden).pack(side="right", padx=(0, 8))
|
||||||
|
b.bind("<Return>", lambda e: self._weiter())
|
||||||
|
b.bind("<Right>", lambda e: self._weiter())
|
||||||
|
b.bind("<Left>", lambda e: self._zurueck())
|
||||||
|
b.bind("<Escape>", lambda e: self.beenden())
|
||||||
|
|
||||||
|
# Position: unter dem Element, sonst darüber, sonst innen oben links; immer im Hauptfenster
|
||||||
|
b.update_idletasks()
|
||||||
|
pw, ph = b.winfo_reqwidth(), b.winfo_reqheight()
|
||||||
|
rx, ry = self.root.winfo_rootx(), self.root.winfo_rooty()
|
||||||
|
rw, rh = self.root.winfo_width(), self.root.winfo_height()
|
||||||
|
if y + bh + 12 + ph <= ry + rh:
|
||||||
|
px, py = x, y + bh + 12
|
||||||
|
elif y - 12 - ph >= ry:
|
||||||
|
px, py = x, y - 12 - ph
|
||||||
|
else:
|
||||||
|
px, py = x + 40, y + 40
|
||||||
|
px = min(max(rx + 8, px), rx + rw - pw - 8)
|
||||||
|
py = min(max(ry + 8, py), ry + rh - ph - 8)
|
||||||
|
b.geometry(f"{pw}x{ph}+{px}+{py}")
|
||||||
|
b.deiconify()
|
||||||
|
b.lift()
|
||||||
|
try:
|
||||||
|
b.grab_set()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
b.focus_force()
|
||||||
|
|
||||||
|
def _weiter(self):
|
||||||
|
if self.nr >= len(self.schritte) - 1:
|
||||||
|
self.beenden()
|
||||||
|
else:
|
||||||
|
self._zeige(self.nr + 1)
|
||||||
|
|
||||||
|
def _zurueck(self):
|
||||||
|
if self.nr > 0:
|
||||||
|
self._zeige(self.nr - 1)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================== Tipp des Tages
|
||||||
|
|
||||||
|
TIPPS = [
|
||||||
|
"Sie können mehrere Dateien gleichzeitig importieren – zum Beispiel alle freenet-Wochenlisten eines Monats "
|
||||||
|
"und die Vodafone-Datei in einem Rutsch.",
|
||||||
|
"Markieren Sie eine ganze Provisionsart (die graue Zeile) und klicken Sie auf „Prozent…“, um z. B. nur 80 % "
|
||||||
|
"auszuzahlen. Mit dem Häkchen „Als Standard merken“ gilt das automatisch auch in den nächsten Monaten.",
|
||||||
|
"Doppelklick auf eine Position – und Sie können den Auszahlungsbetrag direkt überschreiben. Mit ↓ geht es "
|
||||||
|
"gleich in die nächste Zeile.",
|
||||||
|
"Blau dargestellte Beträge wurden von Hand geändert, rote weichen durch eine Prozent-Regel von der "
|
||||||
|
"Provision ab.",
|
||||||
|
"Eine Position soll gar nicht ausgezahlt werden? Rechtsklick ▸ „Auszahlung 0 €“.",
|
||||||
|
"Die GROSSBUCHSTABEN-Regel hat einen Kunden falsch erwischt? Rechtsklick ▸ „Händler zuweisen…“ – diese "
|
||||||
|
"Zuordnung hat immer Vorrang.",
|
||||||
|
"Unter „(Nicht zugeordnet)“ sehen Sie alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
|
||||||
|
"Diese Positionen werden nicht exportiert.",
|
||||||
|
"Mit Strg+F springen Sie ins Suchfeld. Gesucht wird in Rufnummer, Name, Tarif, Provisionsart und mehr.",
|
||||||
|
"Wurde eine Datei versehentlich doppelt oder falsch importiert? Datei ▸ Importierte Dateien… – dort lässt "
|
||||||
|
"sie sich wieder entfernen.",
|
||||||
|
"Erstellen Sie ab und zu eine Datensicherung: Datei ▸ Datensicherung erstellen…",
|
||||||
|
"Alle Änderungen werden sofort gespeichert. Ein „Speichern“-Knopf ist daher nicht nötig.",
|
||||||
|
"Ein Händler wurde erst nach dem Import angelegt? Kein Problem – die Zuordnung passt sich sofort an.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TippDesTages(w.Dialog):
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app.root, "Tipp des Tages")
|
||||||
|
self.app = app
|
||||||
|
haupt = tk.Frame(self, bg=w.FACE)
|
||||||
|
haupt.pack(padx=12, pady=12)
|
||||||
|
box = tk.Frame(haupt, bg=w.WHITE, bd=2, relief="sunken")
|
||||||
|
box.pack(side="left", fill="both")
|
||||||
|
links = tk.Frame(box, bg=w.SHADOW, width=60)
|
||||||
|
links.pack(side="left", fill="y")
|
||||||
|
self._ic = w.make_icon(self, "birne", 3)
|
||||||
|
tk.Label(links, image=self._ic, bg=w.SHADOW).pack(padx=6, pady=10)
|
||||||
|
rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10)
|
||||||
|
rechts.pack(side="left", fill="both")
|
||||||
|
tk.Label(rechts, text="Wussten Sie schon…", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"),
|
||||||
|
anchor="w").pack(fill="x")
|
||||||
|
tk.Frame(rechts, bg=w.DARK, height=1).pack(fill="x", pady=6)
|
||||||
|
self.text = tk.Label(rechts, bg=w.WHITE, justify="left", anchor="nw", wraplength=330, width=48, height=6)
|
||||||
|
self.text.pack(fill="both")
|
||||||
|
knoepfe = tk.Frame(haupt, bg=w.FACE)
|
||||||
|
knoepfe.pack(side="left", anchor="n", padx=(10, 0))
|
||||||
|
w.knopf(knoepfe, "Schließen", self.abbrechen, breite=12, default="active").pack(pady=(0, 6))
|
||||||
|
w.knopf(knoepfe, "Nächster Tipp", self._naechster, breite=12).pack()
|
||||||
|
self.v_start = tk.BooleanVar(value=app.settings.get("tipps_beim_start", True))
|
||||||
|
tk.Checkbutton(self, text="Tipps beim Start anzeigen", variable=self.v_start).pack(anchor="w", padx=12,
|
||||||
|
pady=(0, 10))
|
||||||
|
self.bind("<Return>", lambda e: self.abbrechen())
|
||||||
|
self._naechster()
|
||||||
|
|
||||||
|
def _naechster(self):
|
||||||
|
nr = self.app.settings.get("tipp_nr", 0) % len(TIPPS)
|
||||||
|
self.text.configure(text=TIPPS[nr])
|
||||||
|
self.app.settings["tipp_nr"] = nr + 1
|
||||||
|
|
||||||
|
def abbrechen(self):
|
||||||
|
self.app.settings["tipps_beim_start"] = self.v_start.get()
|
||||||
|
self.destroy()
|
||||||
@@ -641,6 +641,12 @@ class EinstellungenDialog(w.Dialog):
|
|||||||
tk.Label(g, text="Tipp: Liegt die Datei auf einem Netzlaufwerk, können mehrere PCs dieselben\n"
|
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.",
|
"Händler nutzen – aber bitte nicht gleichzeitig damit arbeiten.",
|
||||||
justify="left", fg=w.SHADOW).grid(row=2, column=0, columnspan=2, sticky="w")
|
justify="left", fg=w.SHADOW).grid(row=2, column=0, columnspan=2, sticky="w")
|
||||||
|
g2 = w.gruppe(self, "Darstellung")
|
||||||
|
g2.pack(fill="x", padx=10, pady=(0, 10))
|
||||||
|
self.v_klassisch = tk.BooleanVar(value=bool(settings.get("klassischer_rahmen", True)))
|
||||||
|
tk.Checkbutton(g2, text="Windows-95-Titelleiste verwenden (sonst die normale von Windows)",
|
||||||
|
variable=self.v_klassisch).pack(anchor="w")
|
||||||
|
tk.Label(g2, text="Wirkt beim nächsten Programmstart.", 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):
|
||||||
@@ -651,7 +657,7 @@ class EinstellungenDialog(w.Dialog):
|
|||||||
self.v.set(p)
|
self.v.set(p)
|
||||||
|
|
||||||
def _ok(self):
|
def _ok(self):
|
||||||
self.result = self.v.get().strip()
|
self.result = dict(pfad=self.v.get().strip(), klassisch=self.v_klassisch.get())
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
"""Selbst gezeichneter Windows-95-Fensterrahmen.
|
||||||
|
|
||||||
|
Windows erlaubt Tk nicht, die echte Titelleiste umzufärben. Deshalb wird sie ausgeblendet
|
||||||
|
(overrideredirect) und hier nachgebaut: blaue Titelleiste mit Icon und _ □ x, Verschieben,
|
||||||
|
Doppelklick = Maximieren, Größenänderung über den Rand und den Anfasser unten rechts,
|
||||||
|
graue Menüleiste. Unter Windows sorgen ein paar Win32-Aufrufe dafür, dass das Fenster
|
||||||
|
trotzdem in der Taskleiste erscheint, per Alt+Tab erreichbar ist und minimiert werden kann.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from provtool.gui import win95 as w
|
||||||
|
|
||||||
|
IS_WIN = sys.platform.startswith("win")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Glyphen
|
||||||
|
|
||||||
|
GLYPHEN = {
|
||||||
|
"min": """
|
||||||
|
........
|
||||||
|
........
|
||||||
|
........
|
||||||
|
........
|
||||||
|
........
|
||||||
|
........
|
||||||
|
kkkkkk..
|
||||||
|
kkkkkk..""",
|
||||||
|
"max": """
|
||||||
|
kkkkkkkkk
|
||||||
|
kkkkkkkkk
|
||||||
|
k.......k
|
||||||
|
k.......k
|
||||||
|
k.......k
|
||||||
|
k.......k
|
||||||
|
k.......k
|
||||||
|
kkkkkkkkk""",
|
||||||
|
"restore": """
|
||||||
|
..kkkkkk
|
||||||
|
..kkkkkk
|
||||||
|
..k....k
|
||||||
|
kkkkkk.k
|
||||||
|
kkkkkk.k
|
||||||
|
k....kkk
|
||||||
|
k....k..
|
||||||
|
kkkkkk..""",
|
||||||
|
"close": """
|
||||||
|
kk....kk
|
||||||
|
.kk..kk.
|
||||||
|
..kkkk..
|
||||||
|
...kk...
|
||||||
|
..kkkk..
|
||||||
|
.kk..kk.
|
||||||
|
kk....kk""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def glyph(master, name: str) -> tk.PhotoImage:
|
||||||
|
rows = [r.strip() for r in GLYPHEN[name].strip("\n").splitlines() if r.strip()]
|
||||||
|
breite = max(len(r) for r in rows)
|
||||||
|
img = tk.PhotoImage(master=master, width=breite, height=len(rows))
|
||||||
|
for y, row in enumerate(rows):
|
||||||
|
for x, ch in enumerate(row):
|
||||||
|
if ch != ".":
|
||||||
|
img.put(w.PALETTE[ch], (x, y))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def griff_bild(master, groesse: int = 12) -> tk.PhotoImage:
|
||||||
|
"""Der typische schraffierte Größen-Anfasser unten rechts."""
|
||||||
|
img = tk.PhotoImage(master=master, width=groesse, height=groesse)
|
||||||
|
for y in range(groesse):
|
||||||
|
for x in range(groesse):
|
||||||
|
d = x + y - (groesse - 1)
|
||||||
|
if d >= 0:
|
||||||
|
farbe = (w.HILITE, w.SHADOW, w.SHADOW, None)[d % 4]
|
||||||
|
if farbe:
|
||||||
|
img.put(farbe, (x, y))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Win32-Hilfen
|
||||||
|
|
||||||
|
def _hwnd(fenster: tk.Misc) -> int:
|
||||||
|
import ctypes
|
||||||
|
return ctypes.windll.user32.GetParent(fenster.winfo_id())
|
||||||
|
|
||||||
|
|
||||||
|
def taskleiste_anzeigen(fenster: tk.Tk) -> None:
|
||||||
|
"""Ein overrideredirect-Fenster erscheint unter Windows sonst nicht in Taskleiste/Alt+Tab."""
|
||||||
|
if not IS_WIN:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
GWL_EXSTYLE, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW = -20, 0x00040000, 0x00000080
|
||||||
|
hwnd = _hwnd(fenster)
|
||||||
|
stil = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
||||||
|
stil = (stil & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
|
||||||
|
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, stil)
|
||||||
|
fenster.wm_withdraw()
|
||||||
|
fenster.after(10, lambda: (fenster.wm_deiconify(), fenster.focus_force()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def arbeitsbereich(fenster: tk.Misc) -> tuple[int, int, int, int]:
|
||||||
|
"""Bildschirmbereich ohne Taskleiste (des Monitors, auf dem das Fenster steht): x, y, breite, höhe."""
|
||||||
|
if IS_WIN:
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
class MONITORINFO(ctypes.Structure):
|
||||||
|
_fields_ = [("cbSize", wintypes.DWORD), ("rcMonitor", wintypes.RECT),
|
||||||
|
("rcWork", wintypes.RECT), ("dwFlags", wintypes.DWORD)]
|
||||||
|
|
||||||
|
mon = ctypes.windll.user32.MonitorFromWindow(_hwnd(fenster), 2) # MONITOR_DEFAULTTONEAREST
|
||||||
|
mi = MONITORINFO()
|
||||||
|
mi.cbSize = ctypes.sizeof(MONITORINFO)
|
||||||
|
if ctypes.windll.user32.GetMonitorInfoW(mon, ctypes.byref(mi)):
|
||||||
|
r = mi.rcWork
|
||||||
|
return r.left, r.top, r.right - r.left, r.bottom - r.top
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0, 0, fenster.winfo_screenwidth(), fenster.winfo_screenheight()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Bausteine
|
||||||
|
|
||||||
|
class TitelKnopf(tk.Label):
|
||||||
|
def __init__(self, master, bild: tk.PhotoImage, command):
|
||||||
|
super().__init__(master, image=bild, bg=w.FACE, bd=2, relief="raised", width=14, height=12)
|
||||||
|
self.command = command
|
||||||
|
self.bind("<ButtonPress-1>", lambda e: self.configure(relief="sunken"))
|
||||||
|
self.bind("<ButtonRelease-1>", self._los)
|
||||||
|
|
||||||
|
def _los(self, e):
|
||||||
|
self.configure(relief="raised")
|
||||||
|
if 0 <= e.x < self.winfo_width() and 0 <= e.y < self.winfo_height():
|
||||||
|
self.command()
|
||||||
|
|
||||||
|
|
||||||
|
class Titelleiste(tk.Frame):
|
||||||
|
"""Blaue Win95-Titelleiste. knoepfe: Teilmenge von ("min", "max", "close")."""
|
||||||
|
|
||||||
|
def __init__(self, master, fenster: tk.Misc, titel: str, icon: tk.PhotoImage | None, knoepfe=("close",),
|
||||||
|
schliessen=None, minimieren=None, maximieren=None):
|
||||||
|
super().__init__(master, bg=w.NAVY, height=20)
|
||||||
|
self.fenster = fenster
|
||||||
|
self._bilder = {n: glyph(self, n) for n in ("min", "max", "restore", "close")}
|
||||||
|
if icon is not None:
|
||||||
|
tk.Label(self, image=icon, bg=w.NAVY).pack(side="left", padx=(2, 0))
|
||||||
|
self.titel_var = tk.StringVar(value=titel)
|
||||||
|
self.titel_lbl = tk.Label(self, textvariable=self.titel_var, bg=w.NAVY, fg=w.WHITE, font=w.FONT_BOLD,
|
||||||
|
anchor="w")
|
||||||
|
self.titel_lbl.pack(side="left", fill="x", expand=True, padx=3)
|
||||||
|
self.knoepfe: dict[str, TitelKnopf] = {}
|
||||||
|
if "close" in knoepfe:
|
||||||
|
self.knoepfe["close"] = TitelKnopf(self, self._bilder["close"], schliessen)
|
||||||
|
self.knoepfe["close"].pack(side="right", padx=(2, 2), pady=2)
|
||||||
|
if "max" in knoepfe:
|
||||||
|
self.knoepfe["max"] = TitelKnopf(self, self._bilder["max"], maximieren)
|
||||||
|
self.knoepfe["max"].pack(side="right", pady=2)
|
||||||
|
if "min" in knoepfe:
|
||||||
|
self.knoepfe["min"] = TitelKnopf(self, self._bilder["min"], minimieren)
|
||||||
|
self.knoepfe["min"].pack(side="right", pady=2)
|
||||||
|
self._maximieren = maximieren
|
||||||
|
for widget in (self, self.titel_lbl):
|
||||||
|
widget.bind("<ButtonPress-1>", self._start)
|
||||||
|
widget.bind("<B1-Motion>", self._ziehen)
|
||||||
|
if maximieren:
|
||||||
|
widget.bind("<Double-Button-1>", lambda e: maximieren())
|
||||||
|
self._ab = (0, 0)
|
||||||
|
self.ziehen_erlaubt = lambda: True
|
||||||
|
|
||||||
|
def set_titel(self, t: str):
|
||||||
|
self.titel_var.set(t)
|
||||||
|
|
||||||
|
def max_symbol(self, maximiert: bool):
|
||||||
|
if "max" in self.knoepfe:
|
||||||
|
self.knoepfe["max"].configure(image=self._bilder["restore" if maximiert else "max"])
|
||||||
|
|
||||||
|
def _start(self, e):
|
||||||
|
self._ab = (e.x_root - self.fenster.winfo_rootx(), e.y_root - self.fenster.winfo_rooty())
|
||||||
|
|
||||||
|
def _ziehen(self, e):
|
||||||
|
if not self.ziehen_erlaubt():
|
||||||
|
return
|
||||||
|
self.fenster.geometry(f"+{e.x_root - self._ab[0]}+{e.y_root - self._ab[1]}")
|
||||||
|
|
||||||
|
|
||||||
|
class Menueleiste(tk.Frame):
|
||||||
|
"""Graue Menüleiste (die native von Windows 10/11 ist weiß und lässt sich nicht umfärben)."""
|
||||||
|
|
||||||
|
def __init__(self, master, root: tk.Misc):
|
||||||
|
super().__init__(master, bg=w.FACE)
|
||||||
|
self.root = root
|
||||||
|
self.knoepfe: dict[str, tk.Menubutton] = {}
|
||||||
|
|
||||||
|
def menue(self, label: str, underline: int | None = 0) -> tk.Menu:
|
||||||
|
mb = tk.Menubutton(self, text=label, bg=w.FACE, relief="flat", bd=1, padx=6, pady=1,
|
||||||
|
activebackground=w.NAVY, activeforeground=w.WHITE,
|
||||||
|
underline=-1 if underline is None else underline)
|
||||||
|
menu = tk.Menu(mb, tearoff=0)
|
||||||
|
mb.configure(menu=menu)
|
||||||
|
mb.pack(side="left")
|
||||||
|
self.knoepfe[label] = mb
|
||||||
|
if underline is not None:
|
||||||
|
taste = label[underline].lower()
|
||||||
|
self.root.bind_all(f"<Alt-KeyPress-{taste}>", lambda e, m=mb: self.oeffnen(m))
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def oeffnen(self, mb: tk.Menubutton):
|
||||||
|
menu = mb.nametowidget(mb.cget("menu"))
|
||||||
|
menu.tk_popup(mb.winfo_rootx(), mb.winfo_rooty() + mb.winfo_height())
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Hauptfenster
|
||||||
|
|
||||||
|
class Hauptrahmen:
|
||||||
|
"""Umgibt das Hauptfenster. Im klassischen Modus mit eigener Titelleiste, sonst nur ein Container."""
|
||||||
|
|
||||||
|
RAND = 5 # Pixel am Rand, an denen man die Größe ziehen kann
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk, titel: str, icon: tk.PhotoImage, schliessen, klassisch: bool = True,
|
||||||
|
minbreite: int = 900, minhoehe: int = 520):
|
||||||
|
self.root, self.klassisch = root, klassisch
|
||||||
|
self.minbreite, self.minhoehe = minbreite, minhoehe
|
||||||
|
self.maximiert = False
|
||||||
|
self._normal_geo: str | None = None
|
||||||
|
self._schliessen = schliessen
|
||||||
|
root.minsize(minbreite, minhoehe)
|
||||||
|
if not klassisch:
|
||||||
|
self.aussen = tk.Frame(root, bg=w.FACE)
|
||||||
|
self.aussen.pack(fill="both", expand=True)
|
||||||
|
self.inhalt = self.aussen
|
||||||
|
self.titelleiste = None
|
||||||
|
return
|
||||||
|
|
||||||
|
root.overrideredirect(True)
|
||||||
|
self.aussen = tk.Frame(root, bg=w.FACE, bd=2, relief="raised", padx=2, pady=2)
|
||||||
|
self.aussen.pack(fill="both", expand=True)
|
||||||
|
self.titelleiste = Titelleiste(self.aussen, root, titel, icon, ("min", "max", "close"),
|
||||||
|
schliessen=schliessen, minimieren=self.minimieren,
|
||||||
|
maximieren=self.umschalten)
|
||||||
|
self.titelleiste.pack(fill="x")
|
||||||
|
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert
|
||||||
|
self.inhalt = tk.Frame(self.aussen, bg=w.FACE)
|
||||||
|
self.inhalt.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
self._rand: str = ""
|
||||||
|
self.aussen.bind("<Motion>", self._rand_cursor)
|
||||||
|
self.aussen.bind("<Leave>", lambda e: self.aussen.configure(cursor=""))
|
||||||
|
self.aussen.bind("<ButtonPress-1>", self._resize_start)
|
||||||
|
self.aussen.bind("<B1-Motion>", self._resize)
|
||||||
|
root.bind_all("<Alt-F4>", lambda e: schliessen())
|
||||||
|
root.after(50, lambda: taskleiste_anzeigen(root))
|
||||||
|
|
||||||
|
# ---- öffentliche Helfer
|
||||||
|
def set_titel(self, text: str):
|
||||||
|
self.root.title(text)
|
||||||
|
if self.titelleiste:
|
||||||
|
self.titelleiste.set_titel(text)
|
||||||
|
|
||||||
|
def griff(self, master) -> tk.Label | None:
|
||||||
|
"""Anfasser für die Statusleiste (nur im klassischen Modus)."""
|
||||||
|
if not self.klassisch:
|
||||||
|
return None
|
||||||
|
self._griff_bild = griff_bild(master)
|
||||||
|
g = tk.Label(master, image=self._griff_bild, bg=w.FACE, cursor="sizing" if not IS_WIN else "size_nw_se")
|
||||||
|
g.bind("<ButtonPress-1>", lambda e: self._resize_start(e, "se"))
|
||||||
|
g.bind("<B1-Motion>", self._resize)
|
||||||
|
return g
|
||||||
|
|
||||||
|
def geometrie_setzen(self, geo: str | None, maximiert: bool = False):
|
||||||
|
self.root.update_idletasks()
|
||||||
|
if not geo or "+" not in geo:
|
||||||
|
breite, hoehe = 1200, 720
|
||||||
|
if geo and "x" in geo:
|
||||||
|
try:
|
||||||
|
breite, hoehe = (int(v) for v in geo.split("+")[0].split("x"))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
x0, y0, bw, bh = arbeitsbereich(self.root)
|
||||||
|
breite, hoehe = min(breite, bw), min(hoehe, bh)
|
||||||
|
geo = f"{breite}x{hoehe}+{x0 + (bw - breite) // 2}+{y0 + (bh - hoehe) // 2}"
|
||||||
|
self.root.geometry(geo)
|
||||||
|
if maximiert and self.klassisch:
|
||||||
|
self.root.after(100, self.maximieren)
|
||||||
|
|
||||||
|
def normale_geometrie(self) -> str:
|
||||||
|
return self._normal_geo if self.maximiert and self._normal_geo else self.root.geometry()
|
||||||
|
|
||||||
|
# ---- Fensterfunktionen
|
||||||
|
def minimieren(self):
|
||||||
|
# Windows minimiert keine overrideredirect-Fenster → kurz normalen Rahmen aktivieren
|
||||||
|
geo = self.root.geometry()
|
||||||
|
self.root.overrideredirect(False)
|
||||||
|
self.root.iconify()
|
||||||
|
|
||||||
|
def wieder_da(e):
|
||||||
|
if e.widget is self.root and self.root.state() == "normal":
|
||||||
|
self.root.unbind("<Map>")
|
||||||
|
self.root.overrideredirect(True)
|
||||||
|
self.root.geometry(geo)
|
||||||
|
taskleiste_anzeigen(self.root)
|
||||||
|
self.root.bind("<Map>", wieder_da)
|
||||||
|
|
||||||
|
def maximieren(self):
|
||||||
|
if self.maximiert:
|
||||||
|
return
|
||||||
|
self._normal_geo = self.root.geometry()
|
||||||
|
x, y, bw, bh = arbeitsbereich(self.root)
|
||||||
|
self.root.geometry(f"{bw}x{bh}+{x}+{y}")
|
||||||
|
self.maximiert = True
|
||||||
|
self.titelleiste.max_symbol(True)
|
||||||
|
|
||||||
|
def wiederherstellen(self):
|
||||||
|
if not self.maximiert:
|
||||||
|
return
|
||||||
|
self.maximiert = False
|
||||||
|
if self._normal_geo:
|
||||||
|
self.root.geometry(self._normal_geo)
|
||||||
|
self.titelleiste.max_symbol(False)
|
||||||
|
|
||||||
|
def umschalten(self):
|
||||||
|
self.wiederherstellen() if self.maximiert else self.maximieren()
|
||||||
|
|
||||||
|
# ---- Größe ändern
|
||||||
|
def _welcher_rand(self, x, y) -> str:
|
||||||
|
bw, bh, r = self.aussen.winfo_width(), self.aussen.winfo_height(), self.RAND
|
||||||
|
rand = ("n" if y < r else "s" if y >= bh - r else "") + ("w" if x < r else "e" if x >= bw - r else "")
|
||||||
|
return rand
|
||||||
|
|
||||||
|
CURSOR_WIN = {"n": "size_ns", "s": "size_ns", "e": "size_we", "w": "size_we",
|
||||||
|
"nw": "size_nw_se", "se": "size_nw_se", "ne": "size_ne_sw", "sw": "size_ne_sw"}
|
||||||
|
CURSOR_X11 = {"n": "top_side", "s": "bottom_side", "e": "right_side", "w": "left_side",
|
||||||
|
"nw": "top_left_corner", "se": "bottom_right_corner", "ne": "top_right_corner",
|
||||||
|
"sw": "bottom_left_corner"}
|
||||||
|
|
||||||
|
def _rand_cursor(self, e):
|
||||||
|
if self.maximiert:
|
||||||
|
return
|
||||||
|
rand = self._welcher_rand(e.x, e.y)
|
||||||
|
try:
|
||||||
|
self.aussen.configure(cursor=(self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, ""))
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _resize_start(self, e, rand: str | None = None):
|
||||||
|
self._rand = rand or self._welcher_rand(e.x, e.y)
|
||||||
|
self._start = (e.x_root, e.y_root, self.root.winfo_x(), self.root.winfo_y(),
|
||||||
|
self.root.winfo_width(), self.root.winfo_height())
|
||||||
|
|
||||||
|
def _resize(self, e):
|
||||||
|
if not self._rand or self.maximiert:
|
||||||
|
return
|
||||||
|
sx, sy, x, y, bw, bh = self._start
|
||||||
|
dx, dy = e.x_root - sx, e.y_root - sy
|
||||||
|
if "e" in self._rand:
|
||||||
|
bw = max(self.minbreite, bw + dx)
|
||||||
|
if "s" in self._rand:
|
||||||
|
bh = max(self.minhoehe, bh + dy)
|
||||||
|
if "w" in self._rand:
|
||||||
|
neu = max(self.minbreite, bw - dx)
|
||||||
|
x, bw = x + (bw - neu), neu
|
||||||
|
if "n" in self._rand:
|
||||||
|
neu = max(self.minhoehe, bh - dy)
|
||||||
|
y, bh = y + (bh - neu), neu
|
||||||
|
self.root.geometry(f"{bw}x{bh}+{x}+{y}")
|
||||||
+47
-1
@@ -218,6 +218,23 @@ ICONS = {
|
|||||||
...kkwwwwwwkk...
|
...kkwwwwwwkk...
|
||||||
.....kkkkkkk....
|
.....kkkkkkk....
|
||||||
................""",
|
................""",
|
||||||
|
"birne": """
|
||||||
|
.....kkkkk......
|
||||||
|
....kyyyyyk.....
|
||||||
|
...kyywwyyyk....
|
||||||
|
..kyywwyyyyyk...
|
||||||
|
..kywwyyyyyyk...
|
||||||
|
..kyyyyyyyyyk...
|
||||||
|
..kyyyyyyyyyk...
|
||||||
|
...kyyyyyyyk....
|
||||||
|
....kyyyyyk.....
|
||||||
|
.....kyyyk......
|
||||||
|
.....kdddk......
|
||||||
|
.....kgggk......
|
||||||
|
.....kdddk......
|
||||||
|
.....kgggk......
|
||||||
|
......kkk.......
|
||||||
|
................""",
|
||||||
"app": """
|
"app": """
|
||||||
................
|
................
|
||||||
.....kkkkkkk....
|
.....kkkkkkk....
|
||||||
@@ -480,6 +497,10 @@ def auto_breite(tree: ttk.Treeview, flex: dict[str, float], reserve: int = 4):
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------- Dialoge
|
# --------------------------------------------------------------------------- Dialoge
|
||||||
|
|
||||||
|
# Klassischer Win95-Rahmen auch für Dialoge (wird von der App gesetzt, abschaltbar in den Einstellungen)
|
||||||
|
KLASSISCH = False
|
||||||
|
|
||||||
|
|
||||||
class Dialog(tk.Toplevel):
|
class Dialog(tk.Toplevel):
|
||||||
"""Modaler Dialog im Win95-Stil."""
|
"""Modaler Dialog im Win95-Stil."""
|
||||||
|
|
||||||
@@ -496,6 +517,13 @@ class Dialog(tk.Toplevel):
|
|||||||
self.iconphoto(False, master.winfo_toplevel()._app_icon) # type: ignore[attr-defined]
|
self.iconphoto(False, master.winfo_toplevel()._app_icon) # type: ignore[attr-defined]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self.titelleiste = None
|
||||||
|
if KLASSISCH:
|
||||||
|
from provtool.gui.rahmen import Titelleiste
|
||||||
|
self.overrideredirect(True)
|
||||||
|
self.configure(bd=2, relief="raised", padx=2, pady=2)
|
||||||
|
self.titelleiste = Titelleiste(self, self, titel, None, ("close",), schliessen=self.abbrechen)
|
||||||
|
self.titelleiste.pack(side="top", fill="x")
|
||||||
|
|
||||||
def zeigen(self, fokus=None):
|
def zeigen(self, fokus=None):
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
@@ -503,15 +531,33 @@ class Dialog(tk.Toplevel):
|
|||||||
w, h = self.winfo_reqwidth(), self.winfo_reqheight()
|
w, h = self.winfo_reqwidth(), self.winfo_reqheight()
|
||||||
x = m.winfo_rootx() + max(0, (m.winfo_width() - w) // 2)
|
x = m.winfo_rootx() + max(0, (m.winfo_width() - w) // 2)
|
||||||
y = m.winfo_rooty() + max(0, (m.winfo_height() - h) // 3)
|
y = m.winfo_rooty() + max(0, (m.winfo_height() - h) // 3)
|
||||||
self.geometry(f"+{x}+{y}")
|
self.geometry(f"+{max(0, x)}+{max(0, y)}")
|
||||||
self.deiconify()
|
self.deiconify()
|
||||||
self.lift()
|
self.lift()
|
||||||
|
vorher = self.grab_current() # z. B. der Assistent, wenn hier eine Rückfrage aufgeht
|
||||||
try:
|
try:
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass
|
pass
|
||||||
|
bind_id = None
|
||||||
|
if KLASSISCH:
|
||||||
|
# Ein rahmenloser Dialog darf nicht hinter dem Hauptfenster verschwinden
|
||||||
|
bind_id = m.bind("<FocusIn>", lambda e: self.winfo_exists() and self.lift(), add="+")
|
||||||
|
self.focus_force()
|
||||||
(fokus or self).focus_set()
|
(fokus or self).focus_set()
|
||||||
self.wait_window()
|
self.wait_window()
|
||||||
|
if bind_id:
|
||||||
|
try:
|
||||||
|
m.unbind("<FocusIn>", bind_id)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
# Tk gibt beim Schließen die Sperre frei, stellt die vorige aber nicht wieder her
|
||||||
|
try:
|
||||||
|
if vorher is not None and vorher.winfo_exists():
|
||||||
|
vorher.grab_set()
|
||||||
|
vorher.focus_force()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
def abbrechen(self):
|
def abbrechen(self):
|
||||||
|
|||||||
Reference in new issue
Block a user