""" setup_flow_templates.py — Create doctypes for the Flow Editor (project/service orchestration engine). Three doctypes: - Flow Template : editable template library (admins manage in SettingsPage) - Flow Run : per-execution state attached to a context doc - Flow Step Pending : scheduled steps waiting for a time/condition trigger Run inside the bench container: docker compose exec backend bench --site execute setup_flow_templates.create_all Or via docker exec python path (avoids IPython cell-split gotcha): docker exec -u frappe erpnext-backend-1 bash -c \\ 'cd /home/frappe/frappe-bench/sites && \\ /home/frappe/frappe-bench/env/bin/python -c \\ "import frappe; frappe.init(site=\\"erp.gigafibre.ca\\"); frappe.connect(); \\ from setup_flow_templates import create_all; create_all()"' """ import frappe def create_all(): _create_flow_template() _create_flow_run() _create_flow_step_pending() frappe.db.commit() print("[OK] Flow Editor doctypes created.") # ----------------------------------------------------------------------------- # Flow Template — editable template library # ----------------------------------------------------------------------------- def _create_flow_template(): if frappe.db.exists("DocType", "Flow Template"): print(" Flow Template already exists — skipping.") return doc = frappe.get_doc({ "doctype": "DocType", "name": "Flow Template", "module": "Dispatch", "custom": 1, "autoname": "FT-.#####", "track_changes": 1, "fields": [ # -- Identification --------------------------------------------- {"fieldname": "template_name", "fieldtype": "Data", "label": "Nom du template", "reqd": 1, "in_list_view": 1, "unique": 1, "description": "Nom court et descriptif (ex: Installation fibre résidentielle)"}, {"fieldname": "category", "fieldtype": "Select", "label": "Catégorie", "options": "Internet\nTéléphonie\nTélévision\nDéménagement\nRéparation\nOnboarding\nRenouvellement\nChurn\nDépannage\nCustom", "default": "Custom", "in_list_view": 1, "reqd": 1}, {"fieldname": "applies_to", "fieldtype": "Select", "label": "S'applique à", "options": "Quotation\nService Contract\nIssue\nCustomer\nSubscription", "default": "Service Contract", "in_list_view": 1, "reqd": 1, "description": "Type de document qui déclenche le flow"}, {"fieldname": "col_id1", "fieldtype": "Column Break"}, {"fieldname": "icon", "fieldtype": "Data", "label": "Icône (Material)", "default": "account_tree", "description": "Nom d'icône Quasar/Material (ex: cable, phone_in_talk)"}, {"fieldname": "is_active", "fieldtype": "Check", "label": "Actif", "default": "1", "in_list_view": 1}, {"fieldname": "is_system", "fieldtype": "Check", "label": "Template système", "default": "0", "read_only": 1, "description": "Templates seedés — ne pas supprimer"}, {"fieldname": "version", "fieldtype": "Int", "label": "Version", "default": "1", "read_only": 1, "description": "Incrémenté à chaque sauvegarde"}, # -- Description ------------------------------------------------ {"fieldname": "sec_desc", "fieldtype": "Section Break", "label": "Description"}, {"fieldname": "description", "fieldtype": "Small Text", "label": "Description", "description": "Phrase courte expliquant ce que fait le flow"}, # -- Déclencheur (trigger) -------------------------------------- {"fieldname": "sec_trigger", "fieldtype": "Section Break", "label": "Déclencheur automatique", "description": "Quand ce flow doit-il se déclencher automatiquement?"}, {"fieldname": "trigger_event", "fieldtype": "Select", "label": "Évènement", "options": "\nmanual\non_quotation_created\non_quotation_accepted\non_contract_signed\non_payment_received\non_subscription_active\non_issue_opened\non_customer_created\non_dispatch_completed", "default": "manual", "description": "manual = démarré à la main depuis ProjectWizard ou un bouton"}, {"fieldname": "col_trigger", "fieldtype": "Column Break"}, {"fieldname": "trigger_condition", "fieldtype": "Small Text", "label": "Condition (expression)", "description": "Expression JS/Python (ex: contract.contract_type == 'Résidentiel'). Vide = toujours."}, # -- Définition du flow (JSON) --------------------------------- {"fieldname": "sec_def", "fieldtype": "Section Break", "label": "Définition du flow"}, {"fieldname": "flow_definition", "fieldtype": "Long Text", "label": "Flow definition (JSON)", "reqd": 1, "description": "Arbre complet des étapes. Éditer via SettingsPage > Flows."}, {"fieldname": "step_count", "fieldtype": "Int", "label": "Nombre d'étapes", "read_only": 1, "in_list_view": 1, "description": "Calculé à la sauvegarde"}, # -- Métadonnées ------------------------------------------------ {"fieldname": "sec_meta", "fieldtype": "Section Break", "label": "Métadonnées", "collapsible": 1}, {"fieldname": "tags", "fieldtype": "Data", "label": "Tags (CSV)", "description": "Étiquettes libres (ex: résidentiel,fibre,nouveau-client)"}, {"fieldname": "notes", "fieldtype": "Small Text", "label": "Notes internes"}, ], "permissions": [ {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1}, {"role": "Dispatch User", "read": 1, "write": 1, "create": 1}, {"role": "Sales User", "read": 1}, ], }) doc.insert(ignore_permissions=True) print(" [+] Flow Template doctype created.") # ----------------------------------------------------------------------------- # Flow Run — one execution instance per context doc # ----------------------------------------------------------------------------- def _create_flow_run(): if frappe.db.exists("DocType", "Flow Run"): print(" Flow Run already exists — skipping.") return doc = frappe.get_doc({ "doctype": "DocType", "name": "Flow Run", "module": "Dispatch", "custom": 1, "autoname": "FR-.######", "track_changes": 1, "fields": [ # -- Identification --------------------------------------------- {"fieldname": "flow_template", "fieldtype": "Link", "label": "Flow Template", "options": "Flow Template", "reqd": 1, "in_list_view": 1}, {"fieldname": "template_version", "fieldtype": "Int", "label": "Version du template au démarrage", "read_only": 1, "description": "Figée au démarrage — une modif ultérieure du template n'affecte pas ce run"}, {"fieldname": "col_run1", "fieldtype": "Column Break"}, {"fieldname": "status", "fieldtype": "Select", "label": "Statut", "options": "pending\nrunning\nwaiting\ncompleted\nfailed\ncancelled", "default": "pending", "reqd": 1, "in_list_view": 1}, {"fieldname": "trigger_event", "fieldtype": "Data", "label": "Déclencheur", "description": "Évènement qui a démarré ce run"}, # -- Contexte (doc qui a déclenché) ---------------------------- {"fieldname": "sec_ctx", "fieldtype": "Section Break", "label": "Contexte"}, {"fieldname": "context_doctype", "fieldtype": "Link", "label": "DocType de contexte", "options": "DocType", "in_list_view": 1}, {"fieldname": "context_docname", "fieldtype": "Dynamic Link", "label": "Document", "options": "context_doctype", "in_list_view": 1}, {"fieldname": "col_ctx", "fieldtype": "Column Break"}, {"fieldname": "customer", "fieldtype": "Link", "label": "Client", "options": "Customer", "in_list_view": 1}, {"fieldname": "variables", "fieldtype": "Long Text", "label": "Variables (JSON)", "description": "Variables accumulées pendant l'exécution (pour les conditions/templates)"}, # -- État d'exécution ------------------------------------------ {"fieldname": "sec_state", "fieldtype": "Section Break", "label": "Exécution"}, {"fieldname": "step_state", "fieldtype": "Long Text", "label": "État des étapes (JSON)", "description": "{stepId: {status, started_at, completed_at, result, error}}"}, {"fieldname": "current_step_ids", "fieldtype": "Small Text", "label": "Étapes en cours (CSV)", "description": "IDs des étapes actuellement running/waiting"}, {"fieldname": "col_state", "fieldtype": "Column Break"}, {"fieldname": "started_at", "fieldtype": "Datetime", "label": "Démarré le", "read_only": 1}, {"fieldname": "completed_at", "fieldtype": "Datetime", "label": "Terminé le", "read_only": 1}, {"fieldname": "last_error", "fieldtype": "Small Text", "label": "Dernière erreur", "read_only": 1}, ], "permissions": [ {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1}, {"role": "Dispatch User", "read": 1, "write": 1}, ], }) doc.insert(ignore_permissions=True) print(" [+] Flow Run doctype created.") # ----------------------------------------------------------------------------- # Flow Step Pending — scheduled/waiting steps picked up by cron # ----------------------------------------------------------------------------- def _create_flow_step_pending(): if frappe.db.exists("DocType", "Flow Step Pending"): print(" Flow Step Pending already exists — skipping.") return doc = frappe.get_doc({ "doctype": "DocType", "name": "Flow Step Pending", "module": "Dispatch", "custom": 1, "autoname": "FSP-.######", "track_changes": 1, "fields": [ {"fieldname": "flow_run", "fieldtype": "Link", "label": "Flow Run", "options": "Flow Run", "reqd": 1, "in_list_view": 1}, {"fieldname": "step_id", "fieldtype": "Data", "label": "Step ID", "reqd": 1, "in_list_view": 1, "description": "ID de l'étape dans le flow_definition du template"}, {"fieldname": "col_sp1", "fieldtype": "Column Break"}, {"fieldname": "status", "fieldtype": "Select", "label": "Statut", "options": "waiting\nexecuting\ndone\nfailed\ncancelled", "default": "waiting", "reqd": 1, "in_list_view": 1}, {"fieldname": "trigger_at", "fieldtype": "Datetime", "label": "Déclencher à", "reqd": 1, "in_list_view": 1, "description": "Le scheduler exécute l'étape quand NOW() >= trigger_at"}, {"fieldname": "sec_ctx", "fieldtype": "Section Break", "label": "Contexte"}, {"fieldname": "context_snapshot", "fieldtype": "Long Text", "label": "Snapshot contexte (JSON)", "description": "Variables du flow_run figées au moment où l'étape a été mise en attente"}, {"fieldname": "sec_exec", "fieldtype": "Section Break", "label": "Exécution"}, {"fieldname": "executed_at", "fieldtype": "Datetime", "label": "Exécuté le", "read_only": 1}, {"fieldname": "last_error", "fieldtype": "Small Text", "label": "Erreur", "read_only": 1}, {"fieldname": "retry_count", "fieldtype": "Int", "label": "Nb tentatives", "default": "0", "read_only": 1}, ], "permissions": [ {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1}, {"role": "Dispatch User", "read": 1, "write": 1}, ], }) doc.insert(ignore_permissions=True) print(" [+] Flow Step Pending doctype created.")