[DTP-Worker] Sprint 4 · Générateur workflow vente ERPNext (lead → CONFOTUR)
Contrat pipeline commercial CRM natif (lead → visite → devis → réservation → contrat → CONFOTUR) → fixtures Frappe/ERPNext v15 : Workflow (9 états / 11 transitions) + Workflow State + Workflow Action Master + MANIFEST. Rôles gardant états/transitions résolus depuis rbac_50_roles.json (réutilisation, zéro duplication · #6) : le contrat ne cite qu'un id de rôle, jamais un nom Frappe en dur. CLI build/validate refuse d'écrire si l'un des 9 invariants de graphe casse (monotonie doc_status, atteignabilité, séparation des pouvoirs sur les étapes engageant de l'argent / clôturant). 25 tests (stdlib pur + oracle jsonschema) · job CI crm-workflow-vente-tests ajouté au gate · 146 tests de régression verts au total. Application VPS (DocType porteur OTO Dossier Vente + bench migrate) = agent ERPNext Backend, hors périmètre worker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user