"""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(" 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 "19.79" 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)