"""Assemblage du plan de configuration e-CF DGII depuis `ecf_spec.json`. Entree : le contrat e-CF (types DGII en perimetre, evenements d'emission referencant des `update_value` du workflow, des `base_field` du DocType Dossier Vente et des `role_id` RBAC, field_map, references provider Compupar) + un `RoleResolver` (id → nom Frappe issu de rbac_50_roles.json). Sortie : un bundle deterministe (tri stable, aucun horodatage) → reproductible bit-a-bit a contrat constant, donc diffable et re-generable en CI. Contrainte #6 (zero invention) : aucun chiffre propre a OTO n'est ajoute — RNC emetteur, taux ITBIS, TipoCambio et endpoints Compupar restent `null` tels que dans le contrat. Le builder n'ajoute que la resolution des noms de role et un manifeste de tracabilite (compte des valeurs restant a confirmer inclus). """ from __future__ import annotations import os import sys from typing import Any # Reutilisation (workflow #5) du module CRM voisin : le resolveur de roles # (rbac_50_roles.json) est importe, jamais redefini ici. _HERE = os.path.dirname(os.path.abspath(__file__)) _DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", "..")) _CRM = os.path.join(_DELIVERABLES, "crm") if _CRM not in sys.path: sys.path.insert(0, _CRM) from workflow_vente.wflib.rbac import RoleResolver # noqa: E402 def _event_sort_key(ev: dict) -> tuple: """Ordre stable : par evenement de pipeline puis par role.""" return (ev["update_value"], ev.get("tipo_ecf") or "", ev["role_id"]) def _a_confirmer_count(spec: dict, events: list[dict]) -> int: """Compte deterministe des valeurs restant a confirmer (anti-invention #6).""" n = 0 n += 1 if spec["emisor"].get("a_confirmer") else 0 n += 1 if spec["provider_config"].get("a_confirmer") else 0 n += 1 if spec["moneda"].get("a_confirmer") else 0 n += sum(1 for t in spec["taxes"] if t.get("a_confirmer") or t.get("taux_pct") is None) n += sum(1 for e in events if e.get("a_confirmer") or e.get("tipo_ecf") is None) return n def build_bundle(spec: dict, resolver: RoleResolver) -> dict[str, Any]: """Transforme le contrat e-CF en plan de configuration + manifeste.""" events_spec = sorted(spec["emission_events"], key=_event_sort_key) events: list[dict] = [] for ev in events_spec: events.append({ "update_value": ev["update_value"], "tipo_ecf": ev.get("tipo_ecf"), # jamais fabrique (#6) "base_field": ev["base_field"], "role_id": ev["role_id"], "erpnext_role_name": resolver.erpnext_name(ev["role_id"]), "libelle": ev["libelle"], "a_confirmer": bool(ev.get("a_confirmer")), }) tipos = sorted(spec["tipos_ecf"], key=lambda t: t["code"]) tipos_en_scope = [t["code"] for t in tipos if t.get("en_scope")] plan = { "name": spec["config_name"], "provider": spec["provider"], "devise_field": spec["devise_field"], "emisor": spec["emisor"], "provider_config": spec["provider_config"], "e_ncf": spec["e_ncf"], "tipos_ecf": tipos, "tipos_en_scope": tipos_en_scope, "formas_pago": sorted(spec["formas_pago"], key=lambda f: f["code"]), "forma_pago_defaut": spec["forma_pago_defaut"], "moneda": spec["moneda"], "taxes": spec["taxes"], "emission_events": events, "field_map": spec["field_map"], } roles_used = sorted({ev["role_id"] for ev in events_spec}) manifest = { "generated_from": "ecf_spec.json", "rbac_source": "rbac_50_roles.json", "workflow_source": "crm/workflow_vente/workflow_vente_spec.json", "doctype_source": "crm/dossier_vente/doctype_spec.json", "source_version": spec["version"], "config_name": spec["config_name"], "provider": spec["provider"], "counts": { "tipos_ecf": len(tipos), "tipos_en_scope": len(tipos_en_scope), "formas_pago": len(plan["formas_pago"]), "emission_events": len(events), "roles": len(roles_used), "valeurs_a_confirmer": _a_confirmer_count(spec, events), }, "roles_rbac_utilises": [ {"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)} for rid in roles_used ], # Rappel anti-invention (#6) : aucun chiffre OTO n'est fixe en-repo. Le # Fiscaliste eCF / la Direction renseignent RNC, ITBIS, TipoCambio et les # endpoints Compupar (avec source) cote VPS avant toute emission reelle. "note_anti_invention": ( "RNC emetteur, taux ITBIS, TipoCambio USD→DOP et endpoints/credentials " "Compupar restent `null` (a_confirmer) jusqu'a confirmation Compta " "(avec source · #6). Seules les donnees de reference DGII (types e-CF, " "formes de paiement, format e-NCF) sont encodees, avec leur source." ), } return {"manifest": manifest, "ecf_plan": plan}