Files
certain/backend/tests/test_documents.py
T
GuidoandClaude Opus 4.8 5f88660bcc 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>
2026-08-20 17:14:12 +02:00

118 lines
3.3 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.
import io
import pytest
fitz = pytest.importorskip("fitz")
def _account(admin):
r = admin.post("/api/accounts", json={"name": "Beleg", "type": "girokonto", "currency": "EUR"})
assert r.status_code == 201
return r.json()
def _make_pdf() -> bytes:
doc = fitz.open()
page = doc.new_page()
page.insert_text((50, 72), "15.03.2026 REWE Supermarkt Zuerich -50.00")
page.insert_text((50, 96), "25.03.2026 Gehalt Firma AG 3000.00")
page.insert_text((50, 120), "Kein Datum hier, nur Text 12.34")
return doc.tobytes()
def test_parse_transactions_heuristic():
from app.services import ocr
text = (
"Kontoauszug März 2026\n"
"15.03.2026 REWE Supermarkt -50,00\n"
"2026-03-25 Gehalt 3'000.00\n"
"Zwischensumme\n"
)
rows = ocr.parse_transactions(text)
assert len(rows) == 2
assert rows[0]["date"] == "2026-03-15"
assert rows[0]["amount_cents"] == -5000
assert "REWE" in rows[0]["payee"]
assert rows[1]["date"] == "2026-03-25"
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()
r = admin.post(
"/api/documents/preview",
files={"file": ("beleg.pdf", io.BytesIO(pdf), "application/pdf")},
data={"account_id": str(acc["id"])},
)
assert r.status_code == 200, r.text
body = r.json()
assert body["count"] == 2
rows = body["rows"]
kinds = {(row["type"], row["amount_cents"]) for row in rows}
assert ("expense", -5000) in kinds
assert ("income", 300000) in kinds