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()
|
||||
+131
-1
@@ -159,9 +159,14 @@ def test_zuordnung_regeln_und_export(db, freenet_csv, vodafone_xlsx, tmp_path):
|
||||
|
||||
# Export
|
||||
eigene = [p for p in db.positionen(aid) if p["haendler_id"] == hid]
|
||||
dateien = export.exportiere_haendler(tmp_path / "out", "Händler A", eigene, "Oktober 2026")
|
||||
dateien = export.exportiere_haendler(tmp_path / "out", "Händler A", eigene, "Oktober 2026", pro_anbieter=True)
|
||||
assert [d.name for d in dateien] == ["Händler A - freenet - Oktober 2026.xlsx",
|
||||
"Händler A - Vodafone - Oktober 2026.xlsx"]
|
||||
# Standard: freenet UND Vodafone in einer Datei (gemeldet: Vodafone fehlte im Export)
|
||||
eine = export.exportiere_haendler(tmp_path / "out1", "Händler A", eigene, "Oktober 2026")
|
||||
assert [d.name for d in eine] == ["Händler A - Oktober 2026.xlsx"]
|
||||
arten = [r[1] for r in openpyxl.load_workbook(eine[0]).active.iter_rows(values_only=True) if r[1]]
|
||||
assert "Freischaltungsvergütung Vodafone" in arten and "SIM-Karten" in arten
|
||||
ws = openpyxl.load_workbook(dateien[0]).active
|
||||
zeilen = [[c.value for c in r] for r in ws.iter_rows(min_row=1, max_row=4)]
|
||||
# Sortierung Zeitraum → Provisionsart → Datum
|
||||
@@ -281,3 +286,128 @@ def test_eigene_kennungen_ausblenden(db, freenet_csv):
|
||||
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
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Airtime / Jahresbonus
|
||||
|
||||
def _pos(art, betrag, unterh="111", rufnr="1601111111", name="Kunde", dienst="Teilnehmerverhaeltnis"):
|
||||
z = freenet_zeile(art, unterh, "05.10.2026", rufnr, name, "Tarif", betrag).split(";")
|
||||
z[24] = dienst
|
||||
return ";".join(z)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bonus_csv(tmp_path):
|
||||
zeilen = [FREENET_KOPF,
|
||||
_pos("Grundvergütung Mobilfunk Credit freenet", "50"),
|
||||
_pos("Grundvergütung Mobilfunk Credit freenet", "0"),
|
||||
_pos("Prepaidvergütung", "20", dienst="AirTime Provision"),
|
||||
_pos("Airtime Umsatzbeteiligung", "10,50")]
|
||||
zeilen += [_pos("Jahresbonus Neuaktivierung", "40", rufnr=f"16000000{i:02d}") for i in range(12)]
|
||||
zeilen += [_pos("Jahresbonus Neuaktivierung", "3", rufnr="1600000099")] # weniger als die Stufe
|
||||
p = tmp_path / "bonus.csv"
|
||||
p.write_bytes(("\r\n".join(zeilen) + "\r\n").encode("cp1252"))
|
||||
return p
|
||||
|
||||
|
||||
def test_typ_erkennung():
|
||||
from provtool import regeln
|
||||
assert regeln.erkennen("Prepaidvergütung Vodafone", "AirTime Provision") == regeln.AIRTIME
|
||||
assert regeln.erkennen("Airtime freenet") == regeln.AIRTIME
|
||||
assert regeln.erkennen("Jahresbonus Neuaktivierung") == regeln.JAHRESBONUS
|
||||
assert regeln.erkennen("SIM-Karten", "Teilnehmerverhaeltnis") == regeln.NORMAL
|
||||
|
||||
|
||||
def test_staffel_stufen():
|
||||
from provtool import regeln
|
||||
st = regeln.STANDARD_STAFFEL
|
||||
assert regeln.bonus_betrag(st, 0, 4000) == 0
|
||||
assert regeln.bonus_betrag(st, 1, 4000) == 500 and regeln.bonus_betrag(st, 9, 4000) == 500
|
||||
assert regeln.bonus_betrag(st, 10, 4000) == 1000 and regeln.bonus_betrag(st, 14, 4000) == 1000
|
||||
assert regeln.bonus_betrag(st, 39, 4000) == 3500
|
||||
assert regeln.bonus_betrag(st, 40, 4000) == 4000 and regeln.bonus_betrag(st, 80, 6000) == 6000 # voll
|
||||
assert regeln.bonus_betrag(st, 25, 1000) == 1000 # nie mehr als laut Abrechnung
|
||||
assert regeln.bonus_betrag(st, 12, -4000) == -1000 # Storno mit derselben Stufe
|
||||
with pytest.raises(ValueError):
|
||||
regeln.staffel_pruefen([(1, 500), (1, 600)])
|
||||
assert "1-9: 5,00 €" in regeln.staffel_beschreibung(st) and "ab 40: voll" in regeln.staffel_beschreibung(st)
|
||||
|
||||
|
||||
def test_airtime_und_jahresbonus(db, bonus_csv, tmp_path):
|
||||
from provtool import regeln
|
||||
hid = db.haendler_speichern(Haendler(0, "Sub", standard_prozent=100), [("freenet", "111")])
|
||||
aid = db.abrechnung_neu("Okt")
|
||||
db.importieren(aid, lies_datei(bonus_csv))
|
||||
pos = db.positionen(aid)
|
||||
air = [p for p in pos if p["typ"] == regeln.AIRTIME]
|
||||
bonus = [p for p in pos if p["typ"] == regeln.JAHRESBONUS]
|
||||
assert len(air) == 2 and len(bonus) == 13
|
||||
assert all(p["betrag"] == 0 for p in air), "Standard: 0 % Airtime = keine Airtime"
|
||||
# 13 Karten -> Stufe 10-14 = 10 € je Karte (die 3-€-Position bleibt bei 3 €)
|
||||
assert sorted(p["betrag"] for p in bonus) == [300] + [1000] * 12
|
||||
assert all(p["prozent"] is None for p in bonus)
|
||||
|
||||
# Airtime 50 %, eigene Staffel
|
||||
h = db.haendler(hid)
|
||||
h.airtime_prozent = 50
|
||||
h.staffel = [(1, 200), (13, None)]
|
||||
db.haendler_speichern(h, db.kennungen(hid))
|
||||
db.neu_berechnen(aid)
|
||||
pos = db.positionen(aid)
|
||||
assert sorted(p["betrag"] for p in pos if p["typ"] == regeln.AIRTIME) == [525, 1000]
|
||||
assert sorted(p["betrag"] for p in pos if p["typ"] == regeln.JAHRESBONUS) == [300] + [4000] * 12
|
||||
|
||||
# Zuweisung ändert die Anzahl Karten -> Staffel der übrigen wird neu berechnet
|
||||
weg = [p["id"] for p in pos if p["typ"] == regeln.JAHRESBONUS][:2]
|
||||
db.haendler_zuweisen(weg, UNZUGEORDNET)
|
||||
rest = [p for p in db.positionen(aid) if p["typ"] == regeln.JAHRESBONUS and p["haendler_id"] == hid]
|
||||
assert len(rest) == 11 and all(p["betrag"] == min(200, p["betrag_orig"]) for p in rest)
|
||||
|
||||
# Typ fest einstellen
|
||||
db.art_typ_setzen("Airtime Umsatzbeteiligung", regeln.NORMAL)
|
||||
assert any(a == "Airtime Umsatzbeteiligung" for a, _, _ in db.provisionsarten())
|
||||
db.neu_berechnen(aid)
|
||||
p = next(p for p in db.positionen(aid) if p["provisionsart"] == "Airtime Umsatzbeteiligung")
|
||||
assert p["typ"] == regeln.NORMAL and p["betrag"] == 1050
|
||||
db.art_typ_setzen("Airtime Umsatzbeteiligung", None)
|
||||
|
||||
# Export: Abrechnung mit Airtime-Summenzeile + eigene Airtime-Liste, 0-€-Positionen weg
|
||||
db.neu_berechnen(aid)
|
||||
eigene = [p for p in db.positionen(aid) if p["haendler_id"] == hid]
|
||||
dateien = export.exportiere_haendler(tmp_path / "out", "Sub", eigene, "Okt")
|
||||
assert [d.name for d in dateien] == ["Sub - Okt.xlsx", "Sub - Airtime - Okt.xlsx"]
|
||||
zeilen = [r for r in openpyxl.load_workbook(dateien[0]).active.iter_rows(values_only=True)]
|
||||
daten = [r for r in zeilen if r[1]]
|
||||
assert all(r[10] != 0 for r in daten), "0-€-Positionen dürfen nicht im Export stehen"
|
||||
assert daten[-1][1] == "Airtime" and daten[-1][10] == 15.25
|
||||
assert not any(r[1] == "Prepaidvergütung" for r in daten), "Airtime-Einzelposten gehören in die eigene Liste"
|
||||
air_zeilen = [r for r in openpyxl.load_workbook(dateien[1]).active.iter_rows(values_only=True) if r[1]]
|
||||
assert sorted(r[10] for r in air_zeilen) == [5.25, 10.0]
|
||||
|
||||
# 0 % Airtime: keine Airtime-Zeile, keine Airtime-Datei
|
||||
h = db.haendler(hid)
|
||||
h.airtime_prozent = 0
|
||||
db.haendler_speichern(h, db.kennungen(hid))
|
||||
db.neu_berechnen(aid)
|
||||
eigene = [p for p in db.positionen(aid) if p["haendler_id"] == hid]
|
||||
dateien = export.exportiere_haendler(tmp_path / "out0", "Sub", eigene, "Okt")
|
||||
assert [d.name for d in dateien] == ["Sub - Okt.xlsx"]
|
||||
assert not any(r[1] == "Airtime" for r in openpyxl.load_workbook(dateien[0]).active.iter_rows(values_only=True))
|
||||
|
||||
|
||||
def test_migration_v2_haendler_ohne_airtime(tmp_path):
|
||||
import sqlite3
|
||||
pfad = tmp_path / "v2.db"
|
||||
con = sqlite3.connect(pfad)
|
||||
con.executescript("""CREATE TABLE haendler (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
ansprechpartner TEXT DEFAULT '', email TEXT DEFAULT '', notiz TEXT DEFAULT '',
|
||||
standard_prozent REAL NOT NULL DEFAULT 100, aktiv INTEGER NOT NULL DEFAULT 1);
|
||||
INSERT INTO haendler (name, standard_prozent) VALUES ('Alt', 90);""")
|
||||
con.close()
|
||||
d = Datenbank(pfad)
|
||||
h = d.haendler_liste()[0]
|
||||
assert h.name == "Alt" and h.airtime_prozent == 0 and h.staffel is None
|
||||
assert d.staffel_fuer(h.id) == d.standard_staffel()
|
||||
d.standard_staffel_setzen([(1, 100), (5, None)])
|
||||
assert d.staffel_fuer(h.id) == [(1, 100), (5, None)]
|
||||
d.close()
|
||||
Reference in new issue
Block a user