Files
Claude Code DTP Worker 34f202af40 [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>
2026-07-30 05:36:27 +00:00

119 lines
4.0 KiB
Python

"""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,
}