- 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>
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""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)
|