Modernes Design (Standard, Win95 als Legacy), Zeitraum abrechnen, Abrechnung erledigt, Linux-Build

- 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>
This commit is contained in:
Kerim AtilganandClaude Opus 5.5 committed 2026-11-02 20:47:00 +01:00
1 parent 97c73b82c7
commit 851fd6600a
17 files changed
+829 -80

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
"""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)