"""Parser für Volksbank/Raiffeisenbank-Kontoauszüge. Realer Fixture-Abgleich (Task 7, Schritt 4): Der eigentliche Auszugszeitraum steht nicht als "vom X bis Y"-Satz im Text (dieser Wortlaut kommt nur in einem separaten quartalsweisen Zinsabschluss-Absatz mit abweichendem Zeitraum vor). Anfangs-/Endsaldo tragen stattdessen je ein eigenes Datum ("alter/neuer Kontostand vom DD.MM.YYYY ..."), aus dem der Auszugszeitraum abgeleitet wird; RE_PERIOD bleibt als Fallback erhalten, falls ein Auszug diese Daten nicht enthält. """ import re from datetime import date from pathlib import Path import pdfplumber from app.parsers.base import (ParsedStatement, ParsedTransaction, ParserError, parse_german_amount, parse_german_date) BANK = "vr" RE_IBAN = re.compile(r"\b([A-Z]{2}\d{2}(?:\s?\d{4}){4,5}(?:\s?\d{1,2})?)\b") # Fallback, falls kein Datum an den Kontostand-Zeilen hängt: "vom 01.06.2020 bis 30.06.2020" RE_PERIOD = re.compile(r"vom\s+(\d{2}\.\d{2}\.\d{4})\s+bis\s+(\d{2}\.\d{2}\.\d{4})") # "alter Kontostand vom 01.06.2020 1.234,56 H" (Datum optional, je nach Formatvariante) RE_OPENING = re.compile(r"alter\s+Kontostand\s+(?:vom\s+(\d{2}\.\d{2}\.\d{4})\s+)?([\d.,]+\s*[SH-]?)", re.I) # "neuer Kontostand vom 30.06.2020 2.345,67 H" RE_CLOSING = re.compile(r"neuer\s+Kontostand\s+(?:vom\s+(\d{2}\.\d{2}\.\d{4})\s+)?([\d.,]+\s*[SH-]?)", re.I) # Buchungszeile: "01.06. 01.06. Muster GmbH 1.234,56 S" RE_TX = re.compile(r"^(\d{2}\.\d{2}\.)\s+(\d{2}\.\d{2}\.)\s+(.*?)\s+([\d.,]+\s*[SH-])$") def parse(path: Path) -> ParsedStatement: with pdfplumber.open(path) as pdf: text = "\n".join((page.extract_text() or "") for page in pdf.pages) opening = RE_OPENING.search(text) closing = RE_CLOSING.search(text) if not opening or not closing: raise ParserError("VR: Anfangs-/Endsaldo nicht gefunden") if opening.group(1) and closing.group(1): period_start = parse_german_date(opening.group(1)) period_end = parse_german_date(closing.group(1)) else: period = RE_PERIOD.search(text) if not period: raise ParserError("VR: Abrechnungszeitraum nicht gefunden") period_start = parse_german_date(period.group(1)) period_end = parse_german_date(period.group(2)) iban_m = RE_IBAN.search(text) txs: list[ParsedTransaction] = [] pending: dict | None = None for line in text.splitlines(): m = RE_TX.match(line.strip()) if m: if pending: txs.append(_finish(pending)) pending = { "booking": parse_german_date(m.group(1), period_end.year), "value": parse_german_date(m.group(2), period_end.year), "head": m.group(3).strip(), "amount": parse_german_amount(m.group(4)), "extra": [], } elif pending and line.strip() and not RE_CLOSING.search(line): pending["extra"].append(line.strip()) if pending: txs.append(_finish(pending)) return ParsedStatement(bank=BANK, iban=iban_m.group(1).replace(" ", "") if iban_m else None, period_start=period_start, period_end=period_end, opening_balance=parse_german_amount(opening.group(2)), closing_balance=parse_german_amount(closing.group(2)), transactions=txs) def _finish(p: dict) -> ParsedTransaction: counterparty = p["extra"][0] if p["extra"] else "" purpose = " ".join([p["head"], *p["extra"][1:]]).strip() return ParsedTransaction(booking_date=p["booking"], value_date=p["value"], amount=p["amount"], purpose=purpose, counterparty=counterparty)