"""Construction des fixtures ERPNext depuis `rbac_50_roles.json`. Entrée : le contrat RBAC 50 rôles (déjà validé par `rbac.schema.json` dans le job CI `rbac-tests`). Sortie : un « bundle » déterministe contenant les deux fichiers de fixtures Frappe (`role`, `custom_docperm`) + un manifeste de traçabilité (contrainte #6 : tout provient du contrat, rien n'est inventé). Déterminisme : tri stable (rôles par nom Frappe, DocPerms par (role, doctype, permlevel)) → la sortie est reproductible bit-à-bit à contrat constant, donc diffable et re-générable en CI sans horodatage volatil. """ from __future__ import annotations from typing import Any from . import frappe def _sorted_roles(contract: dict) -> list[dict]: return sorted(contract["roles"], key=lambda r: r["erpnext_role_name"]) def build_bundle(contract: dict) -> dict[str, Any]: """Transforme le contrat RBAC en bundle de fixtures Frappe déterministe.""" roles = _sorted_roles(contract) # --- 1. Fixtures Role (unicité garantie par le contrat, re-vérifiée) ------ role_fixtures: list[dict] = [] seen_roles: set[str] = set() for r in roles: name = r["erpnext_role_name"] if name in seen_roles: raise ValueError(f"Rôle Frappe dupliqué dans le contrat : {name!r}") seen_roles.add(name) role_fixtures.append(frappe.role_fixture(name)) # --- 2. Fixtures Custom DocPerm ------------------------------------------ docperms: list[dict] = [] custom_doctypes: set[str] = set() for r in roles: role_name = r["erpnext_role_name"] if_owner = r["scope_donnees"] == frappe.SCOPE_IF_OWNER # Tri des DocTypes cibles pour un ordre stable indépendant du contrat. for perm in sorted(r["permissions_cibles"], key=lambda p: p["doctype"]): doctype = perm["doctype"] if perm.get("custom") is True: custom_doctypes.add(doctype) docperms.append( frappe.docperm_fixture( doctype, role_name, perm["actions"], if_owner=if_owner, ) ) docperms.sort(key=lambda d: (d["role"], d["parent"], d["permlevel"])) # --- 3. Manifeste de traçabilité ----------------------------------------- manifest = { "generated_from": "rbac_50_roles.json", "source_version": contract["version"], "cible_rbac_roles": contract["cible_rbac_roles"], "counts": { "role": len(role_fixtures), "custom_docperm": len(docperms), "doctypes_uniques": len({d["parent"] for d in docperms}), }, # DocTypes DTP non natifs : à CRÉER + confirmer sur le VPS avant import # des DocPerms qui les ciblent (SPEC §6/§7). Jamais supposés existants. "custom_doctypes_a_confirmer": sorted(custom_doctypes), } return { "manifest": manifest, "role": role_fixtures, "custom_docperm": docperms, }