"""Modele Frappe/ERPNext v15 : structure native du fixture `DocType`. Separe la CONNAISSANCE FRAPPE (types de champ legitimes, drapeaux de docfield, modele DocPerm, enveloppe du document DocType) de l'assemblage metier (`builder.py`). Contrainte #1 « ERPNext natif = priorite absolue » : on produit le DocType standard du moteur Frappe, porteur des dossiers d'incitation CONFOTUR. Aucun acces VPS : chaque fonction renvoie un dict serialisable que l'agent ONAPI/Legal importera via `bench` (hand-off · #8). Nota vs `crm/dossier_vente/dvlib/frappe.py` : ce module ajoute la permission native `report` (le role `legal-directeur` la porte cote RBAC). Les deux modules restent volontairement independants — chaque livrable possede sa connaissance Frappe, on n'ouvre que ce que le contrat emploie reellement. """ from __future__ import annotations from typing import Any # Types de champ Frappe employes par ce DocType. Hors de cet ensemble = invention # -> refusee par `docfield()`. VALID_FIELDTYPES: frozenset[str] = frozenset( { "Section Break", "Column Break", "Data", "Select", "Link", "Date", "Datetime", "Small Text", "Text", "Check", } ) # Types « de mise en page » : ni requis, ni porteurs de donnee. LAYOUT_FIELDTYPES: frozenset[str] = frozenset({"Section Break", "Column Break"}) # Permissions natives du child table `DocPerm` d'un DocType Frappe (v15). # `report` est inclus (porte par le role legal-directeur cote RBAC). VALID_PERMS: frozenset[str] = frozenset( {"read", "write", "create", "submit", "cancel", "amend", "delete", "print", "email", "report", "import", "export", "share"} ) # Ordre de serialisation stable des drapeaux DocPerm (diff propre). _PERM_ORDER = ("read", "write", "create", "submit", "cancel", "amend", "delete", "print", "email", "report", "import", "export", "share") # Drapeaux de docfield connus (defaut 0) — on ne serialise que ceux a 1. _DOCFIELD_FLAGS = ("reqd", "read_only", "in_list_view", "in_standard_filter", "hidden", "bold") def docfield( fieldname: str, fieldtype: str, *, label: str | None = None, options: str | None = None, default: str | None = None, flags: dict[str, int] | None = None, ) -> dict[str, Any]: """Une ligne de la table enfant `fields` d'un DocType.""" if fieldtype not in VALID_FIELDTYPES: raise ValueError(f"Fieldtype non natif Frappe pour {fieldname!r} : {fieldtype!r}") row: dict[str, Any] = {"fieldname": fieldname, "fieldtype": fieldtype} if label is not None: row["label"] = label if options is not None: row["options"] = options if default is not None: row["default"] = default flags = flags or {} for f in _DOCFIELD_FLAGS: v = int(flags.get(f, 0)) if v not in (0, 1): raise ValueError(f"Drapeau {f} de {fieldname!r} non booleen : {v!r}") if v: row[f] = 1 return row def section_break(fieldname: str, label: str) -> dict[str, Any]: return {"fieldname": fieldname, "fieldtype": "Section Break", "label": label} def permission_row(role: str, perms: set[str], *, permlevel: int = 0) -> dict[str, Any]: """Ligne `permissions` (DocPerm integre) d'un DocType. Emet uniquement les drapeaux a 1 (defaut sur = refus), dans l'ordre stable `_PERM_ORDER` — diff reproductible. """ unknown = perms - VALID_PERMS if unknown: raise ValueError(f"Permission(s) non native(s) pour {role!r} : {sorted(unknown)}") row: dict[str, Any] = {"role": role, "permlevel": permlevel} for p in _PERM_ORDER: if p in perms: row[p] = 1 return row def naming_series_field(series: str) -> dict[str, Any]: """Le champ Select `naming_series` requis quand `autoname = "naming_series:"`. Frappe nomme le document depuis ce champ ; ses `options` portent le motif de serie. Rendu `read_only` (serie imposee, pas de saisie libre). """ return docfield( "naming_series", "Select", label="Serie de nommage", options=series, default=series, flags={"read_only": 1}, ) def doctype_doc( *, name: str, module: str, is_submittable: int, title_field: str, search_fields: str, track_changes: int, track_seen: int, fields: list[dict], permissions: list[dict], ) -> dict[str, Any]: """Le document `DocType` complet (fixture custom pret pour `bench`).""" return { "doctype": "DocType", "name": name, "module": module, "custom": 1, "is_submittable": 1 if is_submittable else 0, "naming_rule": "By \"Naming Series\" field", "autoname": "naming_series:", "title_field": title_field, "search_fields": search_fields, "track_changes": 1 if track_changes else 0, "track_seen": 1 if track_seen else 0, "editable_grid": 1, "engine": "InnoDB", "field_order": [f["fieldname"] for f in fields], "fields": fields, "permissions": permissions, }