Windows-Build / windows-exe (push) Canceled after 53m28s
Neu: - Eigene Kennungen (eigene Filiale) ausblenden: nicht angezeigt, nicht mitgerechnet, nicht exportiert; per Rechtsklick, Händler-Menü oder Import-Bericht; Händler-Zuordnung hat Vorrang - Menüs im Win95-Stil: wechseln beim Überfahren der Menüleiste, Tastatur, Kontextmenü gleich - Dialoge sperren nur den Inhalt: Hauptfenster bleibt verschiebbar/maximierbar; Dialoge haben eigenen Taskleisten-Eintrag; Klick auf gesperrten Inhalt holt den Dialog nach vorn (blinkt) - Trennlinie zwischen Händler- und Positionsliste sichtbar und ziehbar, Position wird gemerkt - Strg+Rücktaste / Strg+Entf löschen ganze Wörter in allen Eingabefeldern Behoben: - Gedrückte Knöpfe zeigten keine Beschriftung (activeForeground gehört in Tk zur Klasse Background) - Tipp des Tages: Text lief aus dem sichtbaren Bereich - Größenänderung: breiterer Griffbereich mit Ecken, Zeiger wird zuverlässig zurückgesetzt, flüssiger; Verschieben über Windows selbst (inkl. Andocken), Minimieren über Windows - Zeichen, die MS Sans Serif nicht hat (…, ▸, →, •), durch Win95-übliche ersetzt - Menü/Tooltip/Dialog: Größe+Position zusammen setzen (Tk ignoriert sonst die Position) - Suche reagiert erst nach kurzer Tipp-Pause (flüssigeres Tippen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
"""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).
|
||
"""
|
||
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
|
||
|
||
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"
|
||
|
||
|
||
def schreibe_xlsx(ziel: str | Path, positionen: list[dict], *, kopfzeile: bool = False, summe: bool = True,
|
||
blattname: str = "Tabelle1") -> Path:
|
||
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):
|
||
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 = 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 = True, kopfzeile: bool = False, summe: bool = True) -> list[Path]:
|
||
"""Schreibt eine (oder je Anbieter eine) Datei für einen Händler. Gibt die erzeugten Pfade zurück."""
|
||
ordner = Path(ordner)
|
||
if not positionen:
|
||
return []
|
||
gruppen: dict[str, list[dict]] = {}
|
||
if pro_anbieter:
|
||
for p in positionen:
|
||
gruppen.setdefault(p["anbieter"], []).append(p)
|
||
else:
|
||
gruppen[""] = positionen
|
||
out = []
|
||
for anbieter, pos in sorted(gruppen.items()):
|
||
teile = [haendler_name] + ([ANBIETER.get(anbieter, anbieter)] if anbieter else []) + [titel]
|
||
name = dateiname(" - ".join(t for t in teile if t)) + ".xlsx"
|
||
out.append(schreibe_xlsx(ordner / name, pos, kopfzeile=kopfzeile, summe=summe))
|
||
return out
|