Eigene Kennungen ausblenden, Win95-Menüs, verschiebbares Hauptfenster bei offenem Dialog u. a.
Windows-Build / windows-exe (push) Canceled after 53m28s
Windows-Build / windows-exe (push) Canceled after 53m28s
Neu: - Eigene Kennungen (eigene Filiale) ausblenden: nicht angezeigt, nicht mitgerechnet, nicht exportiert; per Rechtsklick, Händler-Menü oder Import-Bericht; Händler-Zuordnung hat Vorrang - Menüs im Win95-Stil: wechseln beim Überfahren der Menüleiste, Tastatur, Kontextmenü gleich - Dialoge sperren nur den Inhalt: Hauptfenster bleibt verschiebbar/maximierbar; Dialoge haben eigenen Taskleisten-Eintrag; Klick auf gesperrten Inhalt holt den Dialog nach vorn (blinkt) - Trennlinie zwischen Händler- und Positionsliste sichtbar und ziehbar, Position wird gemerkt - Strg+Rücktaste / Strg+Entf löschen ganze Wörter in allen Eingabefeldern Behoben: - Gedrückte Knöpfe zeigten keine Beschriftung (activeForeground gehört in Tk zur Klasse Background) - Tipp des Tages: Text lief aus dem sichtbaren Bereich - Größenänderung: breiterer Griffbereich mit Ecken, Zeiger wird zuverlässig zurückgesetzt, flüssiger; Verschieben über Windows selbst (inkl. Andocken), Minimieren über Windows - Zeichen, die MS Sans Serif nicht hat (…, ▸, →, •), durch Win95-übliche ersetzt - Menü/Tooltip/Dialog: Größe+Position zusammen setzen (Tk ignoriert sonst die Position) - Suche reagiert erst nach kurzer Tipp-Pause (flüssigeres Tippen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
69c2a108a9
commit
807a38622c
15 files changed
+1216
-186
No files matched your search
@@ -32,14 +32,19 @@ normale Windows-Titelleiste verwendet.
|
|||||||
eines Monats plus die Vodafone-Datei. Das Format wird an der Kopfzeile erkannt.
|
eines Monats plus die Vodafone-Datei. Das Format wird an der Kopfzeile erkannt.
|
||||||
Doppelt importierte Dateien werden erkannt. Unbekannte Kennungen erscheinen unter
|
Doppelt importierte Dateien werden erkannt. Unbekannte Kennungen erscheinen unter
|
||||||
„(Nicht zugeordnet)“ und lassen sich direkt einem Händler zuordnen.
|
„(Nicht zugeordnet)“ und lassen sich direkt einem Händler zuordnen.
|
||||||
4. **Prüfen und anpassen**: links Händler wählen, rechts sind die Positionen nach
|
4. **Eigene Filiale ausblenden**: Positionen eurer eigenen Kennungen (z. B. freenet `27518900`,
|
||||||
|
Vodafone `80210760`) müssen nicht verprovisioniert werden. Rechtsklick auf eine nicht zugeordnete
|
||||||
|
Gruppe ▸ *Kennung als eigene ausblenden* (oder *Händler ▸ Eigene Kennungen…* bzw. direkt im
|
||||||
|
Import-Bericht). Sie verschwinden aus Liste, Summen und Export und stehen grau unter
|
||||||
|
„(Eigene – ausgeblendet)“. Händler-Zuordnungen (auch GROSSBUCHSTABEN) haben Vorrang.
|
||||||
|
5. **Prüfen und anpassen**: links Händler wählen, rechts sind die Positionen nach
|
||||||
Provisionsart gruppiert.
|
Provisionsart gruppiert.
|
||||||
- Gruppe markieren → **Prozent…** (`F4`): Auszahlung für die ganze Provisionsart; auf Wunsch
|
- Gruppe markieren → **Prozent…** (`F4`): Auszahlung für die ganze Provisionsart; auf Wunsch
|
||||||
als feste Regel für diesen Händler gespeichert (gilt dann für künftige Importe).
|
als feste Regel für diesen Händler gespeichert (gilt dann für künftige Importe).
|
||||||
- Doppelklick / `F2` auf eine Position: Betrag frei überschreiben
|
- Doppelklick / `F2` auf eine Position: Betrag frei überschreiben
|
||||||
(`Enter` speichern, `↓`/`Tab` nächste Zeile, `Esc` abbrechen). Blau = von Hand geändert.
|
(`Enter` speichern, `↓`/`Tab` nächste Zeile, `Esc` abbrechen). Blau = von Hand geändert.
|
||||||
- Rechtsklick: u. a. „Auszahlung 0 €“.
|
- Rechtsklick: u. a. „Auszahlung 0 €“.
|
||||||
5. **Export** (`Strg+E`): je Händler (und je Anbieter) eine `.xlsx` im Format der Vorlage:
|
6. **Export** (`Strg+E`): je Händler (und je Anbieter) eine `.xlsx` im Format der Vorlage:
|
||||||
|
|
||||||
| A | B | C | D | E | F | G | H | I | J | K | L | M | N |
|
| A | B | C | D | E | F | G | H | I | J | K | L | M | N |
|
||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
|||||||
@@ -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.1.3"
|
__version__ = "1.2.0"
|
||||||
APP_NAME = "Provisions-Manager 95"
|
APP_NAME = "Provisions-Manager 95"
|
||||||
APP_ORG = "Handy-Insel Miesbach"
|
APP_ORG = "Handy-Insel Miesbach"
|
||||||
+27
-2
@@ -45,6 +45,14 @@ CREATE TABLE IF NOT EXISTS regel (
|
|||||||
UNIQUE (haendler_id, provisionsart)
|
UNIQUE (haendler_id, provisionsart)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Eigene Kennungen (eigene Filiale): diese Positionen werden ausgeblendet und nicht verprovisioniert.
|
||||||
|
CREATE TABLE IF NOT EXISTS eigen (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
anbieter TEXT NOT NULL,
|
||||||
|
wert TEXT NOT NULL,
|
||||||
|
UNIQUE (anbieter, wert)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS abrechnung (
|
CREATE TABLE IF NOT EXISTS abrechnung (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
titel TEXT NOT NULL,
|
titel TEXT NOT NULL,
|
||||||
@@ -82,6 +90,7 @@ CREATE INDEX IF NOT EXISTS ix_position_abr ON position(abrechnung_id);
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
UNZUGEORDNET = 0 # Pseudo-Händler-ID für Positionen ohne bekannte Kennung
|
UNZUGEORDNET = 0 # Pseudo-Händler-ID für Positionen ohne bekannte Kennung
|
||||||
|
EIGEN = -2 # Pseudo-Händler-ID für Positionen eigener Kennungen (ausgeblendet)
|
||||||
|
|
||||||
# Sonder-Kennung: Vodafone-Positionen, deren Kundenname komplett in GROSSBUCHSTABEN steht
|
# Sonder-Kennung: Vodafone-Positionen, deren Kundenname komplett in GROSSBUCHSTABEN steht
|
||||||
# (so erfasst ein Unterhändler seine Kunden). Hat Vorrang vor der VO-Nr.
|
# (so erfasst ein Unterhändler seine Kunden). Hat Vorrang vor der VO-Nr.
|
||||||
@@ -206,15 +215,31 @@ class Datenbank:
|
|||||||
kmap = self.kennung_map()
|
kmap = self.kennung_map()
|
||||||
caps = kmap.get((CAPS_ANBIETER, CAPS_WERT))
|
caps = kmap.get((CAPS_ANBIETER, CAPS_WERT))
|
||||||
aktive = {h.id for h in self.haendler_liste(nur_aktive=True)}
|
aktive = {h.id for h in self.haendler_liste(nur_aktive=True)}
|
||||||
|
eigene = set(self.eigene_kennungen())
|
||||||
|
|
||||||
def finde(anbieter: str, kennung: str, name: str | None = None, fest: int | None = None) -> int:
|
def finde(anbieter: str, kennung: str, name: str | None = None, fest: int | None = None) -> int:
|
||||||
if fest is not None:
|
if fest is not None:
|
||||||
return fest if (fest == UNZUGEORDNET or fest in aktive) else UNZUGEORDNET
|
return fest if (fest == UNZUGEORDNET or fest in aktive) else UNZUGEORDNET
|
||||||
if caps and anbieter == "vodafone" and ist_grossgeschrieben(name):
|
if caps and anbieter == "vodafone" and ist_grossgeschrieben(name):
|
||||||
return caps
|
return caps
|
||||||
return kmap.get((anbieter, kennung), UNZUGEORDNET)
|
hid = kmap.get((anbieter, kennung), UNZUGEORDNET)
|
||||||
|
if hid == UNZUGEORDNET and (anbieter, kennung) in eigene:
|
||||||
|
return EIGEN
|
||||||
|
return hid
|
||||||
return finde
|
return finde
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ eigene Kennungen
|
||||||
|
def eigene_kennungen(self) -> list[tuple[str, str]]:
|
||||||
|
return [(r[0], r[1]) for r in self.con.execute("SELECT anbieter, wert FROM eigen ORDER BY anbieter, wert")]
|
||||||
|
|
||||||
|
def eigene_setzen(self, anbieter: str, wert: str, eigen: bool = True) -> None:
|
||||||
|
wert = (wert or "").strip()
|
||||||
|
with self.con:
|
||||||
|
if eigen:
|
||||||
|
self.con.execute("INSERT OR IGNORE INTO eigen (anbieter, wert) VALUES (?,?)", (anbieter, wert))
|
||||||
|
else:
|
||||||
|
self.con.execute("DELETE FROM eigen WHERE anbieter=? AND wert=?", (anbieter, wert))
|
||||||
|
|
||||||
# ------------------------------------------------------------------ Regeln
|
# ------------------------------------------------------------------ Regeln
|
||||||
def regeln(self, hid: int) -> dict[str, float]:
|
def regeln(self, hid: int) -> dict[str, float]:
|
||||||
return {r[0]: r[1] for r in self.con.execute(
|
return {r[0]: r[1] for r in self.con.execute(
|
||||||
@@ -230,7 +255,7 @@ class Datenbank:
|
|||||||
(hid, provisionsart, prozent))
|
(hid, provisionsart, prozent))
|
||||||
|
|
||||||
def prozent_fuer(self, hid: int, provisionsart: str) -> float:
|
def prozent_fuer(self, hid: int, provisionsart: str) -> float:
|
||||||
if hid == UNZUGEORDNET:
|
if hid <= 0: # nicht zugeordnet / eigene Filiale
|
||||||
return 100.0
|
return 100.0
|
||||||
r = self.con.execute("SELECT prozent FROM regel WHERE haendler_id=? AND provisionsart=?",
|
r = self.con.execute("SELECT prozent FROM regel WHERE haendler_id=? AND provisionsart=?",
|
||||||
(hid, provisionsart)).fetchone()
|
(hid, provisionsart)).fetchone()
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Spalten: A Zeitraum | B Provisionsart | C Unterhändler-/VO-Nr | D Datum | E Rufnummer | F IMEI/SN |
|
Spalten: A Zeitraum | B Provisionsart | C Unterhändler-/VO-Nr | D Datum | E Rufnummer | F IMEI/SN |
|
||||||
G Gerät | H Vorname | I Name | J Tarif | K Betrag (Auszahlung) | L Netz | M Dienst | N Aktionscode
|
G Gerät | H Vorname | I Name | J Tarif | K Betrag (Auszahlung) | L Netz | M Dienst | N Aktionscode
|
||||||
Sortierung: Zeitraum → Provisionsart → Datum (sonst Reihenfolge der Abrechnung).
|
Sortierung: Zeitraum -> Provisionsart -> Datum (sonst Reihenfolge der Abrechnung).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
+122
-51
@@ -14,9 +14,11 @@ 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 EIGEN, UNZUGEORDNET, Datenbank
|
||||||
from provtool.gui import assistent as ass
|
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 menue as mn
|
||||||
|
from provtool.gui import modal
|
||||||
from provtool.gui import rahmen as rh
|
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
|
||||||
@@ -35,7 +37,7 @@ def _datum_de(iso: str | None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _zeitraum_kurz(z: str | None) -> str:
|
def _zeitraum_kurz(z: str | None) -> str:
|
||||||
"""'2026.10.01-2026.10.07' → '01.10.–07.10.'"""
|
"""'2026.10.01-2026.10.07' -> '01.10.–07.10.'"""
|
||||||
m = re.fullmatch(r"(\d{4})\.(\d{2})\.(\d{2})\s*-\s*(\d{4})\.(\d{2})\.(\d{2})", (z or "").strip())
|
m = re.fullmatch(r"(\d{4})\.(\d{2})\.(\d{2})\s*-\s*(\d{4})\.(\d{2})\.(\d{2})", (z or "").strip())
|
||||||
if m:
|
if m:
|
||||||
return f"{m[3]}.{m[2]}.–{m[6]}.{m[5]}."
|
return f"{m[3]}.{m[2]}.–{m[6]}.{m[5]}."
|
||||||
@@ -89,6 +91,8 @@ class App:
|
|||||||
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))
|
self.rahmen.geometrie_setzen(self.settings.get("fenster"), self.settings.get("maximiert", False))
|
||||||
|
if self.settings.get("trenner"):
|
||||||
|
root.after(200, lambda: self.pw.sash_place(0, int(self.settings["trenner"]), 0))
|
||||||
|
|
||||||
abr = self.db.abrechnungen()
|
abr = self.db.abrechnungen()
|
||||||
if abr:
|
if abr:
|
||||||
@@ -102,43 +106,46 @@ class App:
|
|||||||
|
|
||||||
# ================================================================= Aufbau
|
# ================================================================= Aufbau
|
||||||
def _menue(self):
|
def _menue(self):
|
||||||
self.menueleiste = rh.Menueleiste(self.inhalt, self.root)
|
self.menueleiste = mn.Menueleiste(self.inhalt, self.root)
|
||||||
self.menueleiste.pack(side="top", fill="x")
|
self.menueleiste.pack(side="top", fill="x")
|
||||||
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)
|
||||||
d.add_separator()
|
d.add_separator()
|
||||||
d.add_command(label="Dateien importieren…", accelerator="Strg+I", command=self.importieren)
|
d.add_command(label="Dateien importieren...", accelerator="Strg+I", command=self.importieren)
|
||||||
d.add_command(label="Importierte Dateien…", command=self.dateien)
|
d.add_command(label="Importierte Dateien...", command=self.dateien)
|
||||||
d.add_command(label="Exportieren…", accelerator="Strg+E", command=self.exportieren)
|
d.add_command(label="Exportieren...", accelerator="Strg+E", command=self.exportieren)
|
||||||
d.add_separator()
|
d.add_separator()
|
||||||
d.add_command(label="Datensicherung erstellen…", command=self.sicherung)
|
d.add_command(label="Datensicherung erstellen...", command=self.sicherung)
|
||||||
d.add_command(label="Datensicherung einspielen…", command=self.wiederherstellen)
|
d.add_command(label="Datensicherung einspielen...", command=self.wiederherstellen)
|
||||||
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)
|
||||||
|
|
||||||
b = self.menueleiste.menue("Bearbeiten")
|
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)
|
||||||
|
|
||||||
h = self.menueleiste.menue("Händler")
|
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))
|
||||||
|
h.add_separator()
|
||||||
|
h.add_command(label="Eigene Kennungen (ausblenden)...", command=self.eigene_verwalten)
|
||||||
|
h.add_command(label="Kennung der Auswahl als eigene ausblenden", command=self.eigene_ausblenden)
|
||||||
|
|
||||||
hi = self.menueleiste.menue("?", underline=None)
|
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="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)
|
||||||
hi.add_separator()
|
hi.add_separator()
|
||||||
hi.add_command(label="Einrichtungs-Assistent…", command=self.assistent)
|
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())
|
||||||
|
|
||||||
def _toolbar(self):
|
def _toolbar(self):
|
||||||
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=1, relief="raised")
|
tb = tk.Frame(self.inhalt, bg=w.FACE, bd=1, relief="raised")
|
||||||
@@ -166,14 +173,17 @@ class App:
|
|||||||
tk.Label(sf, text="Suchen:").pack(side="left")
|
tk.Label(sf, text="Suchen:").pack(side="left")
|
||||||
e = tk.Entry(sf, textvariable=self.v_suche, width=22)
|
e = tk.Entry(sf, textvariable=self.v_suche, width=22)
|
||||||
e.pack(side="left", padx=4)
|
e.pack(side="left", padx=4)
|
||||||
self.v_suche.trace_add("write", lambda *_: self._tabelle())
|
self._suche_geplant = None
|
||||||
|
self.v_suche.trace_add("write", lambda *_: self._suche_planen())
|
||||||
self._suchfeld = e
|
self._suchfeld = e
|
||||||
self._suchrahmen = sf
|
self._suchrahmen = sf
|
||||||
|
|
||||||
def _body(self):
|
def _body(self):
|
||||||
desk = tk.Frame(self.inhalt, 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)
|
# Trennlinie zwischen den Spalten: grau und erhaben wie ein Win95-Splitter, mit der Maus ziehbar
|
||||||
|
pw = self.pw = tk.PanedWindow(desk, orient="horizontal", bg=w.FACE, sashwidth=7, sashrelief="raised",
|
||||||
|
bd=0, opaqueresize=True, sashcursor="sb_h_double_arrow")
|
||||||
pw.pack(fill="both", expand=True, padx=6, pady=6)
|
pw.pack(fill="both", expand=True, padx=6, pady=6)
|
||||||
|
|
||||||
# --- Händler-Fenster
|
# --- Händler-Fenster
|
||||||
@@ -218,9 +228,9 @@ 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))
|
||||||
self.b_prozent = w.knopf(leiste, "Prozent…", self.prozent, self.icons("prozent"))
|
self.b_prozent = w.knopf(leiste, "Prozent...", self.prozent, 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, 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)
|
||||||
w.knopf(leiste, "Alle zuklappen", lambda: self._klappen(False)).pack(side="right")
|
w.knopf(leiste, "Alle zuklappen", lambda: self._klappen(False)).pack(side="right")
|
||||||
@@ -231,13 +241,18 @@ class App:
|
|||||||
|
|
||||||
def _tasten(self):
|
def _tasten(self):
|
||||||
r = self.root
|
r = self.root
|
||||||
r.bind_all("<Control-n>", lambda e: self.neu())
|
|
||||||
r.bind_all("<Control-o>", lambda e: self.abrechnung_waehlen())
|
def frei(befehl):
|
||||||
r.bind_all("<Control-i>", lambda e: self.importieren())
|
# Tastenkürzel des Hauptfensters nicht ausführen, solange ein Dialog offen ist
|
||||||
r.bind_all("<Control-e>", lambda e: self.exportieren())
|
# (sonst öffnete z. B. Strg+N im Dialog einen zweiten Dialog darüber)
|
||||||
r.bind_all("<Control-h>", lambda e: self.haendler())
|
return lambda e: None if modal.offen() else befehl()
|
||||||
r.bind_all("<Control-f>", lambda e: self._suchfeld.focus_set())
|
r.bind_all("<Control-n>", frei(self.neu))
|
||||||
r.bind_all("<F1>", lambda e: self.hilfe())
|
r.bind_all("<Control-o>", frei(self.abrechnung_waehlen))
|
||||||
|
r.bind_all("<Control-i>", frei(self.importieren))
|
||||||
|
r.bind_all("<Control-e>", frei(self.exportieren))
|
||||||
|
r.bind_all("<Control-h>", frei(self.haendler))
|
||||||
|
r.bind_all("<Control-f>", frei(self._suchfeld.focus_set))
|
||||||
|
r.bind_all("<F1>", frei(self.hilfe))
|
||||||
self.ptree.bind("<F2>", lambda e: self.betrag_bearbeiten())
|
self.ptree.bind("<F2>", lambda e: self.betrag_bearbeiten())
|
||||||
self.ptree.bind("<Return>", lambda e: self.betrag_bearbeiten())
|
self.ptree.bind("<Return>", lambda e: self.betrag_bearbeiten())
|
||||||
self.ptree.bind("<F4>", lambda e: self.prozent())
|
self.ptree.bind("<F4>", lambda e: self.prozent())
|
||||||
@@ -258,13 +273,23 @@ class App:
|
|||||||
self.aid = None
|
self.aid = None
|
||||||
self.positionen = []
|
self.positionen = []
|
||||||
self.rahmen.set_titel(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()
|
||||||
|
|
||||||
|
def _suche_planen(self):
|
||||||
|
"""Suche erst kurz nach dem letzten Tastendruck ausführen (flüssiges Tippen)."""
|
||||||
|
if self._suche_geplant is not None:
|
||||||
|
self.root.after_cancel(self._suche_geplant)
|
||||||
|
self._suche_geplant = self.root.after(150, self._suche_ausfuehren)
|
||||||
|
|
||||||
|
def _suche_ausfuehren(self):
|
||||||
|
self._suche_geplant = None
|
||||||
|
self._tabelle()
|
||||||
|
|
||||||
def _gefiltert(self) -> list[dict]:
|
def _gefiltert(self) -> list[dict]:
|
||||||
if self.ansicht == ALLE:
|
if self.ansicht == ALLE:
|
||||||
return self.positionen
|
return [p for p in self.positionen if p["haendler_id"] != EIGEN]
|
||||||
return [p for p in self.positionen if p["haendler_id"] == self.ansicht]
|
return [p for p in self.positionen if p["haendler_id"] == self.ansicht]
|
||||||
|
|
||||||
def _haendlerliste(self, behalte_auswahl=True):
|
def _haendlerliste(self, behalte_auswahl=True):
|
||||||
@@ -276,7 +301,7 @@ class App:
|
|||||||
s[0] += 1
|
s[0] += 1
|
||||||
s[1] += p["betrag"]
|
s[1] += p["betrag"]
|
||||||
s[2] += p["betrag_orig"]
|
s[2] += p["betrag_orig"]
|
||||||
ges = [sum(s[0] for s in stat.values()), sum(s[1] for s in stat.values())]
|
ges = [sum(s[0] for k, s in stat.items() if k != EIGEN), sum(s[1] for k, s in stat.items() if k != EIGEN)]
|
||||||
t.insert("", "end", iid=str(ALLE), text="(Alle Positionen)", values=(ges[0], fmt(ges[1])), tags=("fett",))
|
t.insert("", "end", iid=str(ALLE), text="(Alle Positionen)", values=(ges[0], fmt(ges[1])), tags=("fett",))
|
||||||
for h in self.db.haendler_liste():
|
for h in self.db.haendler_liste():
|
||||||
s = stat.get(h.id, [0, 0, 0])
|
s = stat.get(h.id, [0, 0, 0])
|
||||||
@@ -286,6 +311,10 @@ class App:
|
|||||||
if s:
|
if s:
|
||||||
t.insert("", "end", iid=str(UNZUGEORDNET), text="(Nicht zugeordnet)", values=(s[0], fmt(s[1])),
|
t.insert("", "end", iid=str(UNZUGEORDNET), text="(Nicht zugeordnet)", values=(s[0], fmt(s[1])),
|
||||||
tags=("grau",))
|
tags=("grau",))
|
||||||
|
s = stat.get(EIGEN)
|
||||||
|
if s:
|
||||||
|
t.insert("", "end", iid=str(EIGEN), text="(Eigene – ausgeblendet)", values=(s[0], fmt(s[2])),
|
||||||
|
tags=("grau",))
|
||||||
ziel = str(self.ansicht) if behalte_auswahl and t.exists(str(self.ansicht)) else str(ALLE)
|
ziel = str(self.ansicht) if behalte_auswahl and t.exists(str(self.ansicht)) else str(ALLE)
|
||||||
self.ansicht = int(ziel)
|
self.ansicht = int(ziel)
|
||||||
t.selection_set(ziel)
|
t.selection_set(ziel)
|
||||||
@@ -302,9 +331,11 @@ class App:
|
|||||||
self.haendler(self.ansicht)
|
self.haendler(self.ansicht)
|
||||||
elif self.ansicht == UNZUGEORDNET:
|
elif self.ansicht == UNZUGEORDNET:
|
||||||
self.kennung_zuordnen_auswahl()
|
self.kennung_zuordnen_auswahl()
|
||||||
|
elif self.ansicht == EIGEN:
|
||||||
|
self.eigene_verwalten()
|
||||||
|
|
||||||
def _gruppe_von(self, p: dict) -> str:
|
def _gruppe_von(self, p: dict) -> str:
|
||||||
if self.ansicht == UNZUGEORDNET:
|
if self.ansicht in (UNZUGEORDNET, EIGEN):
|
||||||
return f"{ANBIETER.get(p['anbieter'], p['anbieter'])} · Kennung {p['kennung'] or '(leer)'}"
|
return f"{ANBIETER.get(p['anbieter'], p['anbieter'])} · Kennung {p['kennung'] or '(leer)'}"
|
||||||
if self.ansicht == ALLE:
|
if self.ansicht == ALLE:
|
||||||
name = self.haendler_namen.get(p["haendler_id"], "(Nicht zugeordnet)")
|
name = self.haendler_namen.get(p["haendler_id"], "(Nicht zugeordnet)")
|
||||||
@@ -351,7 +382,8 @@ class App:
|
|||||||
fmt(p["betrag_orig"]),
|
fmt(p["betrag_orig"]),
|
||||||
"manuell" if p["manuell"] else fmt_percent(p["prozent"]), fmt(p["betrag"])),
|
"manuell" if p["manuell"] else fmt_percent(p["prozent"]), fmt(p["betrag"])),
|
||||||
tags=tuple(tags))
|
tags=tuple(tags))
|
||||||
name = {ALLE: "Alle Positionen", UNZUGEORDNET: "Nicht zugeordnet"}.get(
|
name = {ALLE: "Alle Positionen", UNZUGEORDNET: "Nicht zugeordnet",
|
||||||
|
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.f_p.set_titel(f"Positionen – {name}")
|
||||||
so = sum(p["betrag_orig"] for p in pos)
|
so = sum(p["betrag_orig"] for p in pos)
|
||||||
@@ -464,8 +496,10 @@ class App:
|
|||||||
u[0] += 1
|
u[0] += 1
|
||||||
u[1] += p["betrag_orig"]
|
u[1] += p["betrag_orig"]
|
||||||
res = dlg.ImportBericht(self.root, bericht, [(a, k, n, s) for (a, k), (n, s) in sorted(unbekannt.items())]).zeigen()
|
res = dlg.ImportBericht(self.root, bericht, [(a, k, n, s) for (a, k), (n, s) in sorted(unbekannt.items())]).zeigen()
|
||||||
if res:
|
if res and res[0] == "zuordnen":
|
||||||
self.kennung_zuordnen(*res)
|
self.kennung_zuordnen(res[1], res[2])
|
||||||
|
elif res and res[0] == "eigen":
|
||||||
|
self.eigene_ausblenden(res[1])
|
||||||
|
|
||||||
def dateien(self):
|
def dateien(self):
|
||||||
if not self.aid:
|
if not self.aid:
|
||||||
@@ -599,7 +633,7 @@ class App:
|
|||||||
pid = int(iid[1:])
|
pid = int(iid[1:])
|
||||||
p = next(x for x in self.positionen if x["id"] == pid)
|
p = next(x for x in self.positionen if x["id"] == pid)
|
||||||
if not bbox:
|
if not bbox:
|
||||||
# Zelle gerade nicht sichtbar (z. B. Spalte zu schmal) → kleiner Dialog statt Inline-Feld
|
# Zelle gerade nicht sichtbar (z. B. Spalte zu schmal) -> kleiner Dialog statt Inline-Feld
|
||||||
self._betrag_dialog(p)
|
self._betrag_dialog(p)
|
||||||
return
|
return
|
||||||
x, y, bw, bh = bbox
|
x, y, bw, bh = bbox
|
||||||
@@ -675,20 +709,56 @@ class App:
|
|||||||
return
|
return
|
||||||
if iid not in self.ptree.selection():
|
if iid not in self.ptree.selection():
|
||||||
self.ptree.selection_set(iid)
|
self.ptree.selection_set(iid)
|
||||||
m = tk.Menu(self.root, tearoff=0)
|
m = mn.Popup(self.root)
|
||||||
if iid.startswith("p"):
|
if iid.startswith("p"):
|
||||||
m.add_command(label="Betrag ändern…", command=lambda: self.betrag_bearbeiten(iid))
|
m.add_command(label="Betrag ändern...", command=lambda: self.betrag_bearbeiten(iid))
|
||||||
m.add_command(label="Auszahlung in Prozent…", command=self.prozent)
|
m.add_command(label="Auszahlung in Prozent...", command=self.prozent)
|
||||||
m.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen)
|
m.add_command(label="Auf Regel zurücksetzen", command=self.zuruecksetzen)
|
||||||
m.add_separator()
|
m.add_separator()
|
||||||
m.add_command(label="Auszahlung 0 € (nicht auszahlen)", command=lambda: self._null())
|
m.add_command(label="Auszahlung 0 € (nicht auszahlen)", command=lambda: self._null())
|
||||||
m.add_separator()
|
m.add_separator()
|
||||||
m.add_command(label="Händler zuweisen…", command=self.haendler_zuweisen)
|
m.add_command(label="Händler zuweisen...", command=self.haendler_zuweisen)
|
||||||
m.add_command(label="Automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True))
|
m.add_command(label="Automatisch zuordnen", command=lambda: self.haendler_zuweisen(automatisch=True))
|
||||||
if self.ansicht == UNZUGEORDNET or self.ansicht == ALLE:
|
if self.ansicht == UNZUGEORDNET or self.ansicht == ALLE:
|
||||||
m.add_command(label="Kennung einem Händler zuordnen…", command=self.kennung_zuordnen_auswahl)
|
m.add_command(label="Kennung einem Händler zuordnen...", command=self.kennung_zuordnen_auswahl)
|
||||||
|
_, _, ps = self._auswahl_info()
|
||||||
|
if any(p["haendler_id"] == UNZUGEORDNET for p in ps):
|
||||||
|
m.add_separator()
|
||||||
|
m.add_command(label="Kennung als eigene ausblenden", command=self.eigene_ausblenden)
|
||||||
|
if any(p["haendler_id"] == EIGEN for p in ps):
|
||||||
|
m.add_separator()
|
||||||
|
m.add_command(label="Kennung wieder einblenden", command=self.eigene_einblenden)
|
||||||
m.tk_popup(e.x_root, e.y_root)
|
m.tk_popup(e.x_root, e.y_root)
|
||||||
|
|
||||||
|
# ----- Eigene Kennungen (eigene Filiale) ausblenden
|
||||||
|
def _kennungen_der_auswahl(self, nur: int) -> list[tuple[str, str]]:
|
||||||
|
_, _, ps = self._auswahl_info()
|
||||||
|
return sorted({(p["anbieter"], p["kennung"]) for p in ps if p["haendler_id"] == nur})
|
||||||
|
|
||||||
|
def eigene_ausblenden(self, kennungen: list[tuple[str, str]] | None = None):
|
||||||
|
kennungen = kennungen if kennungen is not None else self._kennungen_der_auswahl(UNZUGEORDNET)
|
||||||
|
if not kennungen:
|
||||||
|
w.info(self.root, "Eigene Kennung", "Bitte zuerst nicht zugeordnete Positionen auswählen.")
|
||||||
|
return
|
||||||
|
liste = "\n".join(f" {ANBIETER.get(a, a)}: {k or '(leer)'}" for a, k in kennungen)
|
||||||
|
if not w.frage(self.root, "Eigene Kennung ausblenden",
|
||||||
|
f"Diese Kennung(en) gehören zur eigenen Filiale und werden künftig ausgeblendet "
|
||||||
|
f"(nicht angezeigt, nicht mitgerechnet, nicht exportiert):\n\n{liste}\n\n"
|
||||||
|
"Rückgängig jederzeit unter Händler > Eigene Kennungen."):
|
||||||
|
return
|
||||||
|
for a, k in kennungen:
|
||||||
|
self.db.eigene_setzen(a, k, True)
|
||||||
|
self._aktualisieren()
|
||||||
|
|
||||||
|
def eigene_einblenden(self):
|
||||||
|
for a, k in self._kennungen_der_auswahl(EIGEN):
|
||||||
|
self.db.eigene_setzen(a, k, False)
|
||||||
|
self._aktualisieren()
|
||||||
|
|
||||||
|
def eigene_verwalten(self):
|
||||||
|
if dlg.EigeneKennungenDialog(self.root, self.db).zeigen():
|
||||||
|
self._aktualisieren()
|
||||||
|
|
||||||
def haendler_zuweisen(self, automatisch: bool = False):
|
def haendler_zuweisen(self, automatisch: bool = False):
|
||||||
sel, pids, _ = self._auswahl_info()
|
sel, pids, _ = self._auswahl_info()
|
||||||
if not pids:
|
if not pids:
|
||||||
@@ -746,7 +816,7 @@ class App:
|
|||||||
"Ist sie vielleicht noch in Excel geöffnet?")
|
"Ist sie vielleicht noch in Excel geöffnet?")
|
||||||
return
|
return
|
||||||
w.info(self.root, "Export", f"{len(erzeugt)} Datei(en) gespeichert in:\n{res['ordner']}\n\n"
|
w.info(self.root, "Export", f"{len(erzeugt)} Datei(en) gespeichert in:\n{res['ordner']}\n\n"
|
||||||
+ "\n".join(p.name for p in erzeugt[:15]) + ("\n…" if len(erzeugt) > 15 else ""))
|
+ "\n".join(p.name for p in erzeugt[:15]) + ("\n..." if len(erzeugt) > 15 else ""))
|
||||||
if res["oeffnen"]:
|
if res["oeffnen"]:
|
||||||
_oeffne_ordner(res["ordner"])
|
_oeffne_ordner(res["ordner"])
|
||||||
|
|
||||||
@@ -815,15 +885,15 @@ class App:
|
|||||||
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"
|
||||||
" bei freenet die Unterhändlernummer, bei Vodafone die VO-Nr. oder die Regel\n"
|
" bei freenet die Unterhändlernummer, bei Vodafone die VO-Nr. oder die Regel\n"
|
||||||
" „Name in GROSSBUCHSTABEN“. Einzelne Positionen: Rechtsklick ▸ Händler zuweisen.\n\n"
|
" „Name in GROSSBUCHSTABEN“. Einzelne Positionen: Rechtsklick > Händler zuweisen.\n\n"
|
||||||
"2. Neue Abrechnung (Strg+N), z. B. „Oktober 2026“.\n\n"
|
"2. Neue Abrechnung (Strg+N), z. B. „Oktober 2026“.\n\n"
|
||||||
"3. Dateien importieren (Strg+I) – mehrere CSV/Excel auf einmal möglich,\n"
|
"3. Dateien importieren (Strg+I) – mehrere CSV/Excel auf einmal möglich,\n"
|
||||||
" auch alle Wochenlisten eines Monats.\n\n"
|
" auch alle Wochenlisten eines Monats.\n\n"
|
||||||
"4. Links den Händler wählen. Positionen sind nach Provisionsart gruppiert.\n"
|
"4. Links den Händler wählen. Positionen sind nach Provisionsart gruppiert.\n"
|
||||||
" • Gruppe wählen + „Prozent…“ (F4): Auszahlung für die ganze Provisionsart,\n"
|
" - Gruppe wählen + „Prozent...“ (F4): Auszahlung für die ganze Provisionsart,\n"
|
||||||
" optional als Standard für diesen Händler merken.\n"
|
" optional als Standard für diesen Händler merken.\n"
|
||||||
" • Doppelklick / F2 auf eine Position: Betrag frei ändern\n"
|
" - Doppelklick / F2 auf eine Position: Betrag frei ändern\n"
|
||||||
" (Enter = speichern, ↓/Tab = nächste Zeile, Esc = abbrechen).\n\n"
|
" (Enter = speichern, Pfeil runter/Tab = nächste Zeile, Esc = abbrechen).\n\n"
|
||||||
"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.")
|
||||||
|
|
||||||
@@ -868,9 +938,9 @@ class App:
|
|||||||
"Das wird nicht exportiert."),
|
"Das wird nicht exportiert."),
|
||||||
(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. "
|
"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."),
|
"Mit „Als Standard merken“ gilt das auch in den nächsten Monaten automatisch."),
|
||||||
@@ -955,6 +1025,7 @@ class App:
|
|||||||
return
|
return
|
||||||
self._beendet = True
|
self._beendet = True
|
||||||
try:
|
try:
|
||||||
|
self.settings["trenner"] = self.pw.sash_coord(0)[0]
|
||||||
self.settings["fenster"] = self.rahmen.normale_geometrie()
|
self.settings["fenster"] = self.rahmen.normale_geometrie()
|
||||||
self.settings["maximiert"] = self.rahmen.maximiert
|
self.settings["maximiert"] = self.rahmen.maximiert
|
||||||
config.save(self.settings)
|
config.save(self.settings)
|
||||||
|
|||||||
+22
-20
@@ -18,7 +18,7 @@ DATEITYPEN = [("Abrechnungen (CSV, Excel)", "*.csv *.xlsx *.xlsm *.xls *.txt"),
|
|||||||
|
|
||||||
|
|
||||||
def _nummern(text: str) -> list[str]:
|
def _nummern(text: str) -> list[str]:
|
||||||
"""'27518902 (6 Pos.), 27518999' → ['27518902', '27518999']"""
|
"""'27518902 (6 Pos.), 27518999' -> ['27518902', '27518999']"""
|
||||||
out = []
|
out = []
|
||||||
for teil in text.split(","):
|
for teil in text.split(","):
|
||||||
m = re.match(r"\s*([A-Za-z0-9]+)", teil)
|
m = re.match(r"\s*([A-Za-z0-9]+)", teil)
|
||||||
@@ -122,7 +122,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
if w.frage(self, "Einrichtung abbrechen",
|
if w.frage(self, "Einrichtung abbrechen",
|
||||||
"Möchten Sie die Einrichtung wirklich abbrechen?\n\n"
|
"Möchten Sie die Einrichtung wirklich abbrechen?\n\n"
|
||||||
"Bereits angelegte Händler bleiben gespeichert. Den Assistenten finden Sie jederzeit "
|
"Bereits angelegte Händler bleiben gespeichert. Den Assistenten finden Sie jederzeit "
|
||||||
"unter ? ▸ Einrichtungs-Assistent.", default_ja=False):
|
"unter ? > Einrichtungs-Assistent.", default_ja=False):
|
||||||
self.result = None
|
self.result = None
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
for punkt in ("Speicherort der Daten festlegen",
|
for punkt in ("Speicherort der Daten festlegen",
|
||||||
"Unterhändler mit ihren Kennungen anlegen",
|
"Unterhändler mit ihren Kennungen anlegen",
|
||||||
"die erste Abrechnung anlegen und Dateien importieren"):
|
"die erste Abrechnung anlegen und Dateien importieren"):
|
||||||
self._text(f" • {punkt}")
|
self._text(f" - {punkt}")
|
||||||
self._text("\nDanach zeigt Ihnen ein kurzer Rundgang, wo Sie was finden.\n\n"
|
self._text("\nDanach zeigt Ihnen ein kurzer Rundgang, wo Sie was finden.\n\n"
|
||||||
"Alle Angaben lassen sich später jederzeit ändern.")
|
"Alle Angaben lassen sich später jederzeit ändern.")
|
||||||
self._text("Klicken Sie auf „Weiter“, um fortzufahren.", fg=w.NAVY)
|
self._text("Klicken Sie auf „Weiter“, um fortzufahren.", fg=w.NAVY)
|
||||||
@@ -158,7 +158,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
e = tk.Entry(zeile, textvariable=self.v_db_pfad)
|
e = tk.Entry(zeile, textvariable=self.v_db_pfad)
|
||||||
e.pack(side="left", fill="x", expand=True)
|
e.pack(side="left", fill="x", expand=True)
|
||||||
e.bind("<Key>", lambda ev: self.v_db_eigen.set(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))
|
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 – "
|
self._text("\nTipp: Auf einem Netzlaufwerk können mehrere PCs dieselben Händler nutzen – "
|
||||||
"bitte aber nicht gleichzeitig damit arbeiten.", fg=w.SHADOW)
|
"bitte aber nicht gleichzeitig damit arbeiten.", fg=w.SHADOW)
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
k = tk.Frame(self.seite, bg=w.FACE)
|
k = tk.Frame(self.seite, bg=w.FACE)
|
||||||
k.pack(fill="x", pady=(4, 4))
|
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, "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)
|
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")
|
tk.Label(self.seite, text="Bereits angelegt:", anchor="w").pack(fill="x")
|
||||||
lf = tk.Frame(self.seite, bg=w.FACE)
|
lf = tk.Frame(self.seite, bg=w.FACE)
|
||||||
@@ -313,7 +313,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
w.warnung(self, "Nummern vorschlagen", "Nicht lesbar:\n" + "\n".join(fehler))
|
w.warnung(self, "Nummern vorschlagen", "Nicht lesbar:\n" + "\n".join(fehler))
|
||||||
else:
|
else:
|
||||||
w.info(self, "Nummern vorschlagen",
|
w.info(self, "Nummern vorschlagen",
|
||||||
"Die gefundenen Nummern stehen jetzt in den Auswahllisten (▼) bei freenet und Vodafone – "
|
"Die gefundenen Nummern stehen jetzt in den Auswahllisten (Pfeil-Knopf rechts) bei freenet und Vodafone – "
|
||||||
"sortiert nach Anzahl der Positionen.\n\nDie Nummer mit den meisten Positionen ist meist "
|
"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.")
|
"Ihre eigene Filiale.\n\nDie Dateien werden im nächsten Schritt gleich mit importiert.")
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
self.lb_d.pack(side="left", fill="both", expand=True)
|
self.lb_d.pack(side="left", fill="both", expand=True)
|
||||||
kf = tk.Frame(lf, bg=w.FACE)
|
kf = tk.Frame(lf, bg=w.FACE)
|
||||||
kf.pack(side="left", anchor="n", padx=(6, 0))
|
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 = w.knopf(kf, "Hinzufügen...", self._datei_dazu, breite=11)
|
||||||
self._b_dazu.pack(pady=(0, 4))
|
self._b_dazu.pack(pady=(0, 4))
|
||||||
self._b_weg = w.knopf(kf, "Entfernen", self._datei_weg, breite=11)
|
self._b_weg = w.knopf(kf, "Entfernen", self._datei_weg, breite=11)
|
||||||
self._b_weg.pack()
|
self._b_weg.pack()
|
||||||
@@ -385,12 +385,12 @@ class Einrichtungsassistent(w.Dialog):
|
|||||||
self._ueberschrift("Fertig!")
|
self._ueberschrift("Fertig!")
|
||||||
n = len(self.app.db.haendler_liste())
|
n = len(self.app.db.haendler_liste())
|
||||||
self._text(f"Der {APP_NAME} ist eingerichtet.")
|
self._text(f"Der {APP_NAME} ist eingerichtet.")
|
||||||
self._text(f" • {n} Händler angelegt")
|
self._text(f" - {n} Händler angelegt")
|
||||||
if self.v_abrechnung.get():
|
if self.v_abrechnung.get():
|
||||||
self._text(f" • Abrechnung „{self.v_titel.get().strip()}“"
|
self._text(f" - Abrechnung „{self.v_titel.get().strip()}“"
|
||||||
+ (f" mit {len(self.dateien)} Datei(en)" if self.dateien else " (noch ohne Dateien)"))
|
+ (f" mit {len(self.dateien)} Datei(en)" if self.dateien else " (noch ohne Dateien)"))
|
||||||
self._text("\nSo geht es danach jeden Monat weiter:\n"
|
self._text("\nSo geht es danach jeden Monat weiter:\n"
|
||||||
" Neu → Import → prüfen & anpassen → Export")
|
" Neu > Import > prüfen & anpassen > Export")
|
||||||
tk.Checkbutton(self.seite, text="Kurzen Rundgang durch das Programm starten", variable=self.v_rundgang
|
tk.Checkbutton(self.seite, text="Kurzen Rundgang durch das Programm starten", variable=self.v_rundgang
|
||||||
).pack(anchor="w", pady=(12, 0))
|
).pack(anchor="w", pady=(12, 0))
|
||||||
self._text("\nKlicken Sie auf „Fertig stellen“.", fg=w.NAVY)
|
self._text("\nKlicken Sie auf „Fertig stellen“.", fg=w.NAVY)
|
||||||
@@ -539,21 +539,21 @@ class Rundgang:
|
|||||||
TIPPS = [
|
TIPPS = [
|
||||||
"Sie können mehrere Dateien gleichzeitig importieren – zum Beispiel alle freenet-Wochenlisten eines Monats "
|
"Sie können mehrere Dateien gleichzeitig importieren – zum Beispiel alle freenet-Wochenlisten eines Monats "
|
||||||
"und die Vodafone-Datei in einem Rutsch.",
|
"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 % "
|
"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.",
|
"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 "
|
"Doppelklick auf eine Position – und Sie können den Auszahlungsbetrag direkt überschreiben. Mit Pfeil runter geht es "
|
||||||
"gleich in die nächste Zeile.",
|
"gleich in die nächste Zeile.",
|
||||||
"Blau dargestellte Beträge wurden von Hand geändert, rote weichen durch eine Prozent-Regel von der "
|
"Blau dargestellte Beträge wurden von Hand geändert, rote weichen durch eine Prozent-Regel von der "
|
||||||
"Provision ab.",
|
"Provision ab.",
|
||||||
"Eine Position soll gar nicht ausgezahlt werden? Rechtsklick ▸ „Auszahlung 0 €“.",
|
"Eine Position soll gar nicht ausgezahlt werden? Rechtsklick > „Auszahlung 0 €“.",
|
||||||
"Die GROSSBUCHSTABEN-Regel hat einen Kunden falsch erwischt? Rechtsklick ▸ „Händler zuweisen…“ – diese "
|
"Die GROSSBUCHSTABEN-Regel hat einen Kunden falsch erwischt? Rechtsklick > „Händler zuweisen...“ – diese "
|
||||||
"Zuordnung hat immer Vorrang.",
|
"Zuordnung hat immer Vorrang.",
|
||||||
"Unter „(Nicht zugeordnet)“ sehen Sie alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
|
"Unter „(Nicht zugeordnet)“ sehen Sie alles, was keinem Unterhändler gehört – meist Ihre eigene Filiale. "
|
||||||
"Diese Positionen werden nicht exportiert.",
|
"Diese Positionen werden nicht exportiert.",
|
||||||
"Mit Strg+F springen Sie ins Suchfeld. Gesucht wird in Rufnummer, Name, Tarif, Provisionsart und mehr.",
|
"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 "
|
"Wurde eine Datei versehentlich doppelt oder falsch importiert? Datei > Importierte Dateien... – dort lässt "
|
||||||
"sie sich wieder entfernen.",
|
"sie sich wieder entfernen.",
|
||||||
"Erstellen Sie ab und zu eine Datensicherung: Datei ▸ Datensicherung erstellen…",
|
"Erstellen Sie ab und zu eine Datensicherung: Datei > Datensicherung erstellen...",
|
||||||
"Alle Änderungen werden sofort gespeichert. Ein „Speichern“-Knopf ist daher nicht nötig.",
|
"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.",
|
"Ein Händler wurde erst nach dem Import angelegt? Kein Problem – die Zuordnung passt sich sofort an.",
|
||||||
]
|
]
|
||||||
@@ -583,13 +583,15 @@ class TippDesTages(w.Dialog):
|
|||||||
if self._bild is None: # Ersatz, falls das Bild fehlt
|
if self._bild is None: # Ersatz, falls das Bild fehlt
|
||||||
self._bild = w.make_icon(self, "birne", 3)
|
self._bild = w.make_icon(self, "birne", 3)
|
||||||
tk.Label(box, image=self._bild, bg=w.WHITE).pack(side="left", anchor="n", padx=(12, 4), pady=10)
|
tk.Label(box, image=self._bild, bg=w.WHITE).pack(side="left", anchor="n", padx=(12, 4), pady=10)
|
||||||
rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10)
|
# fester Textbereich: der Tipp bricht darin um und kann nicht mehr abgeschnitten werden
|
||||||
|
rechts = tk.Frame(box, bg=w.WHITE, padx=12, pady=10, width=380, height=self._bild.height() + 20)
|
||||||
rechts.pack(side="left", fill="both")
|
rechts.pack(side="left", fill="both")
|
||||||
tk.Label(rechts, text="Wussten Sie schon…", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"),
|
rechts.pack_propagate(False)
|
||||||
|
tk.Label(rechts, text="Wussten Sie schon...", bg=w.WHITE, font=(w.FONT_FAMILY, w.FONT_SIZE + 6, "bold"),
|
||||||
anchor="w").pack(fill="x")
|
anchor="w").pack(fill="x")
|
||||||
tk.Frame(rechts, bg=w.DARK, height=1).pack(fill="x", pady=6)
|
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=8)
|
self.text = tk.Label(rechts, bg=w.WHITE, justify="left", anchor="nw", wraplength=380 - 2 * 12)
|
||||||
self.text.pack(fill="both")
|
self.text.pack(fill="both", expand=True)
|
||||||
knoepfe = tk.Frame(haupt, bg=w.FACE)
|
knoepfe = tk.Frame(haupt, bg=w.FACE)
|
||||||
knoepfe.pack(side="left", anchor="n", padx=(10, 0))
|
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, "Schließen", self.abbrechen, breite=12, default="active").pack(pady=(0, 6))
|
||||||
|
|||||||
+77
-11
@@ -9,7 +9,7 @@ from provtool.db import CAPS_ANBIETER, CAPS_WERT, Datenbank, Haendler
|
|||||||
from provtool.gui import win95 as w
|
from provtool.gui import win95 as w
|
||||||
from provtool.importer import ANBIETER
|
from provtool.importer import ANBIETER
|
||||||
|
|
||||||
# Auswahl im Händler-Dialog: Schlüssel → Anzeige
|
# Auswahl im Händler-Dialog: Schlüssel -> Anzeige
|
||||||
KENNUNG_ARTEN = {"freenet": "freenet (Unterhändler-Nr.)", "vodafone": "Vodafone (VO-Nr.)",
|
KENNUNG_ARTEN = {"freenet": "freenet (Unterhändler-Nr.)", "vodafone": "Vodafone (VO-Nr.)",
|
||||||
CAPS_ANBIETER: "Vodafone (Name in GROSSBUCHSTABEN)"}
|
CAPS_ANBIETER: "Vodafone (Name in GROSSBUCHSTABEN)"}
|
||||||
CAPS_TEXT = "Kunden-Nachname komplett in GROSSBUCHSTABEN"
|
CAPS_TEXT = "Kunden-Nachname komplett in GROSSBUCHSTABEN"
|
||||||
@@ -100,7 +100,7 @@ class HaendlerDialog(w.Dialog):
|
|||||||
r3.pack(fill="both", expand=True)
|
r3.pack(fill="both", expand=True)
|
||||||
rf = tk.Frame(g3, bg=w.FACE)
|
rf = tk.Frame(g3, bg=w.FACE)
|
||||||
rf.pack(fill="x", pady=(4, 0))
|
rf.pack(fill="x", pady=(4, 0))
|
||||||
tk.Label(rf, text="Regeln entstehen im Hauptfenster über „Prozent…“.", fg=w.SHADOW).pack(side="left")
|
tk.Label(rf, text="Regeln entstehen im Hauptfenster über „Prozent...“.", fg=w.SHADOW).pack(side="left")
|
||||||
w.knopf(rf, "Entfernen", self._regel_weg).pack(side="right")
|
w.knopf(rf, "Entfernen", self._regel_weg).pack(side="right")
|
||||||
|
|
||||||
unten = tk.Frame(self, bg=w.FACE)
|
unten = tk.Frame(self, bg=w.FACE)
|
||||||
@@ -292,7 +292,7 @@ class ProzentDialog(w.Dialog):
|
|||||||
from provtool.money import apply_percent
|
from provtool.money import apply_percent
|
||||||
try:
|
try:
|
||||||
p = parse_percent(self.v.get())
|
p = parse_percent(self.v.get())
|
||||||
self.vorschau.configure(text=f"→ Auszahlung ca. {fmt(apply_percent(self.summe_orig, p))}")
|
self.vorschau.configure(text=f"-> Auszahlung ca. {fmt(apply_percent(self.summe_orig, p))}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.vorschau.configure(text="(ungültiger Wert)")
|
self.vorschau.configure(text="(ungültiger Wert)")
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ class AbrechnungenDialog(w.Dialog):
|
|||||||
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")
|
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, "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, "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())
|
||||||
@@ -440,7 +440,7 @@ class ImportBericht(w.Dialog):
|
|||||||
tk.Label(self, text="Nicht zugeordnete Kennungen (kein Händler hinterlegt – z. B. eigene Filiale):",
|
tk.Label(self, text="Nicht zugeordnete Kennungen (kein Händler hinterlegt – z. B. eigene Filiale):",
|
||||||
anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
anchor="w").pack(fill="x", padx=10, pady=(10, 2))
|
||||||
r2, self.ut = w.scroll_tree(self, columns=("anbieter", "kennung", "anzahl", "summe"), show="headings",
|
r2, self.ut = w.scroll_tree(self, columns=("anbieter", "kennung", "anzahl", "summe"), show="headings",
|
||||||
height=min(6, len(unbekannt)), selectmode="browse")
|
height=min(6, len(unbekannt)), selectmode="extended")
|
||||||
for c, h, b, a in (("anbieter", "Anbieter", 90, "w"), ("kennung", "Kennung", 140, "w"),
|
for c, h, b, a in (("anbieter", "Anbieter", 90, "w"), ("kennung", "Kennung", 140, "w"),
|
||||||
("anzahl", "Pos.", 50, "e"), ("summe", "Summe", 90, "e")):
|
("anzahl", "Pos.", 50, "e"), ("summe", "Summe", 90, "e")):
|
||||||
self.ut.heading(c, text=h)
|
self.ut.heading(c, text=h)
|
||||||
@@ -451,14 +451,80 @@ class ImportBericht(w.Dialog):
|
|||||||
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)
|
||||||
if unbekannt:
|
if unbekannt:
|
||||||
w.knopf(k, "Kennung einem Händler zuordnen…", self._zuordnen).pack(side="left")
|
w.knopf(k, "Kennung einem Händler zuordnen...", self._zuordnen).pack(side="left")
|
||||||
|
w.knopf(k, "Als eigene ausblenden...", self._eigen).pack(side="left", padx=6)
|
||||||
w.knopf(k, "OK", self.abbrechen, breite=10, default="active").pack(side="right")
|
w.knopf(k, "OK", self.abbrechen, breite=10, default="active").pack(side="right")
|
||||||
self.bind("<Return>", lambda e: self.abbrechen())
|
self.bind("<Return>", lambda e: self.abbrechen())
|
||||||
|
|
||||||
def _zuordnen(self):
|
def _zuordnen(self):
|
||||||
s = self.ut.selection()
|
s = self.ut.selection()
|
||||||
if s:
|
if s:
|
||||||
self.result = tuple(s[0].split("\t", 1))
|
a, k = s[0].split("\t", 1)
|
||||||
|
self.result = ("zuordnen", a, k)
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def _eigen(self):
|
||||||
|
s = self.ut.selection()
|
||||||
|
if s:
|
||||||
|
self.result = ("eigen", [tuple(x.split("\t", 1)) for x in s])
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class EigeneKennungenDialog(w.Dialog):
|
||||||
|
"""Kennungen der eigenen Filiale: Positionen damit werden ausgeblendet und nicht exportiert."""
|
||||||
|
|
||||||
|
def __init__(self, master, db: Datenbank):
|
||||||
|
super().__init__(master, "Eigene Kennungen")
|
||||||
|
self.db, self.geaendert = db, False
|
||||||
|
tk.Label(self, text="Positionen mit diesen Kennungen gehören zur eigenen Filiale. Sie werden\n"
|
||||||
|
"ausgeblendet, nicht mitgerechnet und nicht exportiert.\n"
|
||||||
|
"(Ist eine Kennung einem Händler zugeordnet, hat der Händler Vorrang.)",
|
||||||
|
justify="left", anchor="w").pack(fill="x", padx=10, pady=(10, 4))
|
||||||
|
r, self.tree = w.scroll_tree(self, columns=("anbieter", "wert"), show="headings", height=7,
|
||||||
|
selectmode="extended")
|
||||||
|
self.tree.heading("anbieter", text="Anbieter")
|
||||||
|
self.tree.heading("wert", text="Unterhändler-Nr. / VO-Nr.")
|
||||||
|
self.tree.column("anbieter", width=100)
|
||||||
|
self.tree.column("wert", width=200)
|
||||||
|
r.pack(fill="both", expand=True, padx=10)
|
||||||
|
z = tk.Frame(self, bg=w.FACE)
|
||||||
|
z.pack(fill="x", padx=10, pady=6)
|
||||||
|
self.v_anb = tk.StringVar(value="freenet")
|
||||||
|
ttk.Combobox(z, textvariable=self.v_anb, values=list(ANBIETER.values()), width=10,
|
||||||
|
state="readonly").pack(side="left")
|
||||||
|
self.v_wert = tk.StringVar()
|
||||||
|
e = tk.Entry(z, textvariable=self.v_wert, width=18)
|
||||||
|
e.pack(side="left", padx=4)
|
||||||
|
e.bind("<Return>", lambda ev: self._dazu())
|
||||||
|
w.knopf(z, "Hinzufügen", self._dazu).pack(side="left")
|
||||||
|
w.knopf(z, "Entfernen", self._weg).pack(side="left", padx=4)
|
||||||
|
self.knopfleiste([("Schließen", self.abbrechen)]).pack(pady=(0, 10))
|
||||||
|
self._laden()
|
||||||
|
|
||||||
|
def _laden(self):
|
||||||
|
self.tree.delete(*self.tree.get_children())
|
||||||
|
for a, k in self.db.eigene_kennungen():
|
||||||
|
self.tree.insert("", "end", iid=f"{a}\t{k}", values=(ANBIETER.get(a, a), k))
|
||||||
|
|
||||||
|
def _dazu(self):
|
||||||
|
wert = self.v_wert.get().strip()
|
||||||
|
if not wert:
|
||||||
|
return
|
||||||
|
anb = next((k for k, v in ANBIETER.items() if v == self.v_anb.get()), self.v_anb.get())
|
||||||
|
self.db.eigene_setzen(anb, wert, True)
|
||||||
|
self.v_wert.set("")
|
||||||
|
self.geaendert = True
|
||||||
|
self._laden()
|
||||||
|
|
||||||
|
def _weg(self):
|
||||||
|
for iid in self.tree.selection():
|
||||||
|
a, k = iid.split("\t", 1)
|
||||||
|
self.db.eigene_setzen(a, k, False)
|
||||||
|
self.geaendert = True
|
||||||
|
self._laden()
|
||||||
|
|
||||||
|
def abbrechen(self):
|
||||||
|
self.result = self.geaendert
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
@@ -473,7 +539,7 @@ class KennungZuordnen(w.Dialog):
|
|||||||
self.v = tk.StringVar(value="__neu__")
|
self.v = tk.StringVar(value="__neu__")
|
||||||
f = tk.Frame(self, bg=w.FACE)
|
f = tk.Frame(self, bg=w.FACE)
|
||||||
f.pack(fill="x", padx=20)
|
f.pack(fill="x", padx=20)
|
||||||
tk.Radiobutton(f, text="Neuem Händler …", variable=self.v, value="__neu__").pack(anchor="w")
|
tk.Radiobutton(f, text="Neuem Händler ...", variable=self.v, value="__neu__").pack(anchor="w")
|
||||||
haendler = db.haendler_liste()
|
haendler = db.haendler_liste()
|
||||||
if haendler:
|
if haendler:
|
||||||
tk.Radiobutton(f, text="Bestehendem Händler:", variable=self.v, value="__alt__").pack(anchor="w")
|
tk.Radiobutton(f, text="Bestehendem Händler:", variable=self.v, value="__alt__").pack(anchor="w")
|
||||||
@@ -515,7 +581,7 @@ class HaendlerWaehlen(w.Dialog):
|
|||||||
self.cb = ttk.Combobox(self, values=list(self._hmap) + [self.KEINER], state="readonly", width=40)
|
self.cb = ttk.Combobox(self, values=list(self._hmap) + [self.KEINER], state="readonly", width=40)
|
||||||
self.cb.pack(padx=12, pady=4)
|
self.cb.pack(padx=12, pady=4)
|
||||||
tk.Label(self, text="Die feste Zuweisung hat Vorrang vor Kennungen und der GROSSBUCHSTABEN-Regel.\n"
|
tk.Label(self, text="Die feste Zuweisung hat Vorrang vor Kennungen und der GROSSBUCHSTABEN-Regel.\n"
|
||||||
"Rückgängig: Rechtsklick ▸ „Automatisch zuordnen“.", justify="left",
|
"Rückgängig: Rechtsklick > „Automatisch zuordnen“.", justify="left",
|
||||||
fg=w.SHADOW).pack(fill="x", padx=12)
|
fg=w.SHADOW).pack(fill="x", padx=12)
|
||||||
self.bind("<Return>", lambda e: self._ok())
|
self.bind("<Return>", lambda e: self._ok())
|
||||||
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=12)
|
self.knopfleiste([("OK", self._ok), ("Abbrechen", self.abbrechen)]).pack(pady=12)
|
||||||
@@ -571,7 +637,7 @@ class ExportDialog(w.Dialog):
|
|||||||
tk.Label(g, text="Zielordner:").grid(row=1, column=0, sticky="w")
|
tk.Label(g, text="Zielordner:").grid(row=1, column=0, sticky="w")
|
||||||
self.v_ordner = tk.StringVar(value=settings.get("export_ordner") or "")
|
self.v_ordner = tk.StringVar(value=settings.get("export_ordner") or "")
|
||||||
tk.Entry(g, textvariable=self.v_ordner, width=40).grid(row=1, column=1, sticky="we", pady=2)
|
tk.Entry(g, textvariable=self.v_ordner, width=40).grid(row=1, column=1, sticky="we", pady=2)
|
||||||
w.knopf(g, "Durchsuchen…", self._ordner).grid(row=1, column=2, padx=(4, 0))
|
w.knopf(g, "Durchsuchen...", self._ordner).grid(row=1, column=2, padx=(4, 0))
|
||||||
self.v_anb = tk.BooleanVar(value=settings.get("export_pro_anbieter", True))
|
self.v_anb = tk.BooleanVar(value=settings.get("export_pro_anbieter", True))
|
||||||
self.v_kopf = tk.BooleanVar(value=settings.get("export_kopfzeile", False))
|
self.v_kopf = tk.BooleanVar(value=settings.get("export_kopfzeile", False))
|
||||||
self.v_sum = tk.BooleanVar(value=settings.get("export_summe", True))
|
self.v_sum = tk.BooleanVar(value=settings.get("export_summe", True))
|
||||||
@@ -637,7 +703,7 @@ class EinstellungenDialog(w.Dialog):
|
|||||||
row=0, column=0, columnspan=2, sticky="w")
|
row=0, column=0, columnspan=2, sticky="w")
|
||||||
self.v = tk.StringVar(value=db_pfad)
|
self.v = tk.StringVar(value=db_pfad)
|
||||||
tk.Entry(g, textvariable=self.v, width=56).grid(row=1, column=0, sticky="we", pady=4)
|
tk.Entry(g, textvariable=self.v, width=56).grid(row=1, column=0, sticky="we", pady=4)
|
||||||
w.knopf(g, "Ändern…", self._waehlen).grid(row=1, column=1, padx=(4, 0))
|
w.knopf(g, "Ändern...", self._waehlen).grid(row=1, column=1, padx=(4, 0))
|
||||||
tk.Label(g, text="Tipp: Liegt die Datei auf einem Netzlaufwerk, können mehrere PCs dieselben\n"
|
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")
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
"""Menüs im Windows-95-Stil (Menüleiste, Aufklapp- und Kontextmenüs).
|
||||||
|
|
||||||
|
Die nativen Windows-Menüs sind weiß und wechseln nicht mit, wenn man bei offenem Menü mit der
|
||||||
|
Maus zum nächsten Menüleisten-Eintrag fährt. Diese Menüs sind selbst gezeichnet:
|
||||||
|
- grau, Markierung in Navy, Tastenkürzel rechtsbündig, Trennlinien eingelassen
|
||||||
|
- bei offenem Menü wechselt das Menü beim Überfahren der Menüleiste
|
||||||
|
- Tastatur: Pfeil hoch/runter wählen, Enter ausführen, Esc schließen, Pfeil links/rechts benachbartes Menü
|
||||||
|
- Klick daneben oder Wechsel in ein anderes Programm schließt das Menü
|
||||||
|
Die Schnittstelle (add_command, add_separator, tk_popup) entspricht tk.Menu.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from provtool.gui import win95 as w
|
||||||
|
|
||||||
|
|
||||||
|
class Popup:
|
||||||
|
"""Ein Aufklapp- bzw. Kontextmenü."""
|
||||||
|
|
||||||
|
def __init__(self, master: tk.Misc):
|
||||||
|
self.master = master
|
||||||
|
self.eintraege: list[dict] = []
|
||||||
|
self.fenster: tk.Toplevel | None = None
|
||||||
|
self.zeilen: list[tuple[dict, tk.Frame, list[tk.Label]]] = []
|
||||||
|
self.aktiv = -1
|
||||||
|
self.leiste: Menueleiste | None = None
|
||||||
|
self._pruefer = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- tk.Menu-kompatibel
|
||||||
|
def add_command(self, label: str, command=None, accelerator: str = "", state: str = "normal"):
|
||||||
|
self.eintraege.append(dict(art="befehl", label=label, command=command, accelerator=accelerator,
|
||||||
|
state=state))
|
||||||
|
|
||||||
|
def add_separator(self):
|
||||||
|
self.eintraege.append(dict(art="trenner"))
|
||||||
|
|
||||||
|
def tk_popup(self, x: int, y: int):
|
||||||
|
self.oeffnen(x, y)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Anzeige
|
||||||
|
@property
|
||||||
|
def offen(self) -> bool:
|
||||||
|
try:
|
||||||
|
return self.fenster is not None and bool(self.fenster.winfo_exists())
|
||||||
|
except tk.TclError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def oeffnen(self, x: int, y: int, leiste: Menueleiste | None = None):
|
||||||
|
self.schliessen()
|
||||||
|
self.leiste = leiste
|
||||||
|
root = self.master._root()
|
||||||
|
f = self.fenster = tk.Toplevel(root, bg=w.FACE, bd=2, relief="raised")
|
||||||
|
f.withdraw()
|
||||||
|
f.overrideredirect(True)
|
||||||
|
self.zeilen = []
|
||||||
|
for e in self.eintraege:
|
||||||
|
if e["art"] == "trenner":
|
||||||
|
t = tk.Frame(f, bg=w.FACE, height=8)
|
||||||
|
t.pack(fill="x")
|
||||||
|
linie = w.trenner(t, vertikal=False)
|
||||||
|
linie.place(relx=0, rely=0.5, relwidth=1, y=-1)
|
||||||
|
continue
|
||||||
|
zeile = tk.Frame(f, bg=w.FACE)
|
||||||
|
zeile.pack(fill="x")
|
||||||
|
grau = e["state"] == "disabled"
|
||||||
|
text = tk.Label(zeile, text=e["label"], bg=w.FACE, anchor="w", padx=18, pady=2,
|
||||||
|
fg=w.SHADOW if grau else w.DARK)
|
||||||
|
text.pack(side="left", fill="x", expand=True)
|
||||||
|
labels = [text]
|
||||||
|
if e["accelerator"]:
|
||||||
|
acc = tk.Label(zeile, text=e["accelerator"], bg=w.FACE, anchor="e", padx=12, pady=2,
|
||||||
|
fg=w.SHADOW if grau else w.DARK)
|
||||||
|
acc.pack(side="right")
|
||||||
|
labels.append(acc)
|
||||||
|
idx = len(self.zeilen)
|
||||||
|
for wdg in [zeile] + labels:
|
||||||
|
wdg.bind("<Enter>", lambda ev, i=idx: self._markieren(i))
|
||||||
|
wdg.bind("<ButtonRelease-1>", lambda ev, i=idx: self._ausfuehren(i))
|
||||||
|
self.zeilen.append((e, zeile, labels))
|
||||||
|
|
||||||
|
f.update_idletasks()
|
||||||
|
bw, bh = f.winfo_reqwidth(), f.winfo_reqheight()
|
||||||
|
sw, sh = f.winfo_screenwidth(), f.winfo_screenheight()
|
||||||
|
x = max(0, min(x, sw - bw))
|
||||||
|
y = max(0, min(y, sh - bh))
|
||||||
|
# Größe UND Position zusammen setzen: eine reine Positionsangabe ignoriert Tk unter Windows bei
|
||||||
|
# rahmenlosen Fenstern – das Menü landete sonst oben links in der Bildschirmecke.
|
||||||
|
f.geometry(f"{bw}x{bh}+{x}+{y}")
|
||||||
|
f.deiconify()
|
||||||
|
f.lift()
|
||||||
|
f.update_idletasks()
|
||||||
|
try:
|
||||||
|
from provtool.gui import win32
|
||||||
|
win32.besitzer_setzen(f, root) # bleibt über dem Hauptfenster
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Alle Maus-Ereignisse der Anwendung an das Menü (lokale Tk-Sperre): Klick daneben schließt,
|
||||||
|
# Bewegung über die Menüleiste wechselt das Menü.
|
||||||
|
try:
|
||||||
|
f.grab_set()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
f.focus_force()
|
||||||
|
f.bind("<ButtonPress>", self._klick)
|
||||||
|
f.bind("<Motion>", self._bewegung)
|
||||||
|
f.bind("<Escape>", lambda e: self.schliessen())
|
||||||
|
f.bind("<Up>", lambda e: self._schritt(-1))
|
||||||
|
f.bind("<Down>", lambda e: self._schritt(1))
|
||||||
|
f.bind("<Return>", lambda e: self._ausfuehren(self.aktiv))
|
||||||
|
f.bind("<KP_Enter>", lambda e: self._ausfuehren(self.aktiv))
|
||||||
|
f.bind("<Left>", lambda e: self.leiste and self.leiste.nachbar(-1))
|
||||||
|
f.bind("<Right>", lambda e: self.leiste and self.leiste.nachbar(1))
|
||||||
|
self.aktiv = -1
|
||||||
|
self._pruefer = f.after(250, self._fokus_pruefen)
|
||||||
|
|
||||||
|
def schliessen(self):
|
||||||
|
if self._pruefer is not None and self.fenster is not None:
|
||||||
|
try:
|
||||||
|
self.fenster.after_cancel(self._pruefer)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
self._pruefer = None
|
||||||
|
if self.offen:
|
||||||
|
try:
|
||||||
|
self.fenster.grab_release()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
self.fenster.destroy()
|
||||||
|
self.fenster = None
|
||||||
|
if self.leiste is not None:
|
||||||
|
self.leiste._geschlossen(self)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Bedienung
|
||||||
|
def _innerhalb(self, x_root: int, y_root: int) -> bool:
|
||||||
|
"""Liegt der Punkt über diesem Menü? Fragt das Fenstersystem, welches Fenster dort liegt –
|
||||||
|
die Geometrie rahmenloser Fenster meldet Tk unter Windows nicht zuverlässig."""
|
||||||
|
f = self.fenster
|
||||||
|
try:
|
||||||
|
unter = f.winfo_containing(x_root, y_root)
|
||||||
|
except (tk.TclError, KeyError):
|
||||||
|
unter = None
|
||||||
|
if unter is not None:
|
||||||
|
return unter.winfo_toplevel() is f
|
||||||
|
return (f.winfo_rootx() <= x_root < f.winfo_rootx() + f.winfo_width()
|
||||||
|
and f.winfo_rooty() <= y_root < f.winfo_rooty() + f.winfo_height())
|
||||||
|
|
||||||
|
def _klick(self, e):
|
||||||
|
if self._innerhalb(e.x_root, e.y_root):
|
||||||
|
return
|
||||||
|
if self.leiste is not None and self.leiste.knopf_bei(e.x_root, e.y_root) is not None:
|
||||||
|
knopf = self.leiste.knopf_bei(e.x_root, e.y_root)
|
||||||
|
if knopf is self.leiste.offener_knopf:
|
||||||
|
self.schliessen() # Klick auf den eigenen Menütitel klappt zu
|
||||||
|
else:
|
||||||
|
self.leiste.oeffnen(knopf)
|
||||||
|
return "break"
|
||||||
|
self.schliessen()
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _bewegung(self, e):
|
||||||
|
if self.leiste is None or self._innerhalb(e.x_root, e.y_root):
|
||||||
|
return
|
||||||
|
knopf = self.leiste.knopf_bei(e.x_root, e.y_root)
|
||||||
|
if knopf is not None and knopf is not self.leiste.offener_knopf:
|
||||||
|
self.leiste.oeffnen(knopf)
|
||||||
|
|
||||||
|
def _markieren(self, i: int):
|
||||||
|
self.aktiv = i
|
||||||
|
for j, (e, zeile, labels) in enumerate(self.zeilen):
|
||||||
|
an = j == i and e.get("state") != "disabled"
|
||||||
|
bg = w.NAVY if an else w.FACE
|
||||||
|
fg = w.WHITE if an else (w.SHADOW if e.get("state") == "disabled" else w.DARK)
|
||||||
|
zeile.configure(bg=bg)
|
||||||
|
for lbl in labels:
|
||||||
|
lbl.configure(bg=bg, fg=fg)
|
||||||
|
|
||||||
|
def _schritt(self, richtung: int):
|
||||||
|
if not self.zeilen:
|
||||||
|
return
|
||||||
|
i = self.aktiv
|
||||||
|
for _ in range(len(self.zeilen)):
|
||||||
|
i = (i + richtung) % len(self.zeilen)
|
||||||
|
if self.zeilen[i][0].get("state") != "disabled":
|
||||||
|
break
|
||||||
|
self._markieren(i)
|
||||||
|
|
||||||
|
def _ausfuehren(self, i: int):
|
||||||
|
if not (0 <= i < len(self.zeilen)):
|
||||||
|
return
|
||||||
|
e = self.zeilen[i][0]
|
||||||
|
if e.get("state") == "disabled":
|
||||||
|
return
|
||||||
|
self.schliessen()
|
||||||
|
if e.get("command"):
|
||||||
|
# nach dem Schließen ausführen (der Befehl öffnet evtl. einen Dialog)
|
||||||
|
self.master.after_idle(e["command"])
|
||||||
|
|
||||||
|
def _fokus_pruefen(self):
|
||||||
|
"""In ein anderes Programm gewechselt -> Menü schließen."""
|
||||||
|
self._pruefer = None
|
||||||
|
if not self.offen:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
weg = self.fenster.focus_get() is None
|
||||||
|
except (tk.TclError, KeyError):
|
||||||
|
weg = False
|
||||||
|
if weg:
|
||||||
|
self.schliessen()
|
||||||
|
else:
|
||||||
|
self._pruefer = self.fenster.after(250, self._fokus_pruefen)
|
||||||
|
|
||||||
|
|
||||||
|
class Menueleiste(tk.Frame):
|
||||||
|
"""Graue Menüleiste mit Popup-Menüs (die native von Windows 10/11 ist weiß)."""
|
||||||
|
|
||||||
|
def __init__(self, master, root: tk.Misc):
|
||||||
|
super().__init__(master, bg=w.FACE)
|
||||||
|
self.root = root
|
||||||
|
self.knoepfe: dict[str, tk.Label] = {}
|
||||||
|
self.menues: dict[tk.Label, Popup] = {}
|
||||||
|
self.offener_knopf: tk.Label | None = None
|
||||||
|
|
||||||
|
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",
|
||||||
|
underline=-1 if underline is None else underline)
|
||||||
|
knopf.pack(side="left")
|
||||||
|
popup = Popup(self)
|
||||||
|
self.knoepfe[label] = knopf
|
||||||
|
self.menues[knopf] = popup
|
||||||
|
knopf.bind("<ButtonPress-1>", lambda e, k=knopf: self._klick(k))
|
||||||
|
knopf.bind("<Enter>", lambda e, k=knopf: self._hover(k, True))
|
||||||
|
knopf.bind("<Leave>", lambda e, k=knopf: self._hover(k, False))
|
||||||
|
if underline is not None:
|
||||||
|
taste = label[underline].lower()
|
||||||
|
self.root.bind_all(f"<Alt-KeyPress-{taste}>", lambda e, k=knopf: self._alt(k))
|
||||||
|
return popup
|
||||||
|
|
||||||
|
# ---- Zustand
|
||||||
|
def knopf_bei(self, x_root: int, y_root: int) -> tk.Label | None:
|
||||||
|
for k in self.knoepfe.values():
|
||||||
|
if (k.winfo_rootx() <= x_root < k.winfo_rootx() + k.winfo_width()
|
||||||
|
and k.winfo_rooty() <= y_root < k.winfo_rooty() + k.winfo_height()):
|
||||||
|
return k
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _darstellen(self):
|
||||||
|
for k in self.knoepfe.values():
|
||||||
|
if k is self.offener_knopf:
|
||||||
|
k.configure(bg=w.NAVY, fg=w.WHITE, relief="flat")
|
||||||
|
else:
|
||||||
|
k.configure(bg=w.FACE, fg=w.DARK, relief="flat")
|
||||||
|
|
||||||
|
# ---- Bedienung
|
||||||
|
def oeffnen(self, knopf: tk.Label):
|
||||||
|
from provtool.gui import modal
|
||||||
|
if modal.offen():
|
||||||
|
return
|
||||||
|
alt = self.offener_knopf
|
||||||
|
if alt is not None and alt is not knopf:
|
||||||
|
self.menues[alt].leiste = None # ohne Rückmeldung schließen, wir öffnen gleich das nächste
|
||||||
|
self.menues[alt].schliessen()
|
||||||
|
self.offener_knopf = knopf
|
||||||
|
self._darstellen()
|
||||||
|
self.menues[knopf].oeffnen(knopf.winfo_rootx(), knopf.winfo_rooty() + knopf.winfo_height(), leiste=self)
|
||||||
|
|
||||||
|
def schliessen(self):
|
||||||
|
if self.offener_knopf is not None:
|
||||||
|
self.menues[self.offener_knopf].schliessen()
|
||||||
|
|
||||||
|
def _geschlossen(self, popup: Popup):
|
||||||
|
if self.offener_knopf is not None and self.menues.get(self.offener_knopf) is popup:
|
||||||
|
self.offener_knopf = None
|
||||||
|
self._darstellen()
|
||||||
|
|
||||||
|
def nachbar(self, richtung: int):
|
||||||
|
liste = list(self.knoepfe.values())
|
||||||
|
if self.offener_knopf in liste:
|
||||||
|
i = (liste.index(self.offener_knopf) + richtung) % len(liste)
|
||||||
|
self.oeffnen(liste[i])
|
||||||
|
|
||||||
|
def _klick(self, knopf):
|
||||||
|
if self.offener_knopf is knopf:
|
||||||
|
self.schliessen()
|
||||||
|
else:
|
||||||
|
self.oeffnen(knopf)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _hover(self, knopf, drin: bool):
|
||||||
|
if self.offener_knopf is None:
|
||||||
|
knopf.configure(relief="raised" if drin else "flat")
|
||||||
|
|
||||||
|
def _alt(self, knopf):
|
||||||
|
self.oeffnen(knopf)
|
||||||
|
if self.offener_knopf is knopf:
|
||||||
|
self.menues[knopf]._schritt(1)
|
||||||
|
return "break"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""Modale Sperre ohne Tk-„grab“: Nur der INHALT des aufrufenden Fensters wird gesperrt.
|
||||||
|
|
||||||
|
Titelleiste und Rand bleiben bedienbar – das Hauptfenster lässt sich also verschieben, maximieren
|
||||||
|
und in der Größe ändern, während ein Dialog offen ist. Klickt man auf den gesperrten Inhalt, kommt
|
||||||
|
der Dialog nach vorn und blinkt kurz (wie bei Windows).
|
||||||
|
|
||||||
|
Technik: Jedes Widget im gesperrten Bereich bekommt vorn das Bindungs-Tag ``Pm95Gesperrt``. Dessen
|
||||||
|
Bindungen laufen vor allen anderen und beenden Klicks/Tastendrücke mit „break“.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
TAG = "Pm95Gesperrt"
|
||||||
|
_stapel: list[tk.Toplevel] = [] # offene modale Fenster, innerstes zuletzt
|
||||||
|
|
||||||
|
GESPERRTE_EREIGNISSE = ("<ButtonPress>", "<ButtonRelease>", "<Double-ButtonPress>", "<Triple-ButtonPress>",
|
||||||
|
"<B1-Motion>", "<KeyPress>", "<KeyRelease>", "<MouseWheel>")
|
||||||
|
|
||||||
|
|
||||||
|
def oberstes() -> tk.Toplevel | None:
|
||||||
|
"""Das zuletzt geöffnete, noch offene modale Fenster (oder None)."""
|
||||||
|
while _stapel:
|
||||||
|
d = _stapel[-1]
|
||||||
|
try:
|
||||||
|
if d.winfo_exists():
|
||||||
|
return d
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
_stapel.pop()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def offen() -> bool:
|
||||||
|
return oberstes() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _gesperrt_geklickt(event):
|
||||||
|
d = oberstes()
|
||||||
|
if d is not None and hasattr(d, "nach_vorn"):
|
||||||
|
d.nach_vorn(blinken=str(event.type) in ("ButtonPress", "4"))
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def _einrichten(root: tk.Misc):
|
||||||
|
# Merker am Tk-Objekt selbst (nicht am Namen: das Hauptfenster heißt in jeder Tk-Instanz „.“)
|
||||||
|
if getattr(root, "_pm95_sperre_eingerichtet", False):
|
||||||
|
return
|
||||||
|
for seq in GESPERRTE_EREIGNISSE:
|
||||||
|
root.bind_class(TAG, seq, _gesperrt_geklickt)
|
||||||
|
root._pm95_sperre_eingerichtet = True
|
||||||
|
|
||||||
|
|
||||||
|
def sperrbereiche(fenster: tk.Misc) -> list[tk.Misc]:
|
||||||
|
"""Welche Widgets eines Fensters gesperrt werden: alles außer Titelleiste (und Rand)."""
|
||||||
|
eigene = getattr(fenster, "sperrbereich", None)
|
||||||
|
if callable(eigene):
|
||||||
|
return list(eigene())
|
||||||
|
titel = getattr(fenster, "titelleiste", None)
|
||||||
|
return [k for k in fenster.winfo_children() if k is not titel and not isinstance(k, tk.Toplevel)]
|
||||||
|
|
||||||
|
|
||||||
|
class Sperre:
|
||||||
|
"""Sperrt den Inhalt von `fenster`, solange `dialog` offen ist."""
|
||||||
|
|
||||||
|
def __init__(self, fenster: tk.Misc, dialog: tk.Toplevel):
|
||||||
|
_einrichten(fenster._root())
|
||||||
|
self.widgets: list[tk.Misc] = []
|
||||||
|
for bereich in sperrbereiche(fenster):
|
||||||
|
self._sperren(bereich)
|
||||||
|
_stapel.append(dialog)
|
||||||
|
self.dialog = dialog
|
||||||
|
|
||||||
|
def _sperren(self, w: tk.Misc):
|
||||||
|
if isinstance(w, tk.Toplevel):
|
||||||
|
return # eigene Fenster (z. B. andere Dialoge) nicht anfassen
|
||||||
|
tags = w.bindtags()
|
||||||
|
if TAG not in tags:
|
||||||
|
w.bindtags((TAG,) + tuple(tags))
|
||||||
|
self.widgets.append(w)
|
||||||
|
for kind in w.winfo_children():
|
||||||
|
self._sperren(kind)
|
||||||
|
|
||||||
|
def aufheben(self):
|
||||||
|
for w in self.widgets:
|
||||||
|
try:
|
||||||
|
if w.winfo_exists():
|
||||||
|
w.bindtags(tuple(t for t in w.bindtags() if t != TAG))
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
self.widgets = []
|
||||||
|
if self.dialog in _stapel:
|
||||||
|
_stapel.remove(self.dialog)
|
||||||
+79
-34
@@ -183,38 +183,33 @@ class Titelleiste(tk.Frame):
|
|||||||
|
|
||||||
def _start(self, e):
|
def _start(self, e):
|
||||||
self._ab = (e.x_root - self.fenster.winfo_rootx(), e.y_root - self.fenster.winfo_rooty())
|
self._ab = (e.x_root - self.fenster.winfo_rootx(), e.y_root - self.fenster.winfo_rooty())
|
||||||
|
self._nativ = False
|
||||||
|
if NATIV_VERSCHIEBEN and IS_WIN and self.ziehen_erlaubt():
|
||||||
|
# Windows übernimmt das Ziehen: flüssig und mit Andocken am Bildschirmrand
|
||||||
|
from provtool.gui import win32
|
||||||
|
self._nativ = win32.nativ_verschieben(self.fenster)
|
||||||
|
if self._nativ:
|
||||||
|
return "break"
|
||||||
|
|
||||||
def _ziehen(self, e):
|
def _ziehen(self, e):
|
||||||
if not self.ziehen_erlaubt():
|
if self._nativ or not self.ziehen_erlaubt():
|
||||||
return
|
return
|
||||||
self.fenster.geometry(f"+{e.x_root - self._ab[0]}+{e.y_root - self._ab[1]}")
|
self.fenster.geometry(f"+{e.x_root - self._ab[0]}+{e.y_root - self._ab[1]}")
|
||||||
|
|
||||||
|
def blinken(self, mal: int = 3):
|
||||||
|
"""Titelleiste kurz grau/blau blinken lassen (wie Windows bei gesperrtem Fenster)."""
|
||||||
|
farben = [w.SHADOW, w.NAVY] * mal
|
||||||
|
|
||||||
class Menueleiste(tk.Frame):
|
def schritt(i=0):
|
||||||
"""Graue Menüleiste (die native von Windows 10/11 ist weiß und lässt sich nicht umfärben)."""
|
if i >= len(farben) or not self.winfo_exists():
|
||||||
|
return
|
||||||
|
for widget in (self, self.titel_lbl):
|
||||||
|
widget.configure(bg=farben[i])
|
||||||
|
self.after(70, lambda: schritt(i + 1))
|
||||||
|
schritt()
|
||||||
|
|
||||||
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:
|
NATIV_VERSCHIEBEN = True # Tests können das abschalten, um den Tk-Weg zu prüfen
|
||||||
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
|
# --------------------------------------------------------------------------- Hauptfenster
|
||||||
@@ -222,7 +217,8 @@ class Menueleiste(tk.Frame):
|
|||||||
class Hauptrahmen:
|
class Hauptrahmen:
|
||||||
"""Umgibt das Hauptfenster. Im klassischen Modus mit eigener Titelleiste, sonst nur ein Container."""
|
"""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
|
RAND = 6 # Pixel am Rand, an denen man die Größe ziehen kann (= sichtbarer Rahmen)
|
||||||
|
ECKE = 18 # so weit vom Eck entfernt gilt der Rand noch als Ecke (diagonal ziehen)
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk, titel: str, icon: tk.PhotoImage, schliessen, klassisch: bool = True,
|
def __init__(self, root: tk.Tk, titel: str, icon: tk.PhotoImage, schliessen, klassisch: bool = True,
|
||||||
minbreite: int = 900, minhoehe: int = 520):
|
minbreite: int = 900, minhoehe: int = 520):
|
||||||
@@ -237,10 +233,11 @@ class Hauptrahmen:
|
|||||||
self.aussen.pack(fill="both", expand=True)
|
self.aussen.pack(fill="both", expand=True)
|
||||||
self.inhalt = self.aussen
|
self.inhalt = self.aussen
|
||||||
self.titelleiste = None
|
self.titelleiste = None
|
||||||
|
root.sperrbereich = lambda: [self.inhalt] # type: ignore[attr-defined] # für modal.py
|
||||||
return
|
return
|
||||||
|
|
||||||
root.overrideredirect(True)
|
root.overrideredirect(True)
|
||||||
self.aussen = tk.Frame(root, bg=w.FACE, bd=2, relief="raised", padx=2, pady=2)
|
self.aussen = tk.Frame(root, bg=w.FACE, bd=2, relief="raised", padx=self.RAND - 2, pady=self.RAND - 2)
|
||||||
self.aussen.pack(fill="both", expand=True)
|
self.aussen.pack(fill="both", expand=True)
|
||||||
self.titelleiste = Titelleiste(self.aussen, root, titel, icon, ("min", "max", "close"),
|
self.titelleiste = Titelleiste(self.aussen, root, titel, icon, ("min", "max", "close"),
|
||||||
schliessen=schliessen, minimieren=self.minimieren,
|
schliessen=schliessen, minimieren=self.minimieren,
|
||||||
@@ -249,10 +246,17 @@ class Hauptrahmen:
|
|||||||
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert
|
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert
|
||||||
self.inhalt = tk.Frame(self.aussen, bg=w.FACE)
|
self.inhalt = tk.Frame(self.aussen, bg=w.FACE)
|
||||||
self.inhalt.pack(fill="both", expand=True)
|
self.inhalt.pack(fill="both", expand=True)
|
||||||
|
# Gesperrt wird bei offenem Dialog nur der Inhalt – Titelleiste und Rand bleiben bedienbar
|
||||||
|
root.sperrbereich = lambda: [self.inhalt] # type: ignore[attr-defined]
|
||||||
|
|
||||||
self._rand: str = ""
|
self._rand: str = ""
|
||||||
|
self._resize_geplant = None
|
||||||
self.aussen.bind("<Motion>", self._rand_cursor)
|
self.aussen.bind("<Motion>", self._rand_cursor)
|
||||||
self.aussen.bind("<Leave>", lambda e: self.aussen.configure(cursor=""))
|
# Mauszeiger zurücksetzen, sobald der Rand verlassen wird – Kindfenster erben sonst den
|
||||||
|
# Größenänderungs-Zeiger, wenn das letzte Motion-Ereignis am Rand verpasst wurde.
|
||||||
|
self.aussen.bind("<Leave>", self._cursor_zurueck)
|
||||||
|
self.inhalt.bind("<Enter>", self._cursor_zurueck)
|
||||||
|
self.titelleiste.bind("<Enter>", self._cursor_zurueck, add="+")
|
||||||
self.aussen.bind("<ButtonPress-1>", self._resize_start)
|
self.aussen.bind("<ButtonPress-1>", self._resize_start)
|
||||||
self.aussen.bind("<B1-Motion>", self._resize)
|
self.aussen.bind("<B1-Motion>", self._resize)
|
||||||
root.bind_all("<Alt-F4>", lambda e: self.alt_f4(e.widget))
|
root.bind_all("<Alt-F4>", lambda e: self.alt_f4(e.widget))
|
||||||
@@ -264,7 +268,8 @@ class Hauptrahmen:
|
|||||||
# Ist ein modaler Dialog offen, gilt Alt+F4 immer ihm – egal wo der Tastaturfokus liegt
|
# Ist ein modaler Dialog offen, gilt Alt+F4 immer ihm – egal wo der Tastaturfokus liegt
|
||||||
# (unter Windows liefert Tk Tastendrücke an das Fenster mit Fokus, das kann noch das
|
# (unter Windows liefert Tk Tastendrücke an das Fenster mit Fokus, das kann noch das
|
||||||
# Hauptfenster sein).
|
# Hauptfenster sein).
|
||||||
sperre = self.root.grab_current()
|
from provtool.gui import modal
|
||||||
|
sperre = self.root.grab_current() or modal.oberstes()
|
||||||
popup = self._offenes_popup()
|
popup = self._offenes_popup()
|
||||||
if sperre is not None and sperre.winfo_toplevel() is not self.root:
|
if sperre is not None and sperre.winfo_toplevel() is not self.root:
|
||||||
fenster = sperre.winfo_toplevel()
|
fenster = sperre.winfo_toplevel()
|
||||||
@@ -275,6 +280,11 @@ class Hauptrahmen:
|
|||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
fenster = self.root
|
fenster = self.root
|
||||||
if fenster is self.root:
|
if fenster is self.root:
|
||||||
|
from provtool.gui import modal
|
||||||
|
offen = modal.oberstes()
|
||||||
|
if offen is not None: # Alt+F4 bei offenem Dialog schließt den Dialog
|
||||||
|
offen.abbrechen()
|
||||||
|
return "break"
|
||||||
self._schliessen()
|
self._schliessen()
|
||||||
elif hasattr(fenster, "abbrechen"):
|
elif hasattr(fenster, "abbrechen"):
|
||||||
fenster.abbrechen()
|
fenster.abbrechen()
|
||||||
@@ -338,7 +348,14 @@ class Hauptrahmen:
|
|||||||
|
|
||||||
# ---- Fensterfunktionen
|
# ---- Fensterfunktionen
|
||||||
def minimieren(self):
|
def minimieren(self):
|
||||||
# Windows minimiert keine overrideredirect-Fenster → kurz normalen Rahmen aktivieren
|
if IS_WIN:
|
||||||
|
# Direkt über Windows: offene Dialoge (gehören dem Hauptfenster) werden mit aus- und
|
||||||
|
# wieder eingeblendet. NICHT overrideredirect umschalten – Tk baut dabei das Fenster neu
|
||||||
|
# und Windows würde die zugehörigen Dialoge zerstören.
|
||||||
|
from provtool.gui import win32
|
||||||
|
if win32.minimieren(self.root):
|
||||||
|
return
|
||||||
|
# Tk minimiert keine overrideredirect-Fenster -> kurz normalen Rahmen aktivieren
|
||||||
geo = self.root.geometry()
|
geo = self.root.geometry()
|
||||||
self.root.overrideredirect(False)
|
self.root.overrideredirect(False)
|
||||||
self.root.iconify()
|
self.root.iconify()
|
||||||
@@ -373,9 +390,25 @@ class Hauptrahmen:
|
|||||||
|
|
||||||
# ---- Größe ändern
|
# ---- Größe ändern
|
||||||
def _welcher_rand(self, x, y) -> str:
|
def _welcher_rand(self, x, y) -> str:
|
||||||
bw, bh, r = self.aussen.winfo_width(), self.aussen.winfo_height(), self.RAND
|
bw, bh, r, k = self.aussen.winfo_width(), self.aussen.winfo_height(), self.RAND, self.ECKE
|
||||||
rand = ("n" if y < r else "s" if y >= bh - r else "") + ("w" if x < r else "e" if x >= bw - r else "")
|
ns = "n" if y < r else "s" if y >= bh - r else ""
|
||||||
return rand
|
we = "w" if x < r else "e" if x >= bw - r else ""
|
||||||
|
# an einer Kante nahe am Eck -> Ecke (diagonal), wie bei Windows
|
||||||
|
if ns and not we:
|
||||||
|
we = "w" if x < k else "e" if x >= bw - k else ""
|
||||||
|
elif we and not ns:
|
||||||
|
ns = "n" if y < k else "s" if y >= bh - k else ""
|
||||||
|
return ns + we
|
||||||
|
|
||||||
|
def _cursor_zurueck(self, _e=None):
|
||||||
|
if self._rand_zeiger:
|
||||||
|
self._rand_zeiger = False
|
||||||
|
try:
|
||||||
|
self.aussen.configure(cursor="")
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_rand_zeiger = False
|
||||||
|
|
||||||
CURSOR_WIN = {"n": "size_ns", "s": "size_ns", "e": "size_we", "w": "size_we",
|
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"}
|
"nw": "size_nw_se", "se": "size_nw_se", "ne": "size_ne_sw", "sw": "size_ne_sw"}
|
||||||
@@ -387,8 +420,10 @@ class Hauptrahmen:
|
|||||||
if self.maximiert:
|
if self.maximiert:
|
||||||
return
|
return
|
||||||
rand = self._welcher_rand(e.x, e.y)
|
rand = self._welcher_rand(e.x, e.y)
|
||||||
|
zeiger = (self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, "")
|
||||||
try:
|
try:
|
||||||
self.aussen.configure(cursor=(self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, ""))
|
self.aussen.configure(cursor=zeiger)
|
||||||
|
self._rand_zeiger = bool(zeiger)
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -412,4 +447,14 @@ class Hauptrahmen:
|
|||||||
if "n" in self._rand:
|
if "n" in self._rand:
|
||||||
neu = max(self.minhoehe, bh - dy)
|
neu = max(self.minhoehe, bh - dy)
|
||||||
y, bh = y + (bh - neu), neu
|
y, bh = y + (bh - neu), neu
|
||||||
self.root.geometry(f"{bw}x{bh}+{x}+{y}")
|
# Mausbewegungen bündeln: nur die jeweils letzte Größe anwenden (flüssiger unter Windows)
|
||||||
|
self._resize_ziel = f"{bw}x{bh}+{x}+{y}"
|
||||||
|
if self._resize_geplant is None:
|
||||||
|
self._resize_geplant = self.root.after(15, self._resize_anwenden)
|
||||||
|
|
||||||
|
def _resize_anwenden(self):
|
||||||
|
self._resize_geplant = None
|
||||||
|
try:
|
||||||
|
self.root.geometry(self._resize_ziel)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
@@ -50,6 +50,20 @@ def user32():
|
|||||||
u.GetTopWindow.restype = wintypes.HWND
|
u.GetTopWindow.restype = wintypes.HWND
|
||||||
u.BringWindowToTop.argtypes = [wintypes.HWND]
|
u.BringWindowToTop.argtypes = [wintypes.HWND]
|
||||||
u.BringWindowToTop.restype = wintypes.BOOL
|
u.BringWindowToTop.restype = wintypes.BOOL
|
||||||
|
u.GetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int]
|
||||||
|
u.GetWindowLongW.restype = ctypes.c_long
|
||||||
|
u.SetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_long]
|
||||||
|
u.SetWindowLongW.restype = ctypes.c_long
|
||||||
|
u.ReleaseCapture.argtypes = []
|
||||||
|
u.ReleaseCapture.restype = wintypes.BOOL
|
||||||
|
u.SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
|
||||||
|
u.SendMessageW.restype = wintypes.LPARAM
|
||||||
|
u.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
|
||||||
|
u.ShowWindow.restype = wintypes.BOOL
|
||||||
|
u.IsIconic.argtypes = [wintypes.HWND]
|
||||||
|
u.IsIconic.restype = wintypes.BOOL
|
||||||
|
u.FlashWindow.argtypes = [wintypes.HWND, wintypes.BOOL]
|
||||||
|
u.FlashWindow.restype = wintypes.BOOL
|
||||||
_u32 = u
|
_u32 = u
|
||||||
return _u32
|
return _u32
|
||||||
|
|
||||||
@@ -82,6 +96,62 @@ def nach_vorn(fenster: tk.Misc) -> None:
|
|||||||
user32().BringWindowToTop(h)
|
user32().BringWindowToTop(h)
|
||||||
|
|
||||||
|
|
||||||
|
GWL_EXSTYLE, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW = -20, 0x00040000, 0x00000080
|
||||||
|
|
||||||
|
|
||||||
|
def hat_taskleisteneintrag(fenster: tk.Misc) -> bool:
|
||||||
|
h = hwnd(fenster)
|
||||||
|
if not h:
|
||||||
|
return False
|
||||||
|
stil = user32().GetWindowLongW(h, GWL_EXSTYLE)
|
||||||
|
return bool(stil & WS_EX_APPWINDOW) and not (stil & WS_EX_TOOLWINDOW)
|
||||||
|
|
||||||
|
|
||||||
|
def taskleisteneintrag(fenster: tk.Misc) -> bool:
|
||||||
|
"""Eigener Eintrag in Taskleiste/Alt+Tab (rahmenlose Tk-Fenster haben sonst keinen).
|
||||||
|
Wirkt erst nach erneutem Einblenden – gibt True zurück, wenn das nötig ist."""
|
||||||
|
h = hwnd(fenster)
|
||||||
|
if not h:
|
||||||
|
return False
|
||||||
|
stil = user32().GetWindowLongW(h, GWL_EXSTYLE)
|
||||||
|
neu = (stil & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
|
||||||
|
if neu != stil:
|
||||||
|
user32().SetWindowLongW(h, GWL_EXSTYLE, neu)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def nativ_verschieben(fenster: tk.Misc) -> bool:
|
||||||
|
"""Windows übernimmt das Ziehen (flüssig, mit Andocken am Bildschirmrand). True bei Erfolg."""
|
||||||
|
h = hwnd(fenster)
|
||||||
|
if not h:
|
||||||
|
return False
|
||||||
|
user32().ReleaseCapture()
|
||||||
|
user32().SendMessageW(h, 0x00A1, 2, 0) # WM_NCLBUTTONDOWN, HTCAPTION
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def minimieren(fenster: tk.Misc) -> bool:
|
||||||
|
"""Direkt über Windows minimieren – Tk verweigert das bei rahmenlosen Fenstern. Eigene Popups
|
||||||
|
(Dialoge) werden dabei von Windows mit ausgeblendet und beim Wiederherstellen mit eingeblendet."""
|
||||||
|
h = hwnd(fenster)
|
||||||
|
if not h:
|
||||||
|
return False
|
||||||
|
user32().ShowWindow(h, 6) # SW_MINIMIZE
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ist_minimiert(fenster: tk.Misc) -> bool:
|
||||||
|
h = hwnd(fenster)
|
||||||
|
return bool(user32().IsIconic(h)) if h else False
|
||||||
|
|
||||||
|
|
||||||
|
def blinken(fenster: tk.Misc) -> None:
|
||||||
|
h = hwnd(fenster)
|
||||||
|
if h:
|
||||||
|
user32().FlashWindow(h, True)
|
||||||
|
|
||||||
|
|
||||||
def besitzer_setzen(fenster: tk.Misc, besitzer: tk.Misc) -> None:
|
def besitzer_setzen(fenster: tk.Misc, besitzer: tk.Misc) -> None:
|
||||||
h, b = hwnd(fenster), hwnd(besitzer)
|
h, b = hwnd(fenster), hwnd(besitzer)
|
||||||
if h and b:
|
if h and b:
|
||||||
|
|||||||
+124
-43
@@ -294,6 +294,9 @@ def apply(root: tk.Tk) -> None:
|
|||||||
o("*Background", FACE)
|
o("*Background", FACE)
|
||||||
o("*Foreground", DARK)
|
o("*Foreground", DARK)
|
||||||
o("*activeBackground", FACE)
|
o("*activeBackground", FACE)
|
||||||
|
# Tk-Eigenheit: „activeForeground“ gehört zur Optionsklasse „Background“ – ohne diese Zeile
|
||||||
|
# wäre die Schrift gedrückter Knöpfe grau auf grau (unsichtbar).
|
||||||
|
o("*activeForeground", DARK)
|
||||||
o("*highlightThickness", 0)
|
o("*highlightThickness", 0)
|
||||||
o("*selectBackground", NAVY)
|
o("*selectBackground", NAVY)
|
||||||
o("*selectForeground", WHITE)
|
o("*selectForeground", WHITE)
|
||||||
@@ -322,6 +325,7 @@ def apply(root: tk.Tk) -> None:
|
|||||||
|
|
||||||
from tkinter import font as tkfont
|
from tkinter import font as tkfont
|
||||||
zeile = max(18, tkfont.Font(root=root, font=FONT).metrics("linespace") + 5)
|
zeile = max(18, tkfont.Font(root=root, font=FONT).metrics("linespace") + 5)
|
||||||
|
wort_loeschen_einrichten(root)
|
||||||
st = ttk.Style(root)
|
st = ttk.Style(root)
|
||||||
st.theme_use("classic" if "classic" in st.theme_names() else "default")
|
st.theme_use("classic" if "classic" in st.theme_names() else "default")
|
||||||
st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=LIGHT,
|
st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=LIGHT,
|
||||||
@@ -341,6 +345,78 @@ def apply(root: tk.Tk) -> None:
|
|||||||
st.configure("Sash", sashthickness=6, background=FACE)
|
st.configure("Sash", sashthickness=6, background=FACE)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Wort löschen
|
||||||
|
|
||||||
|
def _wortgrenze(text: str, pos: int, richtung: int) -> int:
|
||||||
|
"""Position der nächsten Wortgrenze von pos aus (richtung -1 = links, +1 = rechts),
|
||||||
|
wie Strg+Rücktaste / Strg+Entf unter Windows: erst Leerzeichen, dann das Wort."""
|
||||||
|
if richtung < 0:
|
||||||
|
i = pos
|
||||||
|
while i > 0 and text[i - 1].isspace():
|
||||||
|
i -= 1
|
||||||
|
if i > 0 and text[i - 1].isalnum():
|
||||||
|
while i > 0 and text[i - 1].isalnum():
|
||||||
|
i -= 1
|
||||||
|
elif i > 0:
|
||||||
|
i -= 1 # Satzzeichen einzeln
|
||||||
|
return i
|
||||||
|
i = pos
|
||||||
|
if i < len(text) and text[i].isalnum():
|
||||||
|
while i < len(text) and text[i].isalnum():
|
||||||
|
i += 1
|
||||||
|
elif i < len(text):
|
||||||
|
i += 1
|
||||||
|
while i < len(text) and text[i].isspace():
|
||||||
|
i += 1
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def _wort_weg(event, richtung: int):
|
||||||
|
e = event.widget
|
||||||
|
try:
|
||||||
|
if str(e.cget("state")) in ("disabled", "readonly"):
|
||||||
|
return "break"
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if e.selection_present():
|
||||||
|
e.delete("sel.first", "sel.last")
|
||||||
|
return "break"
|
||||||
|
except (tk.TclError, AttributeError):
|
||||||
|
pass
|
||||||
|
text, pos = e.get(), e.index("insert")
|
||||||
|
grenze = _wortgrenze(text, pos, richtung)
|
||||||
|
if richtung < 0 and grenze < pos:
|
||||||
|
e.delete(grenze, pos)
|
||||||
|
elif richtung > 0 and grenze > pos:
|
||||||
|
e.delete(pos, grenze)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def _text_wort_weg(event, richtung: int):
|
||||||
|
t = event.widget
|
||||||
|
if str(t.cget("state")) == "disabled":
|
||||||
|
return "break"
|
||||||
|
if t.tag_ranges("sel"):
|
||||||
|
t.delete("sel.first", "sel.last")
|
||||||
|
return "break"
|
||||||
|
if richtung < 0:
|
||||||
|
t.delete("insert -1c wordstart" if t.get("insert -1c").isspace() else "insert wordstart", "insert")
|
||||||
|
else:
|
||||||
|
t.delete("insert", "insert wordend")
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def wort_loeschen_einrichten(root: tk.Misc):
|
||||||
|
"""Strg+Rücktaste / Strg+Entf löschen das ganze Wort – in allen Eingabefeldern (Tk kann das
|
||||||
|
unter Windows von Haus aus nicht, dort wird nur ein Zeichen gelöscht)."""
|
||||||
|
for klasse in ("Entry", "TEntry", "TCombobox", "Spinbox", "TSpinbox"):
|
||||||
|
root.bind_class(klasse, "<Control-BackSpace>", lambda e: _wort_weg(e, -1))
|
||||||
|
root.bind_class(klasse, "<Control-Delete>", lambda e: _wort_weg(e, 1))
|
||||||
|
root.bind_class("Text", "<Control-BackSpace>", lambda e: _text_wort_weg(e, -1))
|
||||||
|
root.bind_class("Text", "<Control-Delete>", lambda e: _text_wort_weg(e, 1))
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- Bausteine
|
# --------------------------------------------------------------------------- Bausteine
|
||||||
|
|
||||||
class Sunken(tk.Frame):
|
class Sunken(tk.Frame):
|
||||||
@@ -434,8 +510,12 @@ class Tooltip:
|
|||||||
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
|
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
|
||||||
self.tip = tw = tk.Toplevel(self.widget)
|
self.tip = tw = tk.Toplevel(self.widget)
|
||||||
tw.wm_overrideredirect(True)
|
tw.wm_overrideredirect(True)
|
||||||
tw.wm_geometry(f"+{x}+{y}")
|
tw.withdraw()
|
||||||
tk.Label(tw, text=self.text, bg=INFO_BG, bd=1, relief="solid", padx=4, pady=1).pack()
|
tk.Label(tw, text=self.text, bg=INFO_BG, bd=1, relief="solid", padx=4, pady=1).pack()
|
||||||
|
tw.update_idletasks()
|
||||||
|
# Größe + Position zusammen (reine Position ignoriert Tk unter Windows bei rahmenlosen Fenstern)
|
||||||
|
tw.wm_geometry(f"{tw.winfo_reqwidth()}x{tw.winfo_reqheight()}+{x}+{y}")
|
||||||
|
tw.deiconify()
|
||||||
|
|
||||||
def _weg(self, _e=None):
|
def _weg(self, _e=None):
|
||||||
if self._after:
|
if self._after:
|
||||||
@@ -502,19 +582,24 @@ KLASSISCH = False
|
|||||||
|
|
||||||
|
|
||||||
class Dialog(tk.Toplevel):
|
class Dialog(tk.Toplevel):
|
||||||
"""Modaler Dialog im Win95-Stil."""
|
"""Modaler Dialog im Win95-Stil.
|
||||||
|
|
||||||
|
Modal heißt hier: Der INHALT des aufrufenden Fensters ist gesperrt (siehe modal.py), dessen
|
||||||
|
Titelleiste und Rand bleiben bedienbar. Unter Windows gehört der Dialog dem aufrufenden Fenster
|
||||||
|
(bleibt darüber, kommt mit Alt+Tab nach vorn) und hat einen eigenen Taskleisten-Eintrag.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, master, titel: str, resizable: bool = False):
|
def __init__(self, master, titel: str, resizable: bool = False):
|
||||||
super().__init__(master, bg=FACE)
|
super().__init__(master, bg=FACE)
|
||||||
self.withdraw()
|
self.withdraw()
|
||||||
self.title(titel)
|
self.title(titel)
|
||||||
if not KLASSISCH:
|
if not KLASSISCH:
|
||||||
# Rahmenlose Dialoge bekommen ihren Besitzer über win32.Modal – mit transient() würde Tk
|
# Rahmenlose Dialoge bekommen ihren Besitzer über win32 – mit transient() würde Tk sie
|
||||||
# sie unter Windows als Kindfenster anlegen, die dann hinter anderen Programmen hängen.
|
# unter Windows als Kindfenster anlegen, die dann hinter anderen Programmen hängen.
|
||||||
self.transient(master.winfo_toplevel())
|
self.transient(master.winfo_toplevel())
|
||||||
self.resizable(resizable, resizable)
|
self.resizable(resizable, resizable)
|
||||||
self.result = None
|
self.result = None
|
||||||
self._modal = None # Windows: Besitzer + Sperre des aufrufenden Fensters (siehe win32.Modal)
|
self._sperre = None
|
||||||
self.protocol("WM_DELETE_WINDOW", self.abbrechen)
|
self.protocol("WM_DELETE_WINDOW", self.abbrechen)
|
||||||
self.bind("<Escape>", lambda e: self.abbrechen())
|
self.bind("<Escape>", lambda e: self.abbrechen())
|
||||||
try:
|
try:
|
||||||
@@ -530,64 +615,60 @@ class Dialog(tk.Toplevel):
|
|||||||
self.titelleiste.pack(side="top", fill="x")
|
self.titelleiste.pack(side="top", fill="x")
|
||||||
|
|
||||||
def zeigen(self, fokus=None):
|
def zeigen(self, fokus=None):
|
||||||
|
from provtool.gui import modal, win32
|
||||||
if getattr(self._root(), "_provtool_schliesst", False):
|
if getattr(self._root(), "_provtool_schliesst", False):
|
||||||
self.destroy() # Programm wird beendet – keine neuen Dialoge mehr
|
self.destroy() # Programm wird beendet – keine neuen Dialoge mehr
|
||||||
return None
|
return None
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
m = self.master.winfo_toplevel()
|
m = self.master.winfo_toplevel()
|
||||||
|
self._aufrufer = m
|
||||||
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"+{max(0, x)}+{max(0, y)}")
|
self.geometry(f"{w}x{h}+{max(0, x)}+{max(0, y)}") # Größe + Position zusammen (siehe Tooltip)
|
||||||
self.deiconify()
|
self.deiconify()
|
||||||
self.lift()
|
|
||||||
# Windows: Dialog gehört dem aufrufenden Fenster, das solange gesperrt ist → Klick aufs
|
|
||||||
# Hauptfenster, Taskleiste und Alt+Tab holen automatisch den Dialog nach vorn.
|
|
||||||
from provtool.gui.win32 import Modal
|
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
self.focus_force()
|
if KLASSISCH and win32.IS_WIN:
|
||||||
self._modal = Modal(self, m)
|
# eigener Taskleisten-Eintrag; wird erst nach kurzem Aus-/Einblenden wirksam
|
||||||
vorher = self.grab_current() # z. B. der Assistent, wenn hier eine Rückfrage aufgeht
|
if win32.taskleisteneintrag(self):
|
||||||
self._sperre_setzen()
|
self.withdraw()
|
||||||
bind_id = None
|
self.deiconify()
|
||||||
if KLASSISCH:
|
self.update_idletasks()
|
||||||
# Ein rahmenloser Dialog darf nicht hinter dem Hauptfenster verschwinden
|
win32.besitzer_setzen(self, m)
|
||||||
bind_id = m.bind("<FocusIn>", lambda e: self.winfo_exists() and self.lift(), add="+")
|
self._sperre = modal.Sperre(m, self)
|
||||||
|
self.lift()
|
||||||
self.focus_force()
|
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 _sperre_setzen(self, versuch: int = 0):
|
def nach_vorn(self, blinken: bool = False):
|
||||||
"""Tk-Modalsperre setzen. Ist das Fenster noch nicht ganz sichtbar (langsamer PC, Wine),
|
"""Dialog nach vorn holen (z. B. nach Klick auf das gesperrte Hauptfenster)."""
|
||||||
schlägt grab_set fehl – dann kurz danach erneut versuchen statt ohne Sperre weiterzumachen."""
|
from provtool.gui import win32
|
||||||
try:
|
try:
|
||||||
if self.winfo_exists():
|
self.lift()
|
||||||
self.grab_set()
|
self.focus_force()
|
||||||
|
win32.nach_vorn(self)
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
if versuch < 50:
|
return
|
||||||
self.after(20, lambda: self._sperre_setzen(versuch + 1))
|
if blinken:
|
||||||
|
self.bell()
|
||||||
|
win32.blinken(self)
|
||||||
|
if self.titelleiste is not None:
|
||||||
|
self.titelleiste.blinken()
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
# Erst das aufrufende Fenster wieder freigeben, dann schließen – sonst aktiviert Windows
|
sperre = getattr(self, "_sperre", None)
|
||||||
# beim Schließen ein fremdes Programm statt unseres Fensters.
|
if sperre is not None:
|
||||||
modal = getattr(self, "_modal", None)
|
sperre.aufheben()
|
||||||
if modal is not None:
|
self._sperre = None
|
||||||
modal.beenden()
|
aufrufer = getattr(self, "_aufrufer", None)
|
||||||
self._modal = None
|
|
||||||
super().destroy()
|
super().destroy()
|
||||||
|
try: # Fokus zurück an das aufrufende Fenster
|
||||||
|
if aufrufer is not None and aufrufer.winfo_exists():
|
||||||
|
aufrufer.focus_force()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|
||||||
def abbrechen(self):
|
def abbrechen(self):
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ def ausfuehren(ergebnisdatei: str) -> int:
|
|||||||
(seite(0), lambda: wiz()._weiter(), "Willkommen"),
|
(seite(0), lambda: wiz()._weiter(), "Willkommen"),
|
||||||
(seite(1), lambda: wiz()._weiter(), "Speicherort"),
|
(seite(1), lambda: wiz()._weiter(), "Speicherort"),
|
||||||
(seite(2), haendler, "Händler anlegen"),
|
(seite(2), haendler, "Händler anlegen"),
|
||||||
(lambda: len(s.app.db.haendler_liste()) == 1, lambda: wiz()._weiter(), "Händler → weiter"),
|
(lambda: len(s.app.db.haendler_liste()) == 1, lambda: wiz()._weiter(), "Händler -> weiter"),
|
||||||
(seite(3), abrechnung, "Abrechnung mit Datei"),
|
(seite(3), abrechnung, "Abrechnung mit Datei"),
|
||||||
(seite(4), lambda: wiz()._weiter(), "Fertig stellen"),
|
(seite(4), lambda: wiz()._weiter(), "Fertig stellen"),
|
||||||
(lambda: s.dialog(ImportBericht) is not None, import_bericht_zu, "Import-Bericht schließen"),
|
(lambda: s.dialog(ImportBericht) is not None, import_bericht_zu, "Import-Bericht schließen"),
|
||||||
|
|||||||
+278
-20
@@ -61,6 +61,23 @@ class Lauf:
|
|||||||
# eine evtl. geöffnete Fehlermeldung schließen, damit der Test nicht hängt
|
# eine evtl. geöffnete Fehlermeldung schließen, damit der Test nicht hängt
|
||||||
self.root.after(100, self._meldungen_zu)
|
self.root.after(100, self._meldungen_zu)
|
||||||
self.root.report_callback_exception = melden
|
self.root.report_callback_exception = melden
|
||||||
|
self._warten_bis_sichtbar()
|
||||||
|
|
||||||
|
def _warten_bis_sichtbar(self, sekunden: float = 8.0):
|
||||||
|
"""Wie ein Mensch erst bedienen, wenn das Hauptfenster zu sehen ist: Unter Windows wird es beim
|
||||||
|
Start für den Taskleisten-Eintrag kurz aus- und wieder eingeblendet (unter Wine langsamer)."""
|
||||||
|
ende = time.time() + sekunden
|
||||||
|
stabil = 0
|
||||||
|
while time.time() < ende:
|
||||||
|
try:
|
||||||
|
self.root.update()
|
||||||
|
sichtbar = self.root.winfo_viewable() and self.app.inhalt.winfo_height() > 50
|
||||||
|
except tk.TclError:
|
||||||
|
return
|
||||||
|
stabil = stabil + 1 if sichtbar else 0
|
||||||
|
if stabil >= 10: # ~0,1 s durchgehend sichtbar
|
||||||
|
return
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Zustand
|
# ---------------------------------------------------------------- Zustand
|
||||||
def lebt_sonst(self, text: str) -> str:
|
def lebt_sonst(self, text: str) -> str:
|
||||||
@@ -155,6 +172,10 @@ class Lauf:
|
|||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
self.uebrig = [s[2] for s in offen]
|
self.uebrig = [s[2] for s in offen]
|
||||||
|
|
||||||
|
def diagnose(self) -> str:
|
||||||
|
"""Für Fehlermeldungen: welche Schritte nicht liefen + alle gesammelten Fehler."""
|
||||||
|
return f"nicht ausgeführt: {self.uebrig}\n" + "\n".join(self.fehler)
|
||||||
|
|
||||||
def schliessen_ueber_fenster(self):
|
def schliessen_ueber_fenster(self):
|
||||||
"""Wie Klick auf × / Taskleiste ▸ Fenster schließen (WM_DELETE_WINDOW)."""
|
"""Wie Klick auf × / Taskleiste ▸ Fenster schließen (WM_DELETE_WINDOW)."""
|
||||||
cmd = self.root.tk.call("wm", "protocol", self.root._w, "WM_DELETE_WINDOW")
|
cmd = self.root.tk.call("wm", "protocol", self.root._w, "WM_DELETE_WINDOW")
|
||||||
@@ -233,7 +254,7 @@ def test_alt_f4_im_assistent_fragt_und_schliesst_nur_assistent(home):
|
|||||||
(lambda: lauf.dialog(Einrichtungsassistent) is None, lambda: None, "Assistent ist zu"),
|
(lambda: lauf.dialog(Einrichtungsassistent) is None, lambda: None, "Assistent ist zu"),
|
||||||
])
|
])
|
||||||
assert lauf.lebt, lauf.lebt_sonst("Programm darf nicht beendet werden")
|
assert lauf.lebt, lauf.lebt_sonst("Programm darf nicht beendet werden")
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
assert lauf.app.settings.get("assistent_erledigt") is True
|
assert lauf.app.settings.get("assistent_erledigt") is True
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
lauf.ende()
|
lauf.ende()
|
||||||
@@ -342,7 +363,6 @@ def test_alt_f4_bevor_die_sperre_steht(lauf_mit_daten, monkeypatch):
|
|||||||
Zeichnen sind noch nicht fertig. Alt+F4 muss trotzdem nur den Dialog schließen."""
|
Zeichnen sind noch nicht fertig. Alt+F4 muss trotzdem nur den Dialog schließen."""
|
||||||
from provtool.gui import win95 as w
|
from provtool.gui import win95 as w
|
||||||
from provtool.gui.dialogs import InfoDialog
|
from provtool.gui.dialogs import InfoDialog
|
||||||
monkeypatch.setattr(w.Dialog, "_sperre_setzen", lambda self, versuch=0: None) # keine Sperre
|
|
||||||
monkeypatch.setattr(w.Dialog, "winfo_ismapped", lambda self: 0) # „noch nicht gezeichnet“
|
monkeypatch.setattr(w.Dialog, "winfo_ismapped", lambda self: 0) # „noch nicht gezeichnet“
|
||||||
lauf = lauf_mit_daten
|
lauf = lauf_mit_daten
|
||||||
lauf.ablauf([
|
lauf.ablauf([
|
||||||
@@ -353,7 +373,7 @@ def test_alt_f4_bevor_die_sperre_steht(lauf_mit_daten, monkeypatch):
|
|||||||
(lambda: lauf.dialog(InfoDialog) is None, lambda: None, "Dialog geschlossen"),
|
(lambda: lauf.dialog(InfoDialog) is None, lambda: None, "Dialog geschlossen"),
|
||||||
], sekunden=8)
|
], sekunden=8)
|
||||||
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 darf bei offenem Dialog nicht das ganze Programm beenden")
|
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 darf bei offenem Dialog nicht das ganze Programm beenden")
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
@@ -367,7 +387,7 @@ def test_alt_f4_im_rundgang_beendet_nur_den_rundgang(lauf_mit_daten):
|
|||||||
(lambda: rg().blase is None and not rg().rahmen, lambda: None, "Rundgang samt Rahmen weg"),
|
(lambda: rg().blase is None and not rg().rahmen, lambda: None, "Rundgang samt Rahmen weg"),
|
||||||
], sekunden=8)
|
], sekunden=8)
|
||||||
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 im Rundgang darf nicht das Programm beenden")
|
assert lauf.lebt, lauf.lebt_sonst("Alt+F4 im Rundgang darf nicht das Programm beenden")
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
@@ -396,7 +416,7 @@ def test_betrag_dialog_wenn_zelle_unsichtbar(lauf_mit_daten):
|
|||||||
(lambda: bool(app.ptree.get_children()), oeffnen, "Betrag ändern bei unsichtbarer Zelle"),
|
(lambda: bool(app.ptree.get_children()), oeffnen, "Betrag ändern bei unsichtbarer Zelle"),
|
||||||
(lambda: eingabe() is not None, tippen, "Betrag im Dialog eingeben"),
|
(lambda: eingabe() is not None, tippen, "Betrag im Dialog eingeben"),
|
||||||
])
|
])
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
assert any(p["betrag"] == 777 and p["manuell"] for p in app.positionen)
|
assert any(p["betrag"] == 777 and p["manuell"] for p in app.positionen)
|
||||||
del app.ptree.bbox
|
del app.ptree.bbox
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
@@ -533,7 +553,7 @@ def test_windows_popup_kommt_nach_alt_tab_wieder_nach_vorn(lauf_mit_daten):
|
|||||||
fremd.geometry("400x300+50+50")
|
fremd.geometry("400x300+50+50")
|
||||||
fremd.update()
|
fremd.update()
|
||||||
m.update(besitzer=win32.besitzer_von(d), haupt=win32.hwnd(lauf.root),
|
m.update(besitzer=win32.besitzer_von(d), haupt=win32.hwnd(lauf.root),
|
||||||
haupt_bedienbar=win32.ist_bedienbar(lauf.root))
|
haupt_bedienbar=win32.ist_bedienbar(lauf.root), taskleiste=win32.hat_taskleisteneintrag(d))
|
||||||
win32.nach_vorn(fremd) # „woanders hingeklickt“
|
win32.nach_vorn(fremd) # „woanders hingeklickt“
|
||||||
lauf.root.update()
|
lauf.root.update()
|
||||||
m["woanders"] = win32.stapel(d, lauf.root, fremd)
|
m["woanders"] = win32.stapel(d, lauf.root, fremd)
|
||||||
@@ -548,34 +568,38 @@ def test_windows_popup_kommt_nach_alt_tab_wieder_nach_vorn(lauf_mit_daten):
|
|||||||
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, messen), "messen"),
|
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, messen), "messen"),
|
||||||
(lambda: "zurueck" in m and lauf.dialog(InfoDialog) is None, lambda: None, "geschlossen"),
|
(lambda: "zurueck" in m and lauf.dialog(InfoDialog) is None, lambda: None, "geschlossen"),
|
||||||
], sekunden=10)
|
], sekunden=10)
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
assert m["besitzer"] == m["haupt"], "Popup gehört nicht dem Hauptfenster"
|
assert m["besitzer"] == m["haupt"], "Popup gehört nicht dem Hauptfenster"
|
||||||
assert m["haupt_bedienbar"] is False, "Hauptfenster muss gesperrt sein, solange das Popup offen ist"
|
assert m["haupt_bedienbar"] is True, "Hauptfenster soll verschiebbar bleiben (nur der Inhalt ist gesperrt)"
|
||||||
|
assert m["taskleiste"] is True, "Popup braucht einen eigenen Eintrag in der Taskleiste"
|
||||||
assert m["woanders"][0] == 2, f"Testaufbau: das fremde Fenster sollte oben liegen ({m['woanders']})"
|
assert m["woanders"][0] == 2, f"Testaufbau: das fremde Fenster sollte oben liegen ({m['woanders']})"
|
||||||
assert m["zurueck"][:2] == [0, 1], f"Popup liegt nach dem Zurückholen nicht über dem Hauptfenster: {m['zurueck']}"
|
assert m["zurueck"][:2] == [0, 1], f"Popup liegt nach dem Zurückholen nicht über dem Hauptfenster: {m['zurueck']}"
|
||||||
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
|
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
@nur_windows
|
def test_verschachtelte_rueckfrage_sperrt_der_reihe_nach(lauf_mit_daten):
|
||||||
def test_windows_verschachtelte_rueckfrage(lauf_mit_daten):
|
"""Händler-Dialog -> Rückfrage: gesperrt sind Hauptfenster-Inhalt und Händler-Dialog (nicht dessen
|
||||||
"""Händler-Dialog → Rückfrage: Rückfrage gehört dem Händler-Dialog; beim Schließen der Reihe nach frei."""
|
Titelleiste); nach der Rückfrage ist der Händler-Dialog wieder frei, das Hauptfenster noch nicht."""
|
||||||
from provtool.gui import win32
|
from provtool.gui import modal, win32
|
||||||
from provtool.gui import win95 as w
|
from provtool.gui import win95 as w
|
||||||
from provtool.gui.dialogs import HaendlerDialog
|
from provtool.gui.dialogs import HaendlerDialog
|
||||||
lauf = lauf_mit_daten
|
lauf = lauf_mit_daten
|
||||||
m = {}
|
m = {}
|
||||||
rueckfrage = lambda: next((d for d in lauf.dialoge() if d.title() == "Test-Rückfrage"), None) # noqa: E731
|
rueckfrage = lambda: next((d for d in lauf.dialoge() if d.title() == "Test-Rückfrage"), None) # noqa: E731
|
||||||
|
gesperrt = lambda wdg: modal.TAG in wdg.bindtags() # noqa: E731
|
||||||
|
|
||||||
def messen_innen():
|
def messen_innen():
|
||||||
h, r = lauf.dialog(HaendlerDialog), rueckfrage()
|
h, r = lauf.dialog(HaendlerDialog), rueckfrage()
|
||||||
m.update(r_besitzer=win32.besitzer_von(r), h=win32.hwnd(h), h_an=win32.ist_bedienbar(h),
|
m.update(h_inhalt=gesperrt(h.liste), h_titel=gesperrt(h.titelleiste) if h.titelleiste else False,
|
||||||
haupt_an=win32.ist_bedienbar(lauf.root))
|
haupt_inhalt=gesperrt(lauf.app.ptree), haupt_titel=gesperrt(lauf.app.rahmen.titelleiste)
|
||||||
|
if lauf.app.rahmen.titelleiste else False,
|
||||||
|
r_besitzer=win32.besitzer_von(r), h=win32.hwnd(h))
|
||||||
r.destroy()
|
r.destroy()
|
||||||
|
|
||||||
def messen_aussen():
|
def messen_aussen():
|
||||||
h = lauf.dialog(HaendlerDialog)
|
h = lauf.dialog(HaendlerDialog)
|
||||||
m.update(h_an_danach=win32.ist_bedienbar(h), haupt_an_danach=win32.ist_bedienbar(lauf.root))
|
m.update(h_inhalt_danach=gesperrt(h.liste), haupt_inhalt_danach=gesperrt(lauf.app.ptree))
|
||||||
h.abbrechen()
|
h.abbrechen()
|
||||||
|
|
||||||
lauf.ablauf([
|
lauf.ablauf([
|
||||||
@@ -586,11 +610,13 @@ def test_windows_verschachtelte_rueckfrage(lauf_mit_daten):
|
|||||||
(lambda: "h" in m and rueckfrage() is None, messen_aussen, "außen messen"),
|
(lambda: "h" in m and rueckfrage() is None, messen_aussen, "außen messen"),
|
||||||
(lambda: lauf.dialog(HaendlerDialog) is None, lambda: None, "alles zu"),
|
(lambda: lauf.dialog(HaendlerDialog) is None, lambda: None, "alles zu"),
|
||||||
], sekunden=12)
|
], sekunden=12)
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
assert m["h_inhalt"] and m["haupt_inhalt"], "Inhalte müssen gesperrt sein, solange die Rückfrage offen ist"
|
||||||
|
assert not m["h_titel"] and not m["haupt_titel"], "Titelleisten (verschieben) dürfen nicht gesperrt sein"
|
||||||
|
assert not m["h_inhalt_danach"] and m["haupt_inhalt_danach"]
|
||||||
|
assert not gesperrt(lauf.app.ptree), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
assert m["r_besitzer"] == m["h"], "Rückfrage gehört nicht dem Händler-Dialog"
|
assert m["r_besitzer"] == m["h"], "Rückfrage gehört nicht dem Händler-Dialog"
|
||||||
assert m["h_an"] is False and m["haupt_an"] is False
|
|
||||||
assert m["h_an_danach"] is True and m["haupt_an_danach"] is False
|
|
||||||
assert win32.ist_bedienbar(lauf.root)
|
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
@@ -612,8 +638,240 @@ def test_windows_rundgang_sperrt_und_gibt_frei(lauf_mit_daten):
|
|||||||
messen_und_weiter, f"Schritt {i + 1}"))
|
messen_und_weiter, f"Schritt {i + 1}"))
|
||||||
schritte.append((IMMER, lambda: rg().beenden(), "Rundgang beenden"))
|
schritte.append((IMMER, lambda: rg().beenden(), "Rundgang beenden"))
|
||||||
lauf.ablauf(schritte, sekunden=12)
|
lauf.ablauf(schritte, sekunden=12)
|
||||||
assert not lauf.uebrig, lauf.uebrig
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
assert all(m["gesperrt"]), "Hauptfenster während des Rundgangs nicht gesperrt"
|
assert all(m["gesperrt"]), "Hauptfenster während des Rundgangs nicht gesperrt"
|
||||||
assert all(m["besitzer"]), "Sprechblase gehört nicht dem Hauptfenster"
|
assert all(m["besitzer"]), "Sprechblase gehört nicht dem Hauptfenster"
|
||||||
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Rundgang nicht freigegeben"
|
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Rundgang nicht freigegeben"
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================== Rückmeldungen vom 2.11. (Teil 2)
|
||||||
|
|
||||||
|
def test_gedrueckte_knoepfe_behalten_ihre_schrift(lauf_mit_daten):
|
||||||
|
"""Gemeldet: Im gedrückten Zustand war die Beschriftung weg (activeforeground = Grau)."""
|
||||||
|
from provtool.gui import win95 as w
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
for b in (w.knopf(lauf.root, "Test"), tk.Button(lauf.root, text="roh")):
|
||||||
|
assert b.cget("activeforeground") != b.cget("activebackground"), "Schrift gedrückter Knöpfe unsichtbar"
|
||||||
|
b.destroy()
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_tipp_text_bleibt_im_sichtbaren_bereich(lauf_mit_daten):
|
||||||
|
from provtool.gui.assistent import TIPPS, TippDesTages
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
m = {}
|
||||||
|
|
||||||
|
def messen():
|
||||||
|
d = lauf.dialog(TippDesTages)
|
||||||
|
rahmen = d.text.master
|
||||||
|
for t in TIPPS: # jeden Tipp einmal anzeigen
|
||||||
|
d.text.configure(text=t)
|
||||||
|
d.update_idletasks()
|
||||||
|
m.setdefault("breiten", []).append((d.text.winfo_reqwidth(), rahmen.winfo_width()))
|
||||||
|
m.setdefault("hoehen", []).append((d.text.winfo_reqheight(), rahmen.winfo_height()))
|
||||||
|
d.abbrechen()
|
||||||
|
|
||||||
|
lauf.ablauf([
|
||||||
|
(IMMER, lauf.app.tipp_des_tages, "Tipp öffnen"),
|
||||||
|
(lambda: lauf.dialog(TippDesTages) is not None, lambda: lauf.root.after(100, messen), "messen"),
|
||||||
|
(lambda: "breiten" in m and lauf.dialog(TippDesTages) is None, lambda: None, "zu"),
|
||||||
|
])
|
||||||
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
for text_b, rahmen_b in m["breiten"]:
|
||||||
|
assert text_b <= rahmen_b, f"Tipp-Text ({text_b}px) breiter als sein Bereich ({rahmen_b}px)"
|
||||||
|
for text_h, rahmen_h in m["hoehen"]:
|
||||||
|
assert text_h <= rahmen_h, f"Tipp-Text ({text_h}px) höher als sein Bereich ({rahmen_h}px)"
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_strg_ruecktaste_loescht_ganzes_wort(lauf_mit_daten):
|
||||||
|
from provtool.gui import win95 as w
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
e = tk.Entry(lauf.root)
|
||||||
|
e.insert(0, "Mobilfunk Schmid GmbH")
|
||||||
|
ev = type("E", (), {"widget": e})()
|
||||||
|
e.icursor("end")
|
||||||
|
w._wort_weg(ev, -1)
|
||||||
|
assert e.get() == "Mobilfunk Schmid "
|
||||||
|
w._wort_weg(ev, -1)
|
||||||
|
assert e.get() == "Mobilfunk "
|
||||||
|
e.icursor(0)
|
||||||
|
w._wort_weg(ev, 1)
|
||||||
|
assert e.get() == ""
|
||||||
|
# Bindung ist für alle Eingabefelder aktiv
|
||||||
|
for klasse in ("Entry", "TEntry", "TCombobox"):
|
||||||
|
assert lauf.root.bind_class(klasse, "<Control-BackSpace>"), klasse
|
||||||
|
assert lauf.root.bind_class(klasse, "<Control-Delete>"), klasse
|
||||||
|
e.destroy()
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_menue_wechselt_beim_ueberfahren(lauf_mit_daten):
|
||||||
|
"""Gemeldet: Bei offenem Menü wechselte es nicht mit, wenn man zum nächsten Eintrag fuhr."""
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
leiste = lauf.app.menueleiste
|
||||||
|
datei, haendler = leiste.knoepfe["Datei"], leiste.knoepfe["Händler"]
|
||||||
|
m = {}
|
||||||
|
|
||||||
|
def ueberfahren():
|
||||||
|
popup = leiste.menues[datei]
|
||||||
|
ev = type("E", (), {"x_root": haendler.winfo_rootx() + 3, "y_root": haendler.winfo_rooty() + 3})()
|
||||||
|
popup._bewegung(ev)
|
||||||
|
|
||||||
|
def pruefen_und_esc():
|
||||||
|
m["offen"] = leiste.offener_knopf
|
||||||
|
m["popup_offen"] = leiste.menues[haendler].offen
|
||||||
|
leiste.menues[haendler].fenster.event_generate("<Escape>")
|
||||||
|
leiste.menues[haendler].schliessen()
|
||||||
|
|
||||||
|
lauf.ablauf([
|
||||||
|
(IMMER, lambda: leiste._klick(datei), "Datei öffnen"),
|
||||||
|
(lambda: leiste.menues[datei].offen, ueberfahren, "zu „Händler“ fahren"),
|
||||||
|
(lambda: leiste.menues[haendler].offen, pruefen_und_esc, "prüfen + schließen"),
|
||||||
|
(lambda: leiste.offener_knopf is None, lambda: None, "alles zu"),
|
||||||
|
])
|
||||||
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
assert m["offen"] is haendler and m["popup_offen"]
|
||||||
|
assert not leiste.menues[datei].offen
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_menue_geht_unter_seinem_menuepunkt_auf(lauf_mit_daten):
|
||||||
|
"""Unter Windows landete das Menü zunächst oben links in der Bildschirmecke."""
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
leiste = lauf.app.menueleiste
|
||||||
|
m = {}
|
||||||
|
for name in ("Datei", "Händler"):
|
||||||
|
knopf = leiste.knoepfe[name]
|
||||||
|
|
||||||
|
def messen(knopf=knopf, name=name):
|
||||||
|
f = leiste.menues[knopf].fenster
|
||||||
|
m[name] = (f.winfo_rootx() - knopf.winfo_rootx(),
|
||||||
|
f.winfo_rooty() - (knopf.winfo_rooty() + knopf.winfo_height()))
|
||||||
|
leiste.schliessen()
|
||||||
|
lauf.ablauf([(IMMER, lambda knopf=knopf: leiste._klick(knopf), f"{name} öffnen"),
|
||||||
|
(lambda knopf=knopf: leiste.menues[knopf].offen, lambda: lauf.root.after(100, messen), "messen"),
|
||||||
|
(lambda name=name: name in m, lambda: None, "fertig")])
|
||||||
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
for name, (dx, dy) in m.items():
|
||||||
|
assert abs(dx) <= 3 and abs(dy) <= 3, f"Menü „{name}“ nicht unter seinem Menüpunkt (Abstand {dx}, {dy})"
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_menue_befehl_wird_ausgefuehrt(lauf_mit_daten):
|
||||||
|
from provtool.gui.assistent import TippDesTages
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
leiste = lauf.app.menueleiste
|
||||||
|
hilfe = leiste.knoepfe["?"]
|
||||||
|
|
||||||
|
def tipp_waehlen():
|
||||||
|
popup = leiste.menues[hilfe]
|
||||||
|
i = next(i for i, (e, _, _) in enumerate(popup.zeilen) if e["label"].startswith("Tipp des Tages"))
|
||||||
|
popup._ausfuehren(i)
|
||||||
|
|
||||||
|
lauf.ablauf([
|
||||||
|
(IMMER, lambda: leiste._klick(hilfe), "? öffnen"),
|
||||||
|
(lambda: leiste.menues[hilfe].offen, tipp_waehlen, "Tipp des Tages wählen"),
|
||||||
|
(lambda: lauf.dialog(TippDesTages) is not None, lambda: lauf.dialog(TippDesTages).abbrechen(), "zu"),
|
||||||
|
])
|
||||||
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
assert not leiste.menues[hilfe].offen
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_hauptfenster_bei_offenem_dialog_verschiebbar_inhalt_gesperrt(lauf_mit_daten, monkeypatch):
|
||||||
|
"""Gemeldet: Bei offenem Dialog ließ sich das Hauptfenster nicht verschieben."""
|
||||||
|
from provtool.gui import modal, rahmen
|
||||||
|
from provtool.gui.dialogs import InfoDialog
|
||||||
|
monkeypatch.setattr(rahmen, "NATIV_VERSCHIEBEN", False) # Tk-Weg prüfen (nativ macht Windows selbst)
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
app = lauf.app
|
||||||
|
if not app.rahmen.klassisch:
|
||||||
|
pytest.skip("nur im Win95-Rahmen")
|
||||||
|
m = {"neu_aufgerufen": False}
|
||||||
|
monkeypatch.setattr(type(app), "neu", lambda self: m.__setitem__("neu_aufgerufen", True))
|
||||||
|
titel = app.rahmen.titelleiste.titel_lbl
|
||||||
|
|
||||||
|
def pruefen():
|
||||||
|
x0 = lauf.root.winfo_x()
|
||||||
|
rx, ry = titel.winfo_rootx() + 30, titel.winfo_rooty() + 5
|
||||||
|
titel.event_generate("<ButtonPress-1>", x=30, y=5, rootx=rx, rooty=ry)
|
||||||
|
titel.event_generate("<B1-Motion>", x=70, y=5, rootx=rx + 40, rooty=ry)
|
||||||
|
titel.event_generate("<ButtonRelease-1>", x=70, y=5, rootx=rx + 40, rooty=ry)
|
||||||
|
lauf.root.update()
|
||||||
|
m["verschoben"] = lauf.root.winfo_x() - x0
|
||||||
|
knopf = app.tb["Neu"] # Klick auf den gesperrten Inhalt
|
||||||
|
knopf.event_generate("<ButtonPress-1>", x=5, y=5)
|
||||||
|
knopf.event_generate("<ButtonRelease-1>", x=5, y=5)
|
||||||
|
m["gesperrt"] = modal.TAG in app.ptree.bindtags()
|
||||||
|
lauf.dialog(InfoDialog).abbrechen()
|
||||||
|
|
||||||
|
lauf.ablauf([
|
||||||
|
(IMMER, lambda: InfoDialog(lauf.root).zeigen(), "Dialog öffnen"),
|
||||||
|
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(100, pruefen), "prüfen"),
|
||||||
|
(lambda: "verschoben" in m and lauf.dialog(InfoDialog) is None, lambda: None, "zu"),
|
||||||
|
])
|
||||||
|
assert not lauf.uebrig, f"{lauf.uebrig} {m}\n" + "\n".join(lauf.fehler)
|
||||||
|
assert m["verschoben"] == 40, f"Hauptfenster ließ sich nicht verschieben ({m['verschoben']} px)"
|
||||||
|
assert m["gesperrt"] and not m["neu_aufgerufen"], "Inhalt des Hauptfensters war nicht gesperrt"
|
||||||
|
assert modal.TAG not in app.ptree.bindtags(), "Sperre nach dem Schließen nicht aufgehoben"
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rand_zeiger_wird_zurueckgesetzt(lauf_mit_daten):
|
||||||
|
"""Gemeldet: Der Größenänderungs-Zeiger blieb manchmal stehen."""
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
r = lauf.app.rahmen
|
||||||
|
if not r.klassisch:
|
||||||
|
pytest.skip("nur im Win95-Rahmen")
|
||||||
|
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2})()
|
||||||
|
r._rand_cursor(ev)
|
||||||
|
assert r.aussen.cget("cursor"), "am Rand muss der Größenänderungs-Zeiger erscheinen"
|
||||||
|
r.inhalt.event_generate("<Enter>")
|
||||||
|
lauf.root.update()
|
||||||
|
assert r.aussen.cget("cursor") == "", "Zeiger wurde beim Betreten des Inhalts nicht zurückgesetzt"
|
||||||
|
# Ecken: in der Nähe des Ecks diagonal
|
||||||
|
assert r._welcher_rand(2, 10) == "nw" and r._welcher_rand(r.aussen.winfo_width() - 3, 10) == "ne"
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigene_kennungen_ausblenden_in_der_oberflaeche(lauf_mit_daten):
|
||||||
|
from provtool.db import EIGEN
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
app = lauf.app
|
||||||
|
alle_vorher = int(app.htree.item("-1", "values")[0])
|
||||||
|
app.db.eigene_setzen("freenet", "999")
|
||||||
|
app._aktualisieren()
|
||||||
|
assert app.htree.exists(str(EIGEN)), "Eintrag „(Eigene – ausgeblendet)“ fehlt"
|
||||||
|
assert int(app.htree.item("-1", "values")[0]) == alle_vorher - 1, "eigene Positionen zählen noch mit"
|
||||||
|
assert all(p["haendler_id"] != EIGEN for p in app._gefiltert())
|
||||||
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
@nur_windows
|
||||||
|
def test_windows_minimieren_mit_offenem_dialog(lauf_mit_daten):
|
||||||
|
"""Minimieren bei offenem Dialog darf den Dialog nicht zerstören; nach dem Wiederherstellen ist er da."""
|
||||||
|
from provtool.gui import win32
|
||||||
|
from provtool.gui.dialogs import InfoDialog
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
m = {}
|
||||||
|
|
||||||
|
def minimieren():
|
||||||
|
lauf.app.rahmen.minimieren()
|
||||||
|
lauf.root.update()
|
||||||
|
m["minimiert"] = win32.ist_minimiert(lauf.root)
|
||||||
|
win32.user32().ShowWindow(win32.hwnd(lauf.root), 9) # SW_RESTORE (wie Klick in der Taskleiste)
|
||||||
|
lauf.root.update()
|
||||||
|
m["dialog_da"] = lauf.dialog(InfoDialog) is not None
|
||||||
|
lauf.dialog(InfoDialog).abbrechen()
|
||||||
|
|
||||||
|
lauf.ablauf([
|
||||||
|
(IMMER, lambda: InfoDialog(lauf.root).zeigen(), "Dialog öffnen"),
|
||||||
|
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, minimieren), "minimieren"),
|
||||||
|
(lambda: "dialog_da" in m, lambda: None, "fertig"),
|
||||||
|
], sekunden=12)
|
||||||
|
assert not lauf.uebrig, lauf.diagnose()
|
||||||
|
assert m["minimiert"], "Hauptfenster wurde nicht minimiert"
|
||||||
|
assert m["dialog_da"], "Dialog nach dem Minimieren verschwunden"
|
||||||
|
lauf.pruefen()
|
||||||
@@ -265,3 +265,19 @@ def test_migration_alter_datenbank(tmp_path):
|
|||||||
spalten = {r[1] for r in d.con.execute("PRAGMA table_info(position)")}
|
spalten = {r[1] for r in d.con.execute("PRAGMA table_info(position)")}
|
||||||
assert "haendler_fest" in spalten
|
assert "haendler_fest" in spalten
|
||||||
d.close()
|
d.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigene_kennungen_ausblenden(db, freenet_csv):
|
||||||
|
from provtool.db import EIGEN
|
||||||
|
hid = db.haendler_speichern(Haendler(0, "Händler A"), [("freenet", "111")])
|
||||||
|
aid = db.abrechnung_neu("X")
|
||||||
|
db.importieren(aid, lies_datei(freenet_csv))
|
||||||
|
assert {p["haendler_id"] for p in db.positionen(aid)} == {hid, UNZUGEORDNET}
|
||||||
|
db.eigene_setzen("freenet", "999")
|
||||||
|
ids = {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}
|
||||||
|
assert ids["999"] == EIGEN and ids["111"] == hid
|
||||||
|
# Händler-Kennung hat Vorrang vor „eigen“
|
||||||
|
db.eigene_setzen("freenet", "111")
|
||||||
|
assert {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}["111"] == hid
|
||||||
|
db.eigene_setzen("freenet", "999", False)
|
||||||
|
assert {p["kennung"]: p["haendler_id"] for p in db.positionen(aid)}["999"] == UNZUGEORDNET
|
||||||
Reference in new issue
Block a user