Airtime je Händler, Jahresbonus-Staffel, Export überarbeitet, natives Windows-Fenster
Windows-Build / windows-exe (push) Successful in 5m7s

- Airtime: Auszahlung in % je Händler (0 % = keine). Im Export als eine Summenzeile,
  die Einzelposten in einer eigenen Airtime-Liste (zweite Excel-Datei).
- Jahresbonus Neuaktivierung: Betrag je Karte nach Staffel (Standard 1-9: 5 € ... ab 40: voll),
  Standard-Staffel änderbar, eigene Staffel je Händler möglich.
- Provisionsarten: Airtime/Jahresbonus werden erkannt, Behandlung je Provisionsart umstellbar.
- Export: freenet und Vodafone standardmäßig in einer Datei, Positionen mit 0,00 € weglassen.
- Händler-Dialog: GROSSBUCHSTABEN-Regel als Häkchen (ging verloren, wenn „Hinzufügen“ vergessen wurde).
- Windows: Hauptfenster ist ein echtes Programmfenster mit unsichtbarem Rahmen – Andocken am
  Bildschirmrand, Win+Pfeiltasten und natives Größe-Ziehen; Trennsteg baut erst beim Loslassen neu auf.
- DB-Schema v3 (Migration), Tests für alles Neue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kerim AtilganandClaude Opus 5.5 committed 2026-11-02 17:23:48 +01:00
1 parent 20256d266c
commit 966379293a
14 files changed
+1399 -119

No files matched your search

+96 -6
View File
@@ -3,8 +3,11 @@
Windows erlaubt Tk nicht, die echte Titelleiste umzufärben. Deshalb wird sie ausgeblendet
(overrideredirect) und hier nachgebaut: blaue Titelleiste mit Icon und _ □ x, Verschieben,
Doppelklick = Maximieren, Größenänderung über den Rand und den Anfasser unten rechts,
graue Menüleiste. Unter Windows sorgen ein paar Win32-Aufrufe dafür, dass das Fenster
trotzdem in der Taskleiste erscheint, per Alt+Tab erreichbar ist und minimiert werden kann.
graue Menüleiste.
Unter Windows ist das Hauptfenster trotzdem ein echtes Programmfenster (siehe nativ.py): Windows
übernimmt Verschieben, Größe ändern, Andocken am Bildschirmrand, Win+Pfeiltasten, Taskleiste und
Alt+Tab. Auf anderen Systemen (Entwicklung/Tests) macht Tk das selbst.
"""
from __future__ import annotations
@@ -210,6 +213,7 @@ class Titelleiste(tk.Frame):
NATIV_VERSCHIEBEN = True # Tests können das abschalten, um den Tk-Weg zu prüfen
NATIVER_RAHMEN = True # Windows: echtes Fenster mit unsichtbarem Rahmen (nativ.py)
# --------------------------------------------------------------------------- Hauptfenster
@@ -243,7 +247,8 @@ class Hauptrahmen:
schliessen=schliessen, minimieren=self.minimieren,
maximieren=self.umschalten)
self.titelleiste.pack(fill="x")
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert
# Maximiert darf man nur nativ ziehen (Windows stellt das Fenster dabei wieder her)
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert or self.nativ_an
self.inhalt = tk.Frame(self.aussen, bg=w.FACE)
self.inhalt.pack(fill="both", expand=True)
# Gesperrt wird bei offenem Dialog nur der Inhalt – Titelleiste und Rand bleiben bedienbar
@@ -260,7 +265,66 @@ class Hauptrahmen:
self.aussen.bind("<ButtonPress-1>", self._resize_start)
self.aussen.bind("<B1-Motion>", self._resize)
root.bind_all("<Alt-F4>", lambda e: self.alt_f4(e.widget))
root.after(50, lambda: taskleiste_anzeigen(root))
self.nativ = None
if IS_WIN and NATIVER_RAHMEN:
from provtool.gui.nativ import NativerRahmen
self.nativ = NativerRahmen(root, self.RAND, self.ECKE)
root.bind("<Configure>", self._fenster_geaendert, add="+")
root.bind("<Map>", lambda e: e.widget is root and self._nativ_einrichten(), add="+")
self._nativ_einrichten() # möglichst schon vor dem ersten Anzeigen
root.after(50, self._fenster_einrichten)
# ---- natives Windows-Fenster
@property
def nativ_an(self) -> bool:
return self.nativ is not None and self.nativ.aktiv
def _nativ_einrichten(self) -> bool:
if self.nativ is None:
return False
if self.nativ.aktiv and not self.nativ.veraltet():
return True
try:
sichtbar = bool(self.root.winfo_viewable())
except tk.TclError:
sichtbar = True
if self.nativ.veraltet(): # Tk hat das äußere Fenster neu angelegt
ok = self.nativ.neu_einrichten([self.root, self.aussen])
else:
ok = self.nativ.einrichten([self.root, self.aussen])
if ok:
# Erst eingerichtet, als das Fenster schon zu sehen war -> Taskleiste muss es neu erfassen
self._taskleiste_neu = sichtbar
return ok
_taskleiste_neu = True
def _fenster_einrichten(self):
self._nativ_einrichten()
if self.nativ_an and not self._taskleiste_neu:
return # vor dem ersten Anzeigen eingerichtet: Windows kennt den Taskleisten-Stil schon
taskleiste_anzeigen(self.root)
def _fenster_geaendert(self, e):
"""Zustand nachführen – maximieren/wiederherstellen geht jetzt auch über Windows selbst
(Win+Hoch, an den oberen Rand ziehen, Doppelklick, Taskleiste)."""
if e.widget is not self.root or self.nativ is None:
return
if self.nativ.veraltet():
self._nativ_einrichten()
if not self.nativ_an:
return
try:
maximiert = self.nativ.maximiert()
if maximiert != self.maximiert:
self.maximiert = maximiert
self.titelleiste.max_symbol(maximiert)
self._cursor_zurueck()
if not maximiert and self.root.wm_state() == "normal":
self._normal_geo = self.root.geometry()
except Exception:
pass
def alt_f4(self, widget) -> str:
"""Alt+F4 wie unter Windows: schließt nur das Fenster, in dem man gerade ist."""
@@ -323,10 +387,16 @@ class Hauptrahmen:
return None
self._griff_bild = griff_bild(master)
g = tk.Label(master, image=self._griff_bild, bg=w.FACE, cursor="sizing" if not IS_WIN else "size_nw_se")
g.bind("<ButtonPress-1>", lambda e: self._resize_start(e, "se"))
g.bind("<ButtonPress-1>", self._griff_start)
g.bind("<B1-Motion>", self._resize)
return g
def _griff_start(self, e):
if self.nativ_an and not self.maximiert:
self.nativ.groesse_ziehen("se") # Windows zieht die Größe (flüssig)
return "break"
self._resize_start(e, "se")
def geometrie_setzen(self, geo: str | None, maximiert: bool = False):
self.root.update_idletasks()
if not geo or "+" not in geo:
@@ -340,6 +410,8 @@ class Hauptrahmen:
breite, hoehe = min(breite, bw), min(hoehe, bh)
geo = f"{breite}x{hoehe}+{x0 + (bw - breite) // 2}+{y0 + (bh - hoehe) // 2}"
self.root.geometry(geo)
self._normal_geo = geo
self._nativ_einrichten()
if maximiert and self.klassisch:
self.root.after(100, self.maximieren)
@@ -371,6 +443,13 @@ class Hauptrahmen:
def maximieren(self):
if self.maximiert:
return
if self.nativ_an:
from provtool.gui.nativ import SC_MAXIMIZE
self._normal_geo = self.root.geometry()
self.nativ.befehl(SC_MAXIMIZE)
self.maximiert = self.nativ.maximiert()
self.titelleiste.max_symbol(self.maximiert)
return
self._normal_geo = self.root.geometry()
x, y, bw, bh = arbeitsbereich(self.root)
self.root.geometry(f"{bw}x{bh}+{x}+{y}")
@@ -380,6 +459,12 @@ class Hauptrahmen:
def wiederherstellen(self):
if not self.maximiert:
return
if self.nativ_an:
from provtool.gui.nativ import SC_RESTORE
self.nativ.befehl(SC_RESTORE)
self.maximiert = self.nativ.maximiert()
self.titelleiste.max_symbol(self.maximiert)
return
self.maximiert = False
if self._normal_geo:
self.root.geometry(self._normal_geo)
@@ -417,7 +502,7 @@ class Hauptrahmen:
"sw": "bottom_left_corner"}
def _rand_cursor(self, e):
if self.maximiert:
if self.maximiert or self.nativ_an: # nativ: Windows zeigt den Zeiger selbst
return
rand = self._welcher_rand(e.x, e.y)
zeiger = (self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, "")
@@ -429,6 +514,11 @@ class Hauptrahmen:
def _resize_start(self, e, rand: str | None = None):
self._rand = rand or self._welcher_rand(e.x, e.y)
if self._rand and self.nativ_an and not self.maximiert:
# sollte nicht vorkommen (der Rand ist für Windows durchlässig) – dann eben so
self.nativ.groesse_ziehen(self._rand)
self._rand = ""
return "break"
self._start = (e.x_root, e.y_root, self.root.winfo_x(), self.root.winfo_y(),
self.root.winfo_width(), self.root.winfo_height())