"""v4.9.11 regression tests for Odoo name-only reconciliation. Odoo customers may not have VAT/NIF filled in. Reconciliation should still be able to suggest/link by normalized company name and should not abort the whole process candidate list when an individual suggestion lookup fails. """ import sys import types if "sqlalchemy" not in sys.modules: sqlalchemy = types.ModuleType("sqlalchemy") sqlalchemy.text = lambda sql: sql sys.modules["sqlalchemy"] = sqlalchemy if "app.db" not in sys.modules: app_db = types.ModuleType("app.db") app_db.engine = object() sys.modules["app.db"] = app_db if "app.action_catalog" not in sys.modules: action_catalog = types.ModuleType("app.action_catalog") action_catalog.get_action_config = lambda code: {"route": "rever", "action": code, "action_required": True, "safe_to_post": False} sys.modules["app.action_catalog"] = action_catalog from app import reconciliation_service as svc def test_company_name_normalization_matches_odoo_without_nif(): score = svc._name_match_score( "ELEGANTLEGACY, LDA VALONGO Portugal", "Elegantlegacy Lda", "Oportunidade · elegantlegacy", ) assert score >= 35 def test_single_odoo_sale_without_nif_becomes_process_candidate(monkeypatch): monkeypatch.setattr( svc, "list_reconciliation_items", lambda *, status, limit, days: [ { "id": "odoo-sale-274", "source_system": "odoo", "external_type": "odoo_sale_order", "title": "Venda Odoo sem oportunidade · S00274", "customer_name": "ELEGANTLEGACY, LDA", "customer_tax_id": "", "customer_email": "", "document_number": "S00274", "document_date": "2026-06-02", "amount": "537.00", "currency": "EUR", "operation_suggestions": [], } ], ) candidates = svc.list_reconciliation_process_candidates(status="open", days=7, limit=10) assert len(candidates) == 1 candidate = candidates[0] assert candidate["match_key"] == "name" assert candidate["confidence"] == "alta" assert candidate["identity_reason"] == "nome fiscal" assert candidate["customer_name"] == "ELEGANTLEGACY, LDA" assert candidate["suggested_stage"] == "ODOO_ORDER_CREATED" assert candidate["suggested_action"] == "SEND_INVOICE" assert candidate["item_ids"] == ["odoo-sale-274"] def test_list_process_candidates_is_resilient_when_item_query_fails(monkeypatch): def fail_listing(*, status, limit, days): raise RuntimeError("simulated db lookup failure") monkeypatch.setattr(svc, "list_reconciliation_items", fail_listing) assert svc.list_reconciliation_process_candidates(status="open", days=7, limit=10) == [] def test_person_name_only_is_not_high_confidence_identity(): confidence, reason = svc._process_group_identity_label( "name", [{"customer_name": "Bruno Oliveira", "customer_tax_id": "", "source_system": "odoo"}], ) assert confidence == "média" assert reason == "nome"