- ParsedTransaction und ParsedStatement Datenklassen - parse_german_amount(): German format number parsing (1.234,56 -> Decimal) - parse_german_date(): DD.MM.YYYY date parsing with optional year - detect_bank(): Recognize VR/HVB/DKB from text markers - balance_difference(): Validate statement balance consistency - dedup_hash(): SHA256-based transaction deduplication Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
47 lines
1.2 KiB
Python
47 lines
1.2 KiB
Python
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
|
|
class ParserError(Exception):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ParsedTransaction:
|
|
booking_date: date
|
|
value_date: date | None
|
|
amount: Decimal
|
|
purpose: str
|
|
counterparty: str
|
|
|
|
|
|
@dataclass
|
|
class ParsedStatement:
|
|
bank: str
|
|
iban: str | None
|
|
period_start: date | None
|
|
period_end: date | None
|
|
opening_balance: Decimal | None
|
|
closing_balance: Decimal | None
|
|
transactions: list[ParsedTransaction] = field(default_factory=list)
|
|
|
|
|
|
def parse_german_amount(s: str) -> Decimal:
|
|
s = s.strip().replace(" ", "").replace(" ", "")
|
|
neg = s.endswith("-") or s.startswith("-") or s.endswith("S")
|
|
s = s.strip("+-SH")
|
|
value = Decimal(s.replace(".", "").replace(",", "."))
|
|
return -value if neg else value
|
|
|
|
|
|
def parse_german_date(s: str, default_year: int | None = None) -> date:
|
|
m = re.fullmatch(r"(\d{2})\.(\d{2})\.(\d{4})?", s.strip())
|
|
if not m:
|
|
raise ParserError(f"Kein Datum: {s!r}")
|
|
year = int(m.group(3)) if m.group(3) else default_year
|
|
if year is None:
|
|
raise ParserError(f"Jahr fehlt: {s!r}")
|
|
return date(year, int(m.group(2)), int(m.group(1)))
|