from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path import sys from app.work_center_action_policy import ( effective_action_code, reconstructed_review_status, ) def _ensure_real_sqlalchemy(): module = sys.modules.get("sqlalchemy") if module is not None and not hasattr(module, "__path__"): for key in list(sys.modules): if key == "sqlalchemy" or key.startswith("sqlalchemy."): sys.modules.pop(key, None) def _normalise(items): _ensure_real_sqlalchemy() from app.operations_service import _normalise_work_item_intent return _normalise_work_item_intent(items) def _physical_status(sale, pickings, productions): _ensure_real_sqlalchemy() from app.odoo_service import _derive_physical_status return _derive_physical_status(sale, pickings, productions) ROOT = Path(__file__).resolve().parents[1] def _load_script(): _ensure_real_sqlalchemy() path = ROOT / "scripts" / "apply_v132_operational_coherence.py" spec = spec_from_file_location("apply_v132_operational_coherence", path) module = module_from_spec(spec) assert spec and spec.loader sys.modules[spec.name] = module spec.loader.exec_module(module) return module def test_title_sem_oportunidade_does_not_invent_association_blocker(): item = { "action_code": "FOLLOW_UP_PAYMENT", "opportunity_title": "Orçamento Jasmin sem oportunidade · ORC.1", "opportunity_linking_status": "", "opportunity_metadata": {}, } result = _normalise([item])[0] assert result["action_code"] == "FOLLOW_UP_PAYMENT" def test_explicit_association_blocker_still_wins(): item = { "action_code": "FOLLOW_UP_PAYMENT", "opportunity_linking_status": "ambiguous", "opportunity_metadata": {}, } result = _normalise([item])[0] assert result["action_code"] == "ASSOCIATE_OPPORTUNITY" def test_explicit_reconstructed_review_blocks_physical_validation(): metadata = { "clientflow_record_mode": "reconstructed_invoice_review", "reconstructed_review_status": "required", } item = { "action_code": "VALIDATE_PHYSICAL_ORDER", "opportunity_metadata": metadata, } result = _normalise([item])[0] assert result["action_code"] == "REVIEW_RECONSTRUCTED_PROCESS" assert effective_action_code("VALIDATE_PHYSICAL_ORDER", metadata=metadata) == "REVIEW_RECONSTRUCTED_PROCESS" def test_validated_reconstructed_review_does_not_reopen_from_title(): metadata = { "clientflow_record_mode": "reconstructed_invoice_review", "reconstructed_review_status": "validated", } item = { "action_code": "SEND_INVOICE", "opportunity_title": "Processo reconstruído · Cliente", "opportunity_metadata": metadata, } result = _normalise([item])[0] assert result["action_code"] == "SEND_INVOICE" assert reconstructed_review_status(metadata) == "validated" def test_board_prefers_pending_task_and_explicit_review(): source = (ROOT / "app" / "admin_ui" / "pages" / "opportunities.py").read_text(encoding="utf-8") function = source[source.index("def _opportunity_card_next_action"):source.index("def _is_noise_opportunity")] assert function.index("reconstructed_review_required(metadata)") < function.index("central = _central_next_action_for_card") assert function.index("pending_primary_action_code") < function.index("central = _central_next_action_for_card") assert 'return "Validar processo reconstruído"' in function def test_odoo_assigned_is_reserved_not_ready_to_ship(): result = _physical_status( {"state": "sale"}, [{"name": "WH/OUT/1", "state": "assigned", "picking_type_id": [2, "Delivery Orders"]}], [], ) assert result["physical_status"] == "picking_assigned" assert result["stage"] == "ORDER_PREPARATION" assert result["ready_to_ship"] is False assert result["picking_reserved"] is True def test_migration_plan_rewinds_assigned_and_requires_explicit_review(): module = _load_script() opportunities = [{ "id": "opp-1", "title": "Fwd cliente", "customer_name": "NOLTIA SYSTEM, LDA", "fiscal_name": "NOLTIA SYSTEM, LDA", "stage": "SHIPMENT_CREATED", "last_action_code": "VALIDATE_PHYSICAL_ORDER", "metadata": {"clientflow_record_mode": "reconstructed_invoice_review"}, }] links = [{ "opportunity_id": "opp-1", "system": "odoo", "external_type": "physical_status", "status": "picking_assigned", "payload": {"physical_status": "picking_assigned", "pickings": [{"state": "assigned"}]}, }] tasks = [{ "id": "task-1", "opportunity_id": "opp-1", "action_code": "VALIDATE_PHYSICAL_ORDER", "status": "pending", "metadata": {}, }] plans = module._plan_opportunities(opportunities, links, tasks, []) assert len(plans) == 1 plan = plans[0] assert plan.safe is True assert plan.target_stage == "ORDER_PREPARATION" assert plan.reconstructed_after == "required" assert plan.task_operation == "convert_sensitive_to_review" def test_completed_review_is_persisted_as_validated_not_reopened(): module = _load_script() opportunities = [{ "id": "opp-2", "title": "Processo reconstruído", "customer_name": "RICARDO", "fiscal_name": "RICARDO", "stage": "PAYMENT_CONFIRMED", "last_action_code": "SEND_INVOICE", "metadata": {"clientflow_record_mode": "reconstructed_invoice_review", "reconstructed_review_required": True}, }] tasks = [ {"id": "review-done", "opportunity_id": "opp-2", "action_code": "REVIEW_RECONSTRUCTED_PROCESS", "status": "done", "metadata": {}}, {"id": "invoice", "opportunity_id": "opp-2", "action_code": "SEND_INVOICE", "status": "pending", "metadata": {}}, ] plans = module._plan_opportunities(opportunities, [], tasks, []) assert len(plans) == 1 assert plans[0].reconstructed_after == "validated" assert plans[0].task_operation == "none" def test_stale_candidate_requires_identity_unless_name_or_nif_matches(): module = _load_script() items = [ {"id": "i1", "external_id": "318", "document_number": "S00318", "customer_name": "DUNAS INVENCÍVEIS, LDA", "customer_tax_id": "", "payload": {}}, {"id": "i2", "external_id": "323", "document_number": "S00323", "customer_name": "BBKW, LDA", "customer_tax_id": "", "payload": {}}, ] links = [ {"opportunity_id": "o1", "external_id": "318", "external_name": "S00318"}, {"opportunity_id": "o2", "external_id": None, "external_name": "S00323"}, ] opportunities = { "o1": {"fiscal_name": "DUNAS INVENCÍVEIS, UNIPESSOAL, LDA", "customer_name": "", "title": "", "fiscal_tax_id": ""}, "o2": {"fiscal_name": "", "customer_name": "", "title": "", "fiscal_tax_id": ""}, } plans = module._plan_candidates(items, links, opportunities, allow_unknown_identity=False) by_ref = {plan.reference: plan for plan in plans} assert by_ref["S00318"].safe is True assert by_ref["S00323"].safe is False assert by_ref["S00323"].reason == "identity_unknown_requires_review" def test_reconciliation_no_longer_creates_validation_from_delivery_ready(): source = (ROOT / "app" / "reconciliation_service.py").read_text(encoding="utf-8") assert 'if fulfilment.get("delivery_done"):' in source assert 'if fulfilment.get("delivery_done") or fulfilment.get("delivery_ready")' not in source assert "DELETE FROM operation_links" in source def test_review_action_is_catalogued_and_completion_persists_validation(): catalog = (ROOT / "app" / "action_catalog.py").read_text(encoding="utf-8") service = (ROOT / "app" / "opportunity_service.py").read_text(encoding="utf-8") assert '"REVIEW_RECONSTRUCTED_PROCESS": {' in catalog assert 'if action_code == "REVIEW_RECONSTRUCTED_PROCESS":' in service assert 'reconstructed_process_review_validated' in service