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
@@ -0,0 +1,115 @@
|
||||
"""Besondere Provisionsarten: Airtime und Jahresbonus Neuaktivierung (Staffel).
|
||||
|
||||
Typ einer Position
|
||||
------------------
|
||||
- ``normal`` – Auszahlung = Prozent der Provision (Händler-Standard bzw. Regel je Provisionsart)
|
||||
- ``airtime`` – Auszahlung = Airtime-Prozent des Händlers (0 % = keine Airtime). Im Export als eine
|
||||
Summenzeile in der Abrechnung, die Einzelposten in einer eigenen Airtime-Liste.
|
||||
- ``jahresbonus`` – Jahresbonus Neuaktivierung: Betrag je Karte nach Staffel, abhängig davon, wie viele
|
||||
Karten der Händler in der Abrechnung hat (z. B. 1–9 Karten: 5 € je Karte ... ab 40: voll).
|
||||
|
||||
Der Typ wird aus Provisionsart/Dienst/Tarif erkannt und kann je Provisionsart fest eingestellt werden.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
NORMAL, AIRTIME, JAHRESBONUS = "normal", "airtime", "jahresbonus"
|
||||
TYPEN = {NORMAL: "Normale Provision", AIRTIME: "Airtime", JAHRESBONUS: "Jahresbonus Neuaktivierung (Staffel)"}
|
||||
|
||||
# Staffel: [(ab Anzahl Karten, Cent je Karte oder None = voller Betrag laut Abrechnung), ...]
|
||||
Staffel = list[tuple[int, "int | None"]]
|
||||
STANDARD_STAFFEL: Staffel = [(1, 500), (10, 1000), (15, 1500), (20, 2000), (25, 2500), (30, 3000), (35, 3500),
|
||||
(40, None)]
|
||||
|
||||
_AIRTIME_WOERTER = ("airtime", "air time", "air-time", "umsatzbeteiligung")
|
||||
|
||||
|
||||
def _text(*teile) -> str:
|
||||
return " ".join(str(t or "") for t in teile).lower()
|
||||
|
||||
|
||||
def erkennen(provisionsart: str | None, dienst: str | None = None, tarif: str | None = None) -> str:
|
||||
"""Typ aus den Texten der Position erkennen (ohne feste Einstellung)."""
|
||||
art = _text(provisionsart)
|
||||
if "jahresbonus" in art or ("bonus" in art and "neuaktiv" in art):
|
||||
return JAHRESBONUS
|
||||
if any(wort in _text(provisionsart, dienst, tarif) for wort in _AIRTIME_WOERTER):
|
||||
return AIRTIME
|
||||
return NORMAL
|
||||
|
||||
|
||||
def typ_von(p: dict, fest: dict[str, str]) -> str:
|
||||
"""Typ einer Position: feste Einstellung der Provisionsart, sonst automatisch erkannt."""
|
||||
art = p.get("provisionsart") or ""
|
||||
if art in fest:
|
||||
return fest[art]
|
||||
return erkennen(art, p.get("dienst"), p.get("tarif"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Staffel
|
||||
|
||||
def staffel_pruefen(staffel) -> Staffel:
|
||||
"""Normalisiert und prüft eine Staffel (aufsteigend, ab >= 1, Beträge >= 0)."""
|
||||
out: Staffel = []
|
||||
for ab, cent in staffel:
|
||||
ab = int(ab)
|
||||
if ab < 1:
|
||||
raise ValueError("Die Anzahl Karten muss mindestens 1 sein.")
|
||||
if cent is not None:
|
||||
cent = int(cent)
|
||||
if cent < 0:
|
||||
raise ValueError("Der Betrag je Karte darf nicht negativ sein.")
|
||||
out.append((ab, cent))
|
||||
out.sort(key=lambda s: s[0])
|
||||
if len({ab for ab, _ in out}) != len(out):
|
||||
raise ValueError("Jede Stufe braucht eine andere Anzahl Karten.")
|
||||
if not out:
|
||||
raise ValueError("Die Staffel braucht mindestens eine Stufe.")
|
||||
return out
|
||||
|
||||
|
||||
def staffel_laden(text: str | None) -> Staffel | None:
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return staffel_pruefen(json.loads(text))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def staffel_text(staffel: Staffel) -> str:
|
||||
return json.dumps([[ab, cent] for ab, cent in staffel])
|
||||
|
||||
|
||||
def stufe(staffel: Staffel, karten: int) -> tuple[bool, int | None]:
|
||||
"""(gefunden, Cent je Karte oder None = voll) für die Anzahl Karten."""
|
||||
treffer = None
|
||||
for ab, cent in staffel:
|
||||
if karten >= ab:
|
||||
treffer = (True, cent)
|
||||
return treffer or (False, 0)
|
||||
|
||||
|
||||
def bonus_betrag(staffel: Staffel, karten: int, betrag_orig: int) -> int:
|
||||
"""Auszahlung einer Jahresbonus-Position. Nie mehr als die Provision laut Abrechnung; Stornos
|
||||
(negative Beträge) werden mit derselben Stufe abgezogen."""
|
||||
gefunden, cent = stufe(staffel, karten)
|
||||
if not gefunden:
|
||||
return 0
|
||||
if cent is None:
|
||||
return betrag_orig
|
||||
if betrag_orig >= 0:
|
||||
return min(cent, betrag_orig)
|
||||
return -min(cent, -betrag_orig)
|
||||
|
||||
|
||||
def staffel_beschreibung(staffel: Staffel, kurz: bool = False) -> str:
|
||||
from provtool.money import fmt
|
||||
teile = []
|
||||
for i, (ab, cent) in enumerate(staffel):
|
||||
bis = staffel[i + 1][0] - 1 if i + 1 < len(staffel) else None
|
||||
bereich = f"ab {ab}" if bis is None else (f"{ab}" if bis == ab else f"{ab}-{bis}")
|
||||
wert = "voll" if cent is None else fmt(cent)
|
||||
teile.append(f"{bereich}: {wert}")
|
||||
return (", " if kurz else "\n").join(teile)
|
||||
Reference in new issue
Block a user