Files
provisionsmanager95/provtool/regeln.py
T
Kerim AtilganandClaude Opus 5.5 0880ab2d42 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>
2026-11-02 17:23:48 +01:00

116 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)