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:
1 parent
5a9d6d19a2
commit
da8a7138bd
17 files changed
+829
-80
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user