"""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)