gigafibre-fsm/erpnext/setup_flow_templates.py
louispaulb 41d9b5f316 feat: flow editor, Gemini QR scanner with offline queue, dispatch planning v2
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>
2026-04-22 10:44:17 -04:00

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.")