Backend: - services/ocr: PDF-Text via PyMuPDF, OCR (Tesseract) für Scans/Bilder, heuristischer Parser (Datum + Betrag je Zeile, CH/DE/US-Beträge). - routers/documents: /documents/preview (Upload -> Text -> Kandidaten -> gemeinsame Prüf-/Buchungslogik des CSV-Imports). csv_import refaktoriert: detect_rows() von CSV und Beleg gemeinsam genutzt. - requirements: PyMuPDF, pytesseract, pillow; install.sh installiert tesseract-ocr (+ deu). pytest: Heuristik-Parser und PDF-Vorschau. Frontend: - Import-Seite: CSV/PDF-Umschalter; PDF-Modus mit Beleg-Auslesen, extrahiertem Text zur Kontrolle und Best-Effort-Hinweis; gleiche Prüfansicht + Buchen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
1.6 KiB
Python
56 lines
1.6 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
|
|
|
|
|
|
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
|