feat: PDF-Beleg-Parser blockbasiert (mehrzeilig) + Seiten als Bild

- Neuer Parser erkennt mehrzeilige Auszüge (N26 & Co.): Beschreibung → Value
  Date → Buchungsdatum → Betrag stehen auf eigenen Zeilen; Betrags-Zeile schließt
  eine Buchung ab, Kopf-/Fußzeilen (Name/Adresse/eigene IBAN/Seitenzahl/
  Spaltenköpfe) werden gefiltert, Gegen-IBAN und Zweck werden übernommen
- Alter Ein-Zeilen-Heuristik-Parser bleibt als Fallback
- /documents/preview liefert zusätzlich die Seiten als PNG-Data-URLs
  (PyMuPDF get_pixmap, kein poppler nötig)
- Tests: N26-artige mehrzeilige Fixture + Seiten-Rendering (74 grün);
  am echten 34-Seiten-Auszug 207 Buchungen erkannt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-20 17:14:12 +02:00
co-authored by Claude Opus 4.8
parent 49b927e2eb
commit 5f88660bcc
3 changed files with 221 additions and 2 deletions
+62
View File
@@ -38,6 +38,68 @@ def test_parse_transactions_heuristic():
assert rows[1]["amount_cents"] == 300000 # Schweizer Tausender-Apostroph
N26_TEXT = "\n".join([
"GUIDO OLIVIER SCHMIT",
"Riedstrasse 18, 8908 Hedingen",
"IBAN: DE54100110012138281295 • BIC: NTSBDEB1XXX",
"Issued on",
"20.08.2026",
"1 / 2",
"Description",
"Booking Date",
"Amount",
"La Flamme",
"Mastercard • Bars & Restaurants",
"Value Date 31.01.2026",
"01.02.2026",
"-97,00€",
"Guido Schmit",
"Income",
"IBAN: LT293250031382885526 • BIC: REVOLT21XXX",
"Gesendet von Revolut",
"Value Date 01.02.2026",
"01.02.2026",
"+43,00€",
"Noemi Konig",
"Outgoing Transfers",
"IBAN: DE65120300001207628981 • BIC: BYLADEM1001",
"WG Miete Guido Schmit",
"Value Date 02.02.2026",
"02.02.2026",
"-288,00€",
])
def test_parse_transactions_n26_blocks():
from app.services import ocr
rows = ocr.parse_transactions(N26_TEXT)
assert len(rows) == 3
assert rows[0]["date"] == "2026-02-01"
assert rows[0]["amount_cents"] == -9700
assert rows[0]["payee"] == "La Flamme"
assert rows[1]["amount_cents"] == 4300 # Eingang (+)
assert rows[1]["payee"] == "Guido Schmit"
assert rows[1]["counterparty_iban"] == "LT293250031382885526"
assert rows[2]["amount_cents"] == -28800
assert rows[2]["payee"] == "Noemi Konig"
# Gegen-IBAN der Miete – NICHT die eigene Kopf-IBAN (DE54…).
assert rows[2]["counterparty_iban"] == "DE65120300001207628981"
def test_render_pages_pdf():
from app.services import ocr
doc = fitz.open()
doc.new_page()
pages = ocr.render_pages(doc.tobytes(), "beleg.pdf", "application/pdf")
assert len(pages) == 1
assert pages[0].startswith("data:image/png;base64,")
def test_pdf_document_preview(admin):
acc = _account(admin)
pdf = _make_pdf()