e2becd6219
- Reviewer now sends all pages to the review VLM alongside extracted JSON. - XML builder deduplicates account.tax records by rate. - XML builder raises on confidence below min_confidence_threshold. - Adds fractional tax-rate support with stable external IDs. - Adds expected_invoice.xml fixture, reviewer tests, and XML regression tests.
159 lines
5.0 KiB
Python
159 lines
5.0 KiB
Python
"""Tests for the Odoo XML builder."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from lxml import etree
|
|
|
|
from odoo_ocr.config import Settings
|
|
from odoo_ocr.pipeline.xml_builder import build_odoo_xml
|
|
from odoo_ocr.schemas import ExtractedInvoice, InvoiceLineItem, ReviewResult
|
|
|
|
|
|
def _fixture_invoice() -> ExtractedInvoice:
|
|
return ExtractedInvoice(
|
|
vendor_name="Acme Supplies",
|
|
vendor_vat="GB123456789",
|
|
invoice_number="INV-2024-001",
|
|
invoice_date="2024-05-01",
|
|
due_date="2024-06-01",
|
|
currency="EUR",
|
|
payment_terms="Net 30",
|
|
line_items=[
|
|
InvoiceLineItem(
|
|
description="Consulting services",
|
|
quantity=Decimal("10.0"),
|
|
unit_price=Decimal("100.00"),
|
|
total_price=Decimal("1000.00"),
|
|
tax_rate=Decimal("20"),
|
|
)
|
|
],
|
|
subtotal=Decimal("1000.00"),
|
|
tax_total=Decimal("200.00"),
|
|
total=Decimal("1200.00"),
|
|
)
|
|
|
|
|
|
def test_build_odoo_xml() -> None:
|
|
settings = Settings()
|
|
invoice = _fixture_invoice()
|
|
xml = build_odoo_xml(invoice, ReviewResult(valid=True, confidence=0.95), settings)
|
|
|
|
assert xml.startswith("<?xml version=")
|
|
assert 'model="res.partner"' in xml
|
|
assert 'model="account.move"' in xml
|
|
assert 'model="account.move.line"' in xml
|
|
assert "Acme Supplies" in xml
|
|
assert "INV-2024-001" in xml
|
|
assert "in_invoice" in xml
|
|
|
|
|
|
def test_xml_parses_with_lxml() -> None:
|
|
invoice = _fixture_invoice()
|
|
xml = build_odoo_xml(invoice, ReviewResult(valid=True, confidence=0.95), Settings())
|
|
|
|
root = etree.fromstring(xml.encode("utf-8"))
|
|
models = {record.get("model") for record in root.iter("record")}
|
|
assert models == {"res.partner", "account.tax", "account.move", "account.move.line"}
|
|
|
|
|
|
def test_xml_matches_expected_fixture() -> None:
|
|
invoice = _fixture_invoice()
|
|
xml = build_odoo_xml(invoice, ReviewResult(valid=True, confidence=0.95), Settings())
|
|
|
|
expected = (Path(__file__).parent / "fixtures" / "expected_invoice.xml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
def _normalize(doc: str) -> dict:
|
|
root = etree.fromstring(doc.encode("utf-8"))
|
|
|
|
def _el(element) -> dict:
|
|
return {
|
|
"tag": element.tag,
|
|
"attrs": dict(element.attrib.items()),
|
|
"text": (element.text or "").strip(),
|
|
"children": [_el(child) for child in element],
|
|
}
|
|
|
|
return _el(root)
|
|
|
|
assert _normalize(xml) == _normalize(expected)
|
|
|
|
|
|
def test_fractional_tax_rate_ids_are_distinct() -> None:
|
|
invoice = ExtractedInvoice(
|
|
vendor_name="X",
|
|
invoice_number="1",
|
|
invoice_date="2024-01-01",
|
|
line_items=[
|
|
InvoiceLineItem(description="A", tax_rate=Decimal("19.79")),
|
|
InvoiceLineItem(description="B", tax_rate=Decimal("15.2")),
|
|
InvoiceLineItem(description="C", tax_rate=Decimal("15.7")),
|
|
],
|
|
)
|
|
xml = build_odoo_xml(invoice, ReviewResult(valid=True, confidence=0.95), Settings())
|
|
|
|
assert 'id="purchase_vat_19_79"' in xml
|
|
assert 'id="purchase_vat_15_2"' in xml
|
|
assert 'id="purchase_vat_15_7"' in xml
|
|
assert xml.count('model="account.tax"') == 3
|
|
assert "Purchase VAT 19.79%" in xml
|
|
assert "<field name=\"amount\">19.79</field>" in xml
|
|
|
|
|
|
def test_xml_adds_review_comment_for_low_confidence() -> None:
|
|
settings = Settings()
|
|
invoice = ExtractedInvoice(
|
|
vendor_name="X",
|
|
invoice_number="1",
|
|
invoice_date="2024-01-01",
|
|
line_items=[],
|
|
subtotal=Decimal("0"),
|
|
tax_total=Decimal("0"),
|
|
total=Decimal("0"),
|
|
)
|
|
review = ReviewResult(valid=False, confidence=0.80)
|
|
|
|
xml = build_odoo_xml(invoice, review, settings)
|
|
assert "HUMAN REVIEW ADVISED" in xml
|
|
|
|
|
|
def test_duplicate_tax_rates_emit_one_record() -> None:
|
|
"""Multiple lines sharing the same tax rate must produce a single account.tax record."""
|
|
invoice = ExtractedInvoice(
|
|
vendor_name="X",
|
|
invoice_number="1",
|
|
invoice_date="2024-01-01",
|
|
line_items=[
|
|
InvoiceLineItem(description="A", tax_rate=Decimal("20")),
|
|
InvoiceLineItem(description="B", tax_rate=Decimal("20")),
|
|
InvoiceLineItem(description="C", tax_rate=Decimal("0")),
|
|
],
|
|
)
|
|
xml = build_odoo_xml(invoice, ReviewResult(valid=True, confidence=0.95), Settings())
|
|
|
|
assert xml.count('id="purchase_vat_20"') == 1
|
|
assert xml.count('id="purchase_vat_0"') == 1
|
|
assert xml.count('model="account.tax"') == 2
|
|
|
|
|
|
def test_xml_refuses_low_confidence() -> None:
|
|
settings = Settings()
|
|
invoice = ExtractedInvoice(
|
|
vendor_name="X",
|
|
invoice_number="1",
|
|
invoice_date="2024-01-01",
|
|
line_items=[],
|
|
subtotal=Decimal("0"),
|
|
tax_total=Decimal("0"),
|
|
total=Decimal("0"),
|
|
)
|
|
review = ReviewResult(valid=False, confidence=0.70)
|
|
|
|
with pytest.raises(ValueError, match="below the minimum threshold"):
|
|
build_odoo_xml(invoice, review, settings)
|