Files
bin/finance/app/parsers/dkb.py
2026-07-17 17:58:33 +02:00

80 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Parser für DKB (Deutsche Kreditbank)-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);
er wird daher aus den Datumsangaben der beiden Kontostand-Zeilen abgeleitet.
Buchungszeilen haben nur eine Datumsspalte, die ohne Leerzeichen direkt am
Buchungstext klebt, und der Betrag trägt ein optionales führendes Minus statt
eines nachgestellten S/H-Kürzels. Der Anfangssaldo trägt den Zusatz
", Auszug Nr. N", der Endsaldo den Zusatz "um HH:MM Uhr" beides
unterscheidet die echten Saldo-Zeilen von einer abweichend formulierten
Zwischensumme in einem quartalsweisen Rechnungsabschluss-Anhang.
"""
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 = "dkb"
RE_IBAN = re.compile(r"\b([A-Z]{2}\d{2}(?:\s?\d{4}){4,5}(?:\s?\d{1,2})?)\b")
# "Kontostand am 01.06.2020, Auszug Nr. 1 1.234,56"
RE_OPENING = re.compile(r"Kontostand\s+am\s+(\d{2}\.\d{2}\.\d{4}),\s*Auszug\s+Nr\.\s*\d+\s+(-?[\d.,]+)", re.I)
# "Kontostand am 30.06.2020 um 12:00 Uhr -2.345,67"
RE_CLOSING = re.compile(r"Kontostand\s+am\s+(\d{2}\.\d{2}\.\d{4})\s+um\s+\d{2}:\d{2}\s*Uhr\s+(-?[\d.,]+)", re.I)
# Buchungszeile: "01.06.2020Überweisung -123,45" (Datum direkt am Text, kein Leerzeichen)
RE_TX_HEAD = re.compile(r"^(\d{2}\.\d{2}\.\d{4})(\S.*?)\s+(-?[\d.,]+)$")
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("DKB: Anfangs-/Endsaldo nicht gefunden")
period_start = parse_german_date(opening.group(1))
period_end = parse_german_date(closing.group(1))
iban_m = RE_IBAN.search(text)
txs: list[ParsedTransaction] = []
pending: dict | None = None
for line in text.splitlines():
line = line.strip()
if not line:
continue
m = RE_TX_HEAD.match(line)
if m:
if pending:
txs.append(_finish(pending))
booking = parse_german_date(m.group(1))
pending = {
"booking": booking,
"value": booking,
"head": m.group(2).strip(),
"amount": parse_german_amount(m.group(3)),
"extra": [],
}
continue
if pending and not RE_OPENING.search(line) and not RE_CLOSING.search(line):
pending["extra"].append(line)
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)