Major additions accumulated over 9 days — single commit per request. Flow editor (new): - Generic visual editor for step trees, usable by project wizard + agent flows - PROJECT_KINDS / AGENT_KINDS catalogs decouple UI from domain - Drag-and-drop reorder via vuedraggable with scope isolation per peer group - Chain-aware depends_on rewrite on reorder (sequential only — DAGs preserved) - Variable picker with per-applies_to catalog (Customer / Quotation / Service Contract / Issue / Subscription), insert + copy-clipboard modes - trigger_condition helper with domain-specific JSONLogic examples - Global FlowEditorDialog mounted once in MainLayout, Odoo inline pattern - Server: targo-hub flow-runtime.js, flow-api.js, flow-templates.js - ERPNext: Flow Template/Run doctypes, scheduler, 5 seeded system templates - depends_on chips resolve to step labels instead of opaque "s4" ids QR/OCR scanner (field app): - Camera capture → Gemini Vision via targo-hub with 8s timeout - IndexedDB offline queue retries photos when signal returns - Watcher merges late-arriving scan results into the live UI Dispatch: - Planning mode (draft → publish) with offer pool for unassigned jobs - Shared presets, recurrence selector, suggested-slots dialog - PublishScheduleModal, unassign confirmation Ops app: - ClientDetailPage composables extraction (useClientData, useDeviceStatus, useWifiDiagnostic, useModemDiagnostic) - Project wizard: shared detail sections, wizard catalog/publish composables - Address pricing composable + pricing-mock data - Settings redesign hosting flow templates Targo-hub: - Contract acceptance (JWT residential + DocuSeal commercial tracks) - Referral system - Modem-bridge diagnostic normalizer - Device extractors consolidated Migration scripts: - Invoice/quote print format setup, Jinja rendering - Additional import + fix scripts (reversals, dates, customers, payments) Docs: - Consolidated: old scattered MDs → HANDOFF, ARCHITECTURE, DATA_AND_FLOWS, FLOW_EDITOR_ARCHITECTURE, BILLING_AND_PAYMENTS, CPE_MANAGEMENT, APP_DESIGN_GUIDELINES - Archived legacy wizard PHP for reference - STATUS snapshots for 2026-04-18/19 Cleanup: - Removed ~40 generated PDFs/HTMLs (invoice_preview*, rendered_jinja*) - .gitignore now covers invoice preview output + nested .DS_Store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
236 lines
12 KiB
Python
236 lines
12 KiB
Python
"""
|
|
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 <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.")
|