Files
provisionsmanager95/provtool/export.py
T
Kerim AtilganandClaude Opus 5.5 966379293a
Windows-Build / windows-exe (push) Successful in 5m7s
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

154 lines
6.3 KiB
Python
Raw 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.
"""Export der Händler-Abrechnung als Excel – Aufbau wie „so soll es zum schluss ausschauen.xlsx“.
Spalten: A Zeitraum | B Provisionsart | C Unterhändler-/VO-Nr | D Datum | E Rufnummer | F IMEI/SN |
G Gerät | H Vorname | I Name | J Tarif | K Betrag (Auszahlung) | L Netz | M Dienst | N Aktionscode
Sortierung: Zeitraum -> Provisionsart -> Datum (sonst Reihenfolge der Abrechnung).
Je Händler entstehen zwei Dateien: die Abrechnung (Airtime darin nur als eine Summenzeile) und die
Airtime-Liste mit den Einzelposten. Positionen mit 0,00 € werden weggelassen.
"""
from __future__ import annotations
import re
from datetime import date
from pathlib import Path
from provtool.importer import ANBIETER
from provtool.money import to_euro
from provtool.regeln import AIRTIME
SPALTEN = [
("Zeitraum", 20), ("Provisionsart", 38), ("Unterhändler", 11), ("Datum", 11), ("Rufnummer", 13),
("IMEI/SN", 16), ("Gerät", 16), ("Vorname", 14), ("Name", 30), ("Tarif", 34), ("Betrag", 12),
("Netz", 7), ("Dienst", 22), ("Aktionscode", 12),
]
EURO_FORMAT = '_-* #,##0.00\\ "€"_-;\\-* #,##0.00\\ "€"_-;_-* "-"??\\ "€"_-;_-@_-'
def sortiert(positionen: list[dict]) -> list[dict]:
return sorted(positionen, key=lambda p: (p.get("zeitraum") or "", p.get("provisionsart") or "",
p.get("datum") or "", p.get("reihenfolge") or 0))
def _zahl_oder_text(s: str):
"""Rufnummern/IMEI als Zahl (Format „0“) wie in der Vorlage – führende Nullen bleiben als Text erhalten."""
s = (s or "").strip()
if s.isdigit() and not s.startswith("0") and len(s) <= 15:
return int(s)
return s or None
def dateiname(text: str) -> str:
return re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", text).strip(" .") or "Export"
AIRTIME_TEXT = "Airtime"
def airtime_zeile(airtime: list[dict]) -> dict:
"""Eine Zeile „Airtime = Summe“ für die Abrechnung."""
zeitraeume = sorted({p.get("zeitraum") or "" for p in airtime} - {""})
return {"zeitraum": zeitraeume[0] if len(zeitraeume) == 1 else "", "provisionsart": AIRTIME_TEXT,
"tarif": "Summe laut Airtime-Liste", "betrag": sum(p["betrag"] for p in airtime), "_summe": True}
def schreibe_xlsx(ziel: str | Path, positionen: list[dict], *, kopfzeile: bool = False, summe: bool = True,
blattname: str = "Tabelle1", schluss: list[dict] | None = None) -> Path:
"""schluss: Zeilen, die nach den sortierten Positionen kommen (z. B. die Airtime-Summe)."""
import openpyxl
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
ziel = Path(ziel)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = blattname[:31] or "Tabelle1"
font = Font(name="Aptos Narrow", size=10)
bold = Font(name="Aptos Narrow", size=10, bold=True)
zeile = 1
if kopfzeile:
for c, (titel, _) in enumerate(SPALTEN, start=1):
ws.cell(zeile, c, titel).font = bold
ws.freeze_panes = "A2"
zeile += 1
erste = zeile
for p in sortiert(positionen) + list(schluss or []):
werte = [
p.get("zeitraum") or None,
p.get("provisionsart") or None,
_zahl_oder_text(p.get("kennung")),
date.fromisoformat(p["datum"]) if p.get("datum") else None,
_zahl_oder_text(p.get("rufnummer")),
_zahl_oder_text(p.get("imei")),
p.get("geraet") or None,
p.get("vorname") or None,
p.get("name") or None,
p.get("tarif") or None,
to_euro(p["betrag"]),
p.get("netz") or None,
p.get("dienst") or None,
p.get("aktionscode") or None,
]
for c, v in enumerate(werte, start=1):
cell = ws.cell(zeile, c, v)
cell.font = bold if p.get("_summe") and c in (2, 11) else font
ws.cell(zeile, 4).number_format = "DD.MM.YYYY"
ws.cell(zeile, 5).number_format = "0"
ws.cell(zeile, 6).number_format = "0"
ws.cell(zeile, 11).number_format = EURO_FORMAT
zeile += 1
letzte = zeile - 1
if summe and letzte >= erste:
s = ws.cell(zeile + 1, 11, f"=SUM(K{erste}:K{letzte})")
s.number_format = EURO_FORMAT
s.font = bold
ws.cell(zeile + 1, 10, "Summe").font = bold
for c, (_, breite) in enumerate(SPALTEN, start=1):
ws.column_dimensions[get_column_letter(c)].width = breite
ws.page_setup.orientation = "landscape"
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0
ws.sheet_properties.pageSetUpPr.fitToPage = True
ziel.parent.mkdir(parents=True, exist_ok=True)
wb.save(ziel)
return ziel
def exportiere_haendler(ordner: str | Path, haendler_name: str, positionen: list[dict], titel: str, *,
pro_anbieter: bool = False, kopfzeile: bool = False, summe: bool = True,
null_weglassen: bool = True) -> list[Path]:
"""Schreibt die Abrechnung eines Händlers (bei pro_anbieter je Anbieter eine) und – falls es Airtime
gibt – die Airtime-Liste. In der Abrechnung steht Airtime nur als eine Summenzeile.
Gibt die erzeugten Pfade zurück."""
ordner = Path(ordner)
if null_weglassen:
positionen = [p for p in positionen if p["betrag"]]
if not positionen:
return []
gruppen: dict[str, list[dict]] = {}
for p in positionen:
gruppen.setdefault(p["anbieter"] if pro_anbieter else "", []).append(p)
def name(*teile) -> str:
return dateiname(" - ".join(t for t in (haendler_name,) + teile + (titel,) if t)) + ".xlsx"
out = []
for anbieter, pos in sorted(gruppen.items()):
provision = [p for p in pos if p.get("typ") != AIRTIME]
airtime = [p for p in pos if p.get("typ") == AIRTIME]
schluss = [airtime_zeile(airtime)] if airtime else []
if schluss and null_weglassen and not schluss[0]["betrag"]:
schluss = []
if provision or schluss:
out.append(schreibe_xlsx(ordner / name(ANBIETER.get(anbieter, anbieter) if anbieter else ""),
provision, kopfzeile=kopfzeile, summe=summe, schluss=schluss))
airtime = [p for p in positionen if p.get("typ") == AIRTIME]
if airtime:
out.append(schreibe_xlsx(ordner / name(AIRTIME_TEXT), airtime, kopfzeile=kopfzeile, summe=summe,
blattname=AIRTIME_TEXT))
return out