Airtime je Händler, Jahresbonus-Staffel, Export überarbeitet, natives Windows-Fenster
- 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
c0b740db06
commit
0880ab2d42
14 files changed
+1399
-119
No files matched your search
+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