"""Modèle Frappe/ERPNext v15 : structure native des fixtures Workflow. Sépare la CONNAISSANCE FRAPPE (noms de DocTypes, champs de table enfant, valeurs autorisées) de l'assemblage métier (`builder.py`). Contrainte #1 « ERPNext natif = priorité absolue » : on n'invente aucun moteur de workflow externe — on produit les DocTypes standard du moteur Workflow de Frappe. DocTypes produits (references Frappe) : - `Workflow` → le graphe (document_type + states[] + transitions[]). - `Workflow Document State`→ table enfant `states` : un état + son doc_status + le rôle qui peut éditer (`allow_edit`). - `Workflow Transition` → table enfant `transitions` : arête état→action→état, gardée par un rôle (`allowed`) + `allow_self_approval`. - `Workflow State` → DocType maître (nom d'état + `style` d'affichage). - `Workflow Action Master` → DocType maître (nom d'action). Aucun accès VPS : chaque fonction renvoie un dict sérialisable. """ from __future__ import annotations from typing import Any # doc_status Frappe natifs. Le long d'une transition, doc_status est MONOTONE # (0→0 mise à jour, 0→1 soumission, 1→1 mise à jour, 1→2 annulation) — jamais # 0→2 ni décroissant. Le CLI `validate` fait respecter cet invariant. DOCSTATUS_DRAFT = "0" DOCSTATUS_SUBMITTED = "1" DOCSTATUS_CANCELLED = "2" VALID_DOCSTATUS: frozenset[str] = frozenset( {DOCSTATUS_DRAFT, DOCSTATUS_SUBMITTED, DOCSTATUS_CANCELLED} ) # Styles d'affichage natifs du DocType `Workflow State` (badge de couleur desk). # Une valeur hors de cet ensemble = invention → refusée. VALID_STYLES: frozenset[str] = frozenset( {"", "Primary", "Info", "Success", "Warning", "Danger", "Inverse"} ) def workflow_state_master(state: str, style: str) -> dict[str, Any]: """DocType maître `Workflow State` (nom + style de badge).""" if style not in VALID_STYLES: raise ValueError(f"Style non natif Frappe pour {state!r} : {style!r}") return {"doctype": "Workflow State", "name": state, "workflow_state_name": state, "style": style} def workflow_action_master(action: str) -> dict[str, Any]: """DocType maître `Workflow Action Master` (nom d'action).""" return {"doctype": "Workflow Action Master", "name": action, "workflow_action_name": action} def document_state_row( state: str, doc_status: str, allow_edit: str, *, update_field: str, update_value: str, message: str, ) -> dict[str, Any]: """Ligne de la table enfant `states` d'un Workflow.""" if doc_status not in VALID_DOCSTATUS: raise ValueError(f"doc_status non natif pour {state!r} : {doc_status!r}") return { "state": state, "doc_status": doc_status, "allow_edit": allow_edit, "update_field": update_field, "update_value": update_value, "is_optional_state": 0, "message": message, } def transition_row( state: str, action: str, next_state: str, allowed: str, *, allow_self_approval: int, ) -> dict[str, Any]: """Ligne de la table enfant `transitions` d'un Workflow.""" return { "state": state, "action": action, "next_state": next_state, "allowed": allowed, "allow_self_approval": 1 if allow_self_approval else 0, "condition": None, } def workflow_doc( *, name: str, document_type: str, workflow_state_field: str, is_active: int, send_email_alert: int, override_status: int, states: list[dict], transitions: list[dict], ) -> dict[str, Any]: """Le document `Workflow` complet (states + transitions en tables enfant).""" return { "doctype": "Workflow", "name": name, "workflow_name": name, "document_type": document_type, "workflow_state_field": workflow_state_field, "is_active": 1 if is_active else 0, "send_email_alert": 1 if send_email_alert else 0, "override_status": 1 if override_status else 0, "states": states, "transitions": transitions, }