- 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>
118 lines
3.3 KiB
Python
118 lines
3.3 KiB
Python
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
|