d815c7ab63
Transforme rbac_50_roles.json en fixtures Frappe/ERPNext v15 natives, prêtes à appliquer via bench (VPS · agent ERPNext). Réalise le prochain incrément annoncé au §7 de RBAC_50_ROLES_SPEC.md. Zéro pip, zéro VPS, sortie déterministe. - fixturelib/frappe.py : modèle natif (15 flags DocPerm, mapping 1:1 des verbes RBAC, if_owner⇔scope "own"). fixturelib/builder.py : bundle déterministe. - rbac_fixtures_gen.py : CLI build/validate (refuse d'écrire si invariant KO). - fixtures.schema.json : contrat de sortie (validateur maison Publiciste réutilisé). - 11 tests unittest : schéma+oracle, 50 rôles, séparation des pouvoirs, round-trip fidèle au contrat, déterminisme. Job CI rbac-fixtures-tests au gate. - Anti-invention #6 : 100% dérivé du contrat, flags non pilotés à 0, DocTypes custom signalés « à confirmer VPS ». Vérif : 11/11 verts + gate CI local vert (exit 0) + régression 60 tests OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""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,
|
|
}
|