Airtime je Händler, Jahresbonus-Staffel, Export überarbeitet, natives Windows-Fenster
Windows-Build / windows-exe (push) Successful in 5m7s
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:
1 parent
20256d266c
commit
966379293a
14 files changed
+1399
-119
No files matched your search
+174
-3
@@ -70,14 +70,19 @@ class Lauf:
|
||||
denn ein modaler Dialog (z. B. der Assistent beim Erststart) würde die festhalten."""
|
||||
try:
|
||||
sichtbar = bool(self.root.winfo_viewable()) and self.app.inhalt.winfo_height() > 50
|
||||
lage = (self.root.winfo_rootx(), self.root.winfo_rooty(), self.root.winfo_width(),
|
||||
self.root.winfo_height())
|
||||
except tk.TclError:
|
||||
return True
|
||||
if not sichtbar:
|
||||
# erst wenn Lage und Größe einen Moment stabil sind (beim ersten Anzeigen gibt es unter Wine
|
||||
# kurz einen Zwischenstand)
|
||||
if not sichtbar or lage != getattr(self, "_lage", None):
|
||||
self._lage = lage
|
||||
self._sichtbar_seit = None
|
||||
return False
|
||||
if self._sichtbar_seit is None:
|
||||
self._sichtbar_seit = time.time()
|
||||
return time.time() - self._sichtbar_seit >= 0.1
|
||||
return time.time() - self._sichtbar_seit >= 0.15
|
||||
|
||||
# ---------------------------------------------------------------- Zustand
|
||||
def lebt_sonst(self, text: str) -> str:
|
||||
@@ -311,11 +316,14 @@ def _dialog_oeffner():
|
||||
"prozent": lambda a: (a.ptree.selection_set(a.ptree.get_children()[0]), a.prozent()),
|
||||
"neu_berechnen": lambda a: (a.db.betrag_setzen(a.positionen[0]["id"], 1), a._aktualisieren(),
|
||||
a.neu_berechnen()),
|
||||
"provisionsarten": lambda a: a.provisionsarten(),
|
||||
"staffel": lambda a: a.standard_staffel(),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["haendler", "abrechnungen", "export", "einstellungen", "neu", "info", "tipp",
|
||||
"dateien", "hilfe", "assistent", "prozent", "neu_berechnen"])
|
||||
"dateien", "hilfe", "assistent", "prozent", "neu_berechnen", "provisionsarten",
|
||||
"staffel"])
|
||||
def test_schliessen_waehrend_dialog_offen(lauf_mit_daten, name):
|
||||
lauf = lauf_mit_daten
|
||||
oeffnen = _dialog_oeffner()[name]
|
||||
@@ -825,8 +833,16 @@ def test_rand_zeiger_wird_zurueckgesetzt(lauf_mit_daten):
|
||||
r = lauf.app.rahmen
|
||||
if not r.klassisch:
|
||||
pytest.skip("nur im Win95-Rahmen")
|
||||
lauf.ablauf([]) # erst wenn das Hauptfenster zu sehen ist (Größen stimmen dann)
|
||||
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2})()
|
||||
r._rand_cursor(ev)
|
||||
if r.nativ_an:
|
||||
# Windows: den Zeiger setzt Windows selbst (WM_SETCURSOR am äußeren Fenster, siehe
|
||||
# test_windows_echtes_fenster_andocken_und_rand) – Tk darf keinen eigenen setzen, der hängen bleibt.
|
||||
assert r.aussen.cget("cursor") == "", "Tk-Zeiger trotz nativem Rahmen gesetzt"
|
||||
assert r._welcher_rand(2, 10) == "nw" and r._welcher_rand(r.aussen.winfo_width() - 3, 10) == "ne"
|
||||
lauf.pruefen()
|
||||
return
|
||||
assert r.aussen.cget("cursor"), "am Rand muss der Größenänderungs-Zeiger erscheinen"
|
||||
r.inhalt.event_generate("<Enter>")
|
||||
lauf.root.update()
|
||||
@@ -875,3 +891,158 @@ def test_windows_minimieren_mit_offenem_dialog(lauf_mit_daten):
|
||||
assert m["minimiert"], "Hauptfenster wurde nicht minimiert"
|
||||
assert m["dialog_da"], "Dialog nach dem Minimieren verschwunden"
|
||||
lauf.pruefen()
|
||||
|
||||
|
||||
@nur_windows
|
||||
def test_windows_echtes_fenster_andocken_und_rand(lauf_mit_daten):
|
||||
"""Gemeldet: Win+Hoch/Runter und Andocken am Bildschirmrand gingen nicht, Größe ziehen war zäh.
|
||||
Das Hauptfenster muss für Windows ein echtes Programmfenster sein (Größenrahmen, Min/Max), dessen
|
||||
Windows-Rahmen unsichtbar ist; am Rand meldet es Windows den Größenrahmen."""
|
||||
from provtool.gui import nativ, rahmen
|
||||
lauf = lauf_mit_daten
|
||||
r = lauf.app.rahmen
|
||||
if not r.klassisch:
|
||||
pytest.skip("nur im Win95-Rahmen")
|
||||
m = {}
|
||||
|
||||
def pruefen():
|
||||
n = r.nativ
|
||||
m["aktiv"] = r.nativ_an
|
||||
if not r.nativ_an:
|
||||
return
|
||||
stil, ex = n.stil()
|
||||
m["stil"] = {name: bool(stil & bit) for name, bit in (
|
||||
("Größenrahmen", nativ.WS_THICKFRAME), ("Maximieren", nativ.WS_MAXIMIZEBOX),
|
||||
("Minimieren", nativ.WS_MINIMIZEBOX), ("Systemmenü", nativ.WS_SYSMENU), ("Titel", nativ.WS_CAPTION))}
|
||||
m["programmfenster"] = not (ex & nativ.WS_EX_TOOLWINDOW) and bool(ex & nativ.WS_EX_APPWINDOW)
|
||||
m["rahmen_unsichtbar"] = n.innen_gleich_aussen()
|
||||
x, y, bw, bh = n.aussenmass()
|
||||
m["tk_gleich_aussen"] = (lauf.root.winfo_width(), lauf.root.winfo_height()) == (bw, bh)
|
||||
my = y + bh // 2
|
||||
m["links"] = n.trefferpruefung(n.hwnd, x + 2, my) == nativ.HT["w"]
|
||||
m["ecke"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + bh - 3) == nativ.HT["se"]
|
||||
m["ecke_lang"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + 10) == nativ.HT["ne"]
|
||||
m["innen"] = n.trefferpruefung(n.hwnd, x + bw // 2, y + bh // 2) == nativ.HTCLIENT
|
||||
m["rand_durchlaessig"] = n.trefferpruefung(r.aussen.winfo_id(), x + 2, my) == nativ.HTTRANSPARENT
|
||||
m["innen_nicht"] = n.trefferpruefung(r.aussen.winfo_id(), x + bw // 2, y + 3 + 30) != nativ.HTTRANSPARENT
|
||||
# die Maus landet am Rand wirklich beim äußeren Fenster (durch Tk-Fenster hindurch)
|
||||
m["maus_am_rand"] = n.fenster_bei(x + 2, my) == n.hwnd
|
||||
m["maus_innen"] = n.fenster_bei(x + bw // 2, y + bh // 2) != n.hwnd
|
||||
|
||||
# Tk darf das Fenster bei internen Größenanfragen nicht um einen Rahmen vergrößern
|
||||
n.setzen(x, y, bw - 37, bh - 23) # wie Größe ziehen durch den Benutzer
|
||||
lauf.root.update()
|
||||
vorher = n.aussenmass()
|
||||
lauf.app.status.set("info", " " + "sehr langer Statustext " * 20)
|
||||
lauf.root.update_idletasks()
|
||||
lauf.root.update()
|
||||
m["waechst_nicht"] = (n.aussenmass(), vorher)
|
||||
|
||||
# Maximieren über Windows (wie Win+Hoch / an den oberen Rand ziehen) und zurück
|
||||
n.befehl(nativ.SC_MAXIMIZE)
|
||||
lauf.root.update()
|
||||
ax, ay, aw, ah = rahmen.arbeitsbereich(lauf.root)
|
||||
m["max"] = (r.maximiert, n.maximiert())
|
||||
m["max_inhalt"] = ((lauf.root.winfo_rootx(), lauf.root.winfo_rooty(), lauf.root.winfo_width(),
|
||||
lauf.root.winfo_height()), (ax, ay, aw, ah))
|
||||
m["max_symbol"] = str(r.titelleiste.knoepfe["max"].cget("image")) == str(r.titelleiste._bilder["restore"])
|
||||
m["kein_rand_maximiert"] = n.randbereich(ax + 1, ay + ah // 2) == ""
|
||||
n.befehl(nativ.SC_RESTORE)
|
||||
lauf.root.update()
|
||||
m["zurueck"] = (r.maximiert, n.aussenmass() == vorher)
|
||||
m["normale_geometrie"] = r.normale_geometrie()
|
||||
|
||||
lauf.ablauf([(IMMER, lambda: lauf.root.after(300, pruefen), "prüfen"),
|
||||
(lambda: "aktiv" in m and ("zurueck" in m or not m["aktiv"]), lambda: None, "fertig")],
|
||||
sekunden=15)
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
assert m["aktiv"], "natives Fenster wurde nicht eingerichtet"
|
||||
assert all(m["stil"].values()), f"Fensterstil unvollständig: {m['stil']}"
|
||||
assert m["programmfenster"], "Fenster ist für Windows noch ein Werkzeugfenster (kein Andocken/Win+Pfeil)"
|
||||
assert m["rahmen_unsichtbar"] and m["tk_gleich_aussen"], "Windows-Rahmen ist sichtbar / Tk-Größe passt nicht"
|
||||
for k in ("links", "ecke", "ecke_lang", "innen", "rand_durchlaessig", "innen_nicht", "maus_am_rand",
|
||||
"maus_innen"):
|
||||
assert m[k], f"Trefferprüfung am Rand falsch: {k} ({m})"
|
||||
assert m["waechst_nicht"][0] == m["waechst_nicht"][1], f"Fenster ist gewachsen: {m['waechst_nicht']}"
|
||||
assert m["max"] == (True, True), f"Maximieren über Windows nicht erkannt: {m['max']}"
|
||||
assert m["max_inhalt"][0] == m["max_inhalt"][1], f"maximiert nicht genau im Arbeitsbereich: {m['max_inhalt']}"
|
||||
assert m["max_symbol"] and m["kein_rand_maximiert"]
|
||||
assert m["zurueck"] == (False, True), f"Wiederherstellen: {m['zurueck']}"
|
||||
assert m["normale_geometrie"].startswith(f"{m['waechst_nicht'][1][2]}x{m['waechst_nicht'][1][3]}+")
|
||||
lauf.pruefen()
|
||||
|
||||
|
||||
def test_haendler_dialog_airtime_staffel_grossbuchstaben(lauf_mit_daten):
|
||||
"""Airtime-% und eigene Jahresbonus-Staffel je Händler; die GROSSBUCHSTABEN-Regel ist ein Häkchen
|
||||
(gemeldet: Vodafone fehlte im Export – die Regel ging verloren, wenn man „Hinzufügen“ vergaß)."""
|
||||
from provtool.db import CAPS_ANBIETER, CAPS_WERT
|
||||
from provtool.gui.dialogs import HaendlerDialog, StaffelDialog
|
||||
lauf = lauf_mit_daten
|
||||
app = lauf.app
|
||||
hid = app.db.haendler_liste()[0].id
|
||||
hd = lambda: lauf.dialog(HaendlerDialog) # noqa: E731
|
||||
sd = lambda: lauf.dialog(StaffelDialog) # noqa: E731
|
||||
|
||||
def eintragen():
|
||||
d = hd()
|
||||
d.v_air.set("40")
|
||||
d.v_caps.set(True)
|
||||
d.v_eigen_staffel.set(True)
|
||||
d._staffel_zeigen()
|
||||
lauf.root.after(10, d._staffel_bearbeiten)
|
||||
|
||||
def staffel_eingeben():
|
||||
d = sd()
|
||||
d._setzen([(1, 100), (3, None)])
|
||||
d.zeilen[0][2].set("2,50")
|
||||
d._ok()
|
||||
|
||||
lauf.ablauf([
|
||||
(IMMER, lambda: app.haendler(hid), "Händler-Dialog"),
|
||||
(lambda: hd() is not None, eintragen, "Airtime, Häkchen, eigene Staffel"),
|
||||
(lambda: sd() is not None, staffel_eingeben, "Staffel eingeben"),
|
||||
(lambda: sd() is None and "2,50" in hd().l_staffel.cget("text"), lambda: hd()._speichern(), "Speichern"),
|
||||
(IMMER, lambda: hd().abbrechen(), "Schließen"),
|
||||
(lambda: hd() is None, lambda: None, "zu"),
|
||||
])
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
h = app.db.haendler(hid)
|
||||
assert h.airtime_prozent == 40
|
||||
assert h.staffel == [(1, 250), (3, None)]
|
||||
assert (CAPS_ANBIETER, CAPS_WERT) in app.db.kennungen(hid)
|
||||
assert ("freenet", "111") in app.db.kennungen(hid)
|
||||
lauf.pruefen()
|
||||
|
||||
|
||||
def test_export_dialog_standard_eine_datei_ohne_null(lauf_mit_daten, tmp_path):
|
||||
from provtool.gui.dialogs import ExportDialog
|
||||
lauf = lauf_mit_daten
|
||||
app = lauf.app
|
||||
app.db.betrag_setzen(next(p["id"] for p in app.positionen if p["provisionsart"] == "SIM-Karten"
|
||||
and p["haendler_id"] > 0), 0)
|
||||
app._aktualisieren()
|
||||
app.settings["export_ordner"] = str(tmp_path / "x")
|
||||
app.settings["export_pro_anbieter"] = True # alte Einstellung darf nicht mehr trennen
|
||||
ergebnis = {}
|
||||
|
||||
def ok():
|
||||
d = lauf.dialog(ExportDialog)
|
||||
ergebnis.update(anb=d.v_anb.get(), null=d.v_null.get(), haendler=list(d.haken))
|
||||
d.v_auf.set(False)
|
||||
d._ok()
|
||||
|
||||
from provtool.gui import win95 as w
|
||||
lauf.ablauf([
|
||||
(IMMER, app.exportieren, "Export"),
|
||||
(lambda: lauf.dialog(ExportDialog) is not None, ok, "Exportieren"),
|
||||
(lambda: any(d.title() == "Export" for d in lauf.dialoge()),
|
||||
lambda: [d.abbrechen() for d in lauf.dialoge() if isinstance(d, w.Dialog)], "Meldung schließen"),
|
||||
])
|
||||
assert not lauf.uebrig, lauf.diagnose()
|
||||
assert ergebnis == {"anb": False, "null": True, "haendler": [app.db.haendler_liste()[0].id]}
|
||||
import openpyxl
|
||||
dateien = sorted((tmp_path / "x").glob("*.xlsx"))
|
||||
assert [d.name for d in dateien] == ["Händler A - Oktober 2026.xlsx"]
|
||||
zeilen = [r for r in openpyxl.load_workbook(dateien[0]).active.iter_rows(values_only=True) if r[1]]
|
||||
assert [r[1] for r in zeilen] == ["Sonderbonus Aktion"], "0-€-Position wurde exportiert"
|
||||
lauf.pruefen()
|
||||
Reference in new issue
Block a user