81 lines
3.7 KiB
Python
81 lines
3.7 KiB
Python
"""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)
|