- Design: „modern“ (hell, flach, wie Windows 11, normaler Windows-Rahmen) ist Standard; „win95“ bleibt unter Datei > Einstellungen wählbar (Einstellung „design“). - Export: Zeitraum von–bis (nach Aktivierungsdatum oder Abrechnungszeitraum des Anbieters), exportierte Positionen werden als abgerechnet gekennzeichnet (Betrag/Händler eingefroren) und per Fingerabdruck auch in späteren Dateien/Abrechnungen nicht noch einmal abgerechnet. - Datei > Abrechnung erledigt / wieder öffnen: erledigte Abrechnungen sind gegen Änderungen gesperrt. - DB-Schema v4 (Migration inkl. Fingerabdrücke für alte Positionen). - Linux-Build (python:3.12-bookworm) als zweiter Pipeline-Job mit Tests und Selbsttest; Release-Upload für beide Jobs über build/release_hochladen.sh. - Behoben: normaler Windows-Rahmen stürzte beim Start ab (Hauptrahmen.nativ fehlte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Zeitraum für Ansicht und Export: nur Positionen von–bis abrechnen.
|
||
|
||
Bezugsdatum einer Position („basis“):
|
||
- ``aktivierung`` – Aktivierungs-/Anschaltdatum der Position (Spalte „Datum“)
|
||
- ``abrechnung`` – Abrechnungszeitraum des Anbieters: bei freenet der Beginn der Abrechnungswoche
|
||
(„2026.10.01-2026.10.07“), bei Vodafone der Monat des Abrechnungslaufs („VF2609“,
|
||
„CY202609UM“ -> 1.9.2026). Ist er nicht lesbar, gilt das Aktivierungsdatum.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
from datetime import date, timedelta
|
||
|
||
AKTIVIERUNG, ABRECHNUNG = "aktivierung", "abrechnung"
|
||
BASEN = {AKTIVIERUNG: "nach Aktivierungsdatum", ABRECHNUNG: "nach Abrechnungszeitraum des Anbieters"}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Zeitraum:
|
||
von: date | None = None
|
||
bis: date | None = None
|
||
basis: str = AKTIVIERUNG
|
||
|
||
def enthaelt(self, p: dict) -> bool:
|
||
d = bezugsdatum(p, self.basis)
|
||
if d is None:
|
||
return False # ohne Datum lässt sich nicht sagen, ob sie dazugehört
|
||
return (self.von is None or d >= self.von) and (self.bis is None or d <= self.bis)
|
||
|
||
def text(self) -> str:
|
||
von = self.von.strftime("%d.%m.%Y") if self.von else "Anfang"
|
||
bis = self.bis.strftime("%d.%m.%Y") if self.bis else "heute"
|
||
return f"{von} – {bis} ({BASEN[self.basis]})"
|
||
|
||
|
||
def datum_lesen(text: str) -> date | None:
|
||
"""'1.10.2026', '01.10.26', '2026-10-01' -> date; leer -> None; sonst ValueError."""
|
||
s = (text or "").strip()
|
||
if not s:
|
||
return None
|
||
m = re.fullmatch(r"(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})", s)
|
||
if m:
|
||
t, mo, j = (int(x) for x in m.groups())
|
||
if j < 100:
|
||
j += 2000
|
||
return date(j, mo, t)
|
||
m = re.fullmatch(r"(\d{4})-(\d{1,2})-(\d{1,2})", s)
|
||
if m:
|
||
return date(*(int(x) for x in m.groups()))
|
||
raise ValueError(f"Kein gültiges Datum: {text!r} (bitte TT.MM.JJJJ)")
|
||
|
||
|
||
def abrechnungsdatum(zeitraum: str | None) -> date | None:
|
||
s = (zeitraum or "").strip()
|
||
m = re.match(r"(\d{4})\.(\d{2})\.(\d{2})", s) # freenet: 2026.10.01-2026.10.07
|
||
if m:
|
||
try:
|
||
return date(*(int(x) for x in m.groups()))
|
||
except ValueError:
|
||
return None
|
||
m = re.match(r"(?:VF|CY)(\d{4}|\d{2})(\d{2})", s.upper()) # Vodafone: VF2609, CY202609UM
|
||
if m:
|
||
j, mo = int(m.group(1)), int(m.group(2))
|
||
if j < 100:
|
||
j += 2000
|
||
if 1 <= mo <= 12:
|
||
return date(j, mo, 1)
|
||
return None
|
||
|
||
|
||
def bezugsdatum(p: dict, basis: str) -> date | None:
|
||
if basis == ABRECHNUNG:
|
||
d = abrechnungsdatum(p.get("zeitraum"))
|
||
if d is not None:
|
||
return d
|
||
if p.get("datum"):
|
||
try:
|
||
return date.fromisoformat(p["datum"])
|
||
except ValueError:
|
||
return None
|
||
return None
|
||
|
||
|
||
def vormonat(heute: date | None = None) -> tuple[date, date]:
|
||
heute = heute or date.today()
|
||
ende = heute.replace(day=1) - timedelta(days=1)
|
||
return ende.replace(day=1), ende
|
||
|
||
|
||
def dieser_monat(heute: date | None = None) -> tuple[date, date]:
|
||
heute = heute or date.today()
|
||
naechster = (heute.replace(day=28) + timedelta(days=4)).replace(day=1)
|
||
return heute.replace(day=1), naechster - timedelta(days=1)
|