da37e91c86
- Refactors process_single into inner pipeline with guaranteed client close. - Runs classifier -> OCR branch -> extraction -> review -> XML/sidecar. - Uses review-corrected invoice when available. - Confidence >= 0.75 produces XML; < 0.90 adds a human-review comment. - Adds end-to-end CLI tests covering confidence bands, corrections, and batch mode.
134 lines
3.9 KiB
Python
134 lines
3.9 KiB
Python
"""End-to-end CLI pipeline tests using the fake VLM client."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from lxml import etree
|
|
from PIL import Image
|
|
|
|
from odoo_ocr import cli
|
|
from odoo_ocr.config import Settings
|
|
from tests.conftest import FakeVLMClient
|
|
|
|
EXTRACTION: dict[str, Any] = {
|
|
"vendor_name": "Acme",
|
|
"vendor_address": None,
|
|
"vendor_vat": None,
|
|
"invoice_number": "INV-9",
|
|
"invoice_date": "2024-01-01",
|
|
"due_date": None,
|
|
"currency": "EUR",
|
|
"payment_terms": None,
|
|
"line_items": [
|
|
{
|
|
"description": "Widget",
|
|
"quantity": 1,
|
|
"unit_price": 100.0,
|
|
"total_price": 100.0,
|
|
"tax_rate": 0,
|
|
}
|
|
],
|
|
"subtotal": 100.0,
|
|
"tax_total": 0.0,
|
|
"total": 100.0,
|
|
"iban": None,
|
|
"raw_ocr_text": "",
|
|
}
|
|
|
|
|
|
def _responses(confidence: float) -> dict[str, Any]:
|
|
return {
|
|
"document classifier": {
|
|
"category": "scanned_print",
|
|
"confidence": 0.9,
|
|
"reasoning": "printed invoice",
|
|
},
|
|
"OCR engine": {"text": "Invoice INV-9 from Acme, total 100.00 EUR"},
|
|
"invoice data extraction": EXTRACTION,
|
|
"invoice review assistant": {
|
|
"valid": True,
|
|
"confidence": confidence,
|
|
"issues": [],
|
|
"corrected_invoice": None,
|
|
},
|
|
}
|
|
|
|
|
|
def _write_invoice(tmp_path: Path) -> Path:
|
|
src = tmp_path / "invoice.jpg"
|
|
Image.new("RGB", (100, 100), "white").save(src)
|
|
return src
|
|
|
|
|
|
def _install_fake(monkeypatch: pytest.MonkeyPatch, responses: dict[str, Any]) -> FakeVLMClient:
|
|
fake = FakeVLMClient(responses=responses)
|
|
monkeypatch.setattr(cli, "OllamaClient", lambda _settings: fake)
|
|
return fake
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("confidence", "expect_xml", "expect_review_comment"),
|
|
[(0.95, True, False), (0.80, True, True), (0.70, False, False)],
|
|
)
|
|
async def test_confidence_bands(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
confidence: float,
|
|
expect_xml: bool,
|
|
expect_review_comment: bool,
|
|
) -> None:
|
|
src = _write_invoice(tmp_path)
|
|
out_dir = tmp_path / "out"
|
|
_install_fake(monkeypatch, _responses(confidence))
|
|
|
|
sidecar = await cli.process_single(src, out_dir, Settings())
|
|
|
|
assert sidecar["review"]["confidence"] == confidence
|
|
assert (out_dir / "invoice_sidecar.json").exists()
|
|
data = json.loads((out_dir / "invoice_sidecar.json").read_text(encoding="utf-8"))
|
|
assert data["extracted_invoice"]["vendor_name"] == "Acme"
|
|
|
|
xml_file = out_dir / "invoice.xml"
|
|
assert xml_file.exists() is expect_xml
|
|
if expect_xml:
|
|
xml = xml_file.read_text(encoding="utf-8")
|
|
assert ("HUMAN REVIEW ADVISED" in xml) is expect_review_comment
|
|
root = etree.fromstring(xml.encode("utf-8"))
|
|
models = {record.get("model") for record in root.iter("record")}
|
|
assert {"res.partner", "account.move", "account.move.line", "account.tax"} <= models
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_uses_corrected_invoice_from_review(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
src = _write_invoice(tmp_path)
|
|
responses = _responses(0.95)
|
|
responses["invoice review assistant"]["corrected_invoice"] = {
|
|
**EXTRACTION,
|
|
"invoice_number": "INV-999",
|
|
}
|
|
_install_fake(monkeypatch, responses)
|
|
|
|
await cli.process_single(src, tmp_path / "out", Settings())
|
|
|
|
xml = (tmp_path / "out" / "invoice.xml").read_text(encoding="utf-8")
|
|
assert "<field name=\"ref\">INV-999</field>" in xml
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_process_batch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_install_fake(monkeypatch, _responses(0.95))
|
|
(tmp_path / "a.jpg").touch()
|
|
Image.new("RGB", (10, 10), "white").save(tmp_path / "b.jpg")
|
|
|
|
results = await cli.process_batch(tmp_path, tmp_path / "out", Settings())
|
|
|
|
assert len(results) == 1
|
|
assert (tmp_path / "out" / "b.xml").exists()
|