""" seed_flow_templates.py — Seed initial Flow Templates (system-owned). Migrates the 4 hardcoded project templates (fiber_install, phone_service, move_service, repair_service) from apps/ops/src/config/project-templates.js into Flow Template docs (is_system=1). Also seeds: - residential_onboarding : runs on_contract_signed, ties install + reminders - quotation_follow_up : runs on_quotation_created, sends reminders Run (inside backend container): 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 seed_flow_templates import seed_all; seed_all()"' Idempotent: skips templates that already exist by template_name. """ import json import frappe # Flow definition schema: # { # "version": 1, # "trigger": { "event": "...", "condition": "" }, # "variables": {...}, # flow-level defaults # "steps": [ , ... ] # } # # Step schema: # { # "id": "step_xxx", # stable ID # "kind": "dispatch_job"|"issue"|"notify"|"webhook"|"erp_update"|"wait"|"condition"|"subscription_activate", # "label": "...", # human-readable # "parent_id": null | "step_yyy", # "branch": null | "yes" | "no" | custom, # "depends_on": ["step_xxx"], # array of step IDs; empty = run at flow start # "trigger": { # when to execute this step # "type": "on_flow_start" | "on_prev_complete" | "after_delay" | "on_date" | "on_webhook" | "manual", # "delay_hours": 24, # for after_delay # "delay_days": 7, # for after_delay # "at": "2026-05-01T09:00", # for on_date # }, # "payload": { ... } # kind-specific fields (see below) # } # # Payloads by kind: # dispatch_job: { subject, job_type, priority, duration_h, assigned_group, # on_open_webhook, on_close_webhook, merge_key } # issue: { subject, description, priority, raised_by, issue_type } # notify: { channel: "sms"|"email", to, template_id, subject, body } # webhook: { url, method: "POST", headers, body_template } # erp_update: { doctype, docname_ref, fields: {field: value} } # wait: {} (uses trigger.delay_hours/delay_days) # condition: { field, op: "==|!=|<|>|<=|>=|in|not_in", value } # Children on branch "yes" / "no" # subscription_activate: { subscription_ref } # --------------------------------------------------------------------------- # Helper builders (keep seed JSON readable) # --------------------------------------------------------------------------- def _dispatch_step(sid, label, subject, job_type, priority, duration_h, group, depends_on=None, merge_key=None, open_wh="", close_wh=""): return { "id": sid, "kind": "dispatch_job", "label": label, "parent_id": None, "branch": None, "depends_on": depends_on or [], "trigger": {"type": "on_prev_complete" if depends_on else "on_flow_start"}, "payload": { "subject": subject, "job_type": job_type, "priority": priority, "duration_h": duration_h, "assigned_group": group, "merge_key": merge_key or sid, "on_open_webhook": open_wh, "on_close_webhook": close_wh, }, } def _notify_step(sid, label, channel, template_id, depends_on=None, trigger_type="on_prev_complete", delay_hours=None): trig = {"type": trigger_type} if delay_hours: trig["delay_hours"] = delay_hours return { "id": sid, "kind": "notify", "label": label, "parent_id": None, "branch": None, "depends_on": depends_on or [], "trigger": trig, "payload": { "channel": channel, "to": "{{customer.primary_phone}}" if channel == "sms" else "{{customer.email_id}}", "template_id": template_id, }, } def _issue_step(sid, label, subject, depends_on=None): return { "id": sid, "kind": "issue", "label": label, "parent_id": None, "branch": None, "depends_on": depends_on or [], "trigger": {"type": "on_prev_complete" if depends_on else "on_flow_start"}, "payload": { "subject": subject, "priority": "Medium", "issue_type": "Suivi", }, } def _wait_step(sid, label, delay_hours=None, delay_days=None, depends_on=None): trig = {"type": "after_delay"} if delay_hours: trig["delay_hours"] = delay_hours if delay_days: trig["delay_days"] = delay_days return { "id": sid, "kind": "wait", "label": label, "parent_id": None, "branch": None, "depends_on": depends_on or [], "trigger": trig, "payload": {}, } # --------------------------------------------------------------------------- # Seed definitions # --------------------------------------------------------------------------- def _tpl_fiber_install(): return { "template_name": "Installation fibre résidentielle", "category": "Internet", "applies_to": "Service Contract", "icon": "cable", "description": "Vérification pré-install, installation, activation, test de débit", "is_system": 1, "trigger_event": "manual", "flow_definition": { "version": 1, "trigger": {"event": "manual", "condition": ""}, "variables": {}, "steps": [ _dispatch_step("s1", "Vérification pré-installation", "Vérification pré-installation (éligibilité & OLT)", "Autre", "medium", 0.5, "Admin", merge_key="fiber_pre_check"), _dispatch_step("s2", "Installation fibre", "Installation fibre chez le client", "Installation", "high", 3, "Tech Targo", depends_on=["s1"], merge_key="fiber_install_visit"), _dispatch_step("s3", "Activation & config ONT", "Activation du service & configuration ONT", "Installation", "high", 0.5, "Admin", depends_on=["s2"], merge_key="fiber_activation"), _dispatch_step("s4", "Test de débit", "Test de débit & validation client", "Dépannage", "medium", 0.5, "Tech Targo", depends_on=["s3"], merge_key="fiber_speed_test"), ], }, } def _tpl_phone_service(): return { "template_name": "Service téléphonique résidentiel", "category": "Téléphonie", "applies_to": "Service Contract", "icon": "phone_in_talk", "description": "Importation du numéro, installation fibre, portage du numéro", "is_system": 1, "trigger_event": "manual", "flow_definition": { "version": 1, "trigger": {"event": "manual", "condition": ""}, "variables": {}, "steps": [ _dispatch_step("s1", "Importer numéro", "Importer le numéro de téléphone", "Autre", "medium", 0.5, "Admin", merge_key="port_phone_request"), _dispatch_step("s2", "Installation fibre", "Installation fibre chez le client", "Installation", "high", 3, "Tech Targo", depends_on=["s1"], merge_key="fiber_install_visit"), _dispatch_step("s3", "Portage numéro", "Portage du numéro vers Gigafibre", "Autre", "medium", 0.5, "Admin", depends_on=["s2"], merge_key="port_phone_execute"), _dispatch_step("s4", "Test téléphonie", "Validation et test du service téléphonique", "Dépannage", "medium", 0.5, "Tech Targo", depends_on=["s3"], merge_key="phone_service_test"), ], }, } def _tpl_move_service(): return { "template_name": "Déménagement de service", "category": "Déménagement", "applies_to": "Service Contract", "icon": "local_shipping", "description": "Retrait ancien site, installation nouveau site, transfert abonnement", "is_system": 1, "trigger_event": "manual", "flow_definition": { "version": 1, "trigger": {"event": "manual", "condition": ""}, "variables": {}, "steps": [ _dispatch_step("s1", "Préparation", "Préparation déménagement (vérifier éligibilité nouveau site)", "Autre", "medium", 0.5, "Admin", merge_key="move_prep"), _dispatch_step("s2", "Retrait ancien site", "Retrait équipement ancien site", "Retrait", "medium", 1, "Tech Targo", depends_on=["s1"], merge_key="move_removal"), _dispatch_step("s3", "Installation nouveau site", "Installation au nouveau site", "Installation", "high", 3, "Tech Targo", depends_on=["s2"], merge_key="fiber_install_visit"), _dispatch_step("s4", "Transfert abonnement", "Transfert abonnement & mise à jour adresse", "Autre", "medium", 0.5, "Admin", depends_on=["s3"], merge_key="move_transfer"), ], }, } def _tpl_repair_service(): return { "template_name": "Réparation service client", "category": "Dépannage", "applies_to": "Issue", "icon": "build", "description": "Diagnostic, intervention terrain, validation", "is_system": 1, "trigger_event": "on_issue_opened", "flow_definition": { "version": 1, "trigger": {"event": "on_issue_opened", "condition": ""}, "variables": {}, "steps": [ _dispatch_step("s1", "Diagnostic à distance", "Diagnostic à distance", "Dépannage", "high", 0.5, "Admin", merge_key="repair_diag"), _dispatch_step("s2", "Intervention terrain", "Intervention terrain", "Réparation", "high", 2, "Tech Targo", depends_on=["s1"], merge_key="repair_visit"), _dispatch_step("s3", "Validation client", "Validation & suivi client", "Dépannage", "medium", 0.5, "Admin", depends_on=["s2"], merge_key="repair_validate"), ], }, } def _tpl_residential_onboarding(): """ Flow complet déclenché à la signature du Service Contract résidentiel. Ferme la boucle CTR-00004 : installation + rappels + satisfaction + renouvellement. """ return { "template_name": "Onboarding résidentiel (post-signature)", "category": "Onboarding", "applies_to": "Service Contract", "icon": "rocket_launch", "description": "Flow complet après signature: SMS bienvenue, installation, test, survey, renouvellement", "is_system": 1, "trigger_event": "on_contract_signed", "trigger_condition": "contract.contract_type == 'Résidentiel'", "flow_definition": { "version": 1, "trigger": { "event": "on_contract_signed", "condition": "contract.contract_type == 'Résidentiel'", }, "variables": {}, "steps": [ # Immédiat — accueil _notify_step("welcome_sms", "SMS de bienvenue", channel="sms", template_id="welcome_residential", trigger_type="on_flow_start"), _issue_step("onboarding_ticket", "Ticket suivi onboarding", "Onboarding résidentiel — suivi client"), # Installation (dépend de rien, démarre immédiatement) _dispatch_step("install", "Installation fibre", "Installation fibre chez le client", "Installation", "high", 3, "Tech Targo", merge_key="fiber_install_visit", open_wh="", close_wh=""), # Activation abonnement après installation complétée { "id": "activate_sub", "kind": "subscription_activate", "label": "Activer l'abonnement", "parent_id": None, "branch": None, "depends_on": ["install"], "trigger": {"type": "on_prev_complete"}, "payload": {"subscription_ref": "{{contract.subscription}}"}, }, # SMS confirmation service actif _notify_step("active_sms", "SMS service actif", channel="sms", template_id="service_activated", depends_on=["activate_sub"]), # Survey satisfaction 24h après install _wait_step("wait_24h", "Attendre 24h après install", delay_hours=24, depends_on=["install"]), _notify_step("survey_sms", "SMS sondage satisfaction", channel="sms", template_id="satisfaction_survey", depends_on=["wait_24h"]), # Rappel renouvellement à 11 mois (anticipation du renouvel à 12 mois) _wait_step("wait_11m", "Attendre 11 mois", delay_days=330, depends_on=["activate_sub"]), _issue_step("renewal_ticket", "Ticket renouvellement", "Rappel: contrat arrive à échéance dans 1 mois", depends_on=["wait_11m"]), ], }, } def _tpl_quotation_follow_up(): """Relance douce 48h après envoi de quotation non acceptée.""" return { "template_name": "Relance quotation non signée", "category": "Custom", "applies_to": "Quotation", "icon": "mail", "description": "SMS de rappel 48h après envoi d'un devis résidentiel", "is_system": 1, "trigger_event": "on_quotation_created", "trigger_condition": "quotation.status == 'Submitted'", "flow_definition": { "version": 1, "trigger": { "event": "on_quotation_created", "condition": "quotation.status == 'Submitted'", }, "variables": {}, "steps": [ _wait_step("wait_48h", "Attendre 48h", delay_hours=48), _notify_step("reminder_sms", "SMS rappel devis", channel="sms", template_id="quotation_reminder", depends_on=["wait_48h"]), ], }, } # --------------------------------------------------------------------------- # Seeder # --------------------------------------------------------------------------- SEEDS = [ _tpl_fiber_install, _tpl_phone_service, _tpl_move_service, _tpl_repair_service, _tpl_residential_onboarding, _tpl_quotation_follow_up, ] def _count_steps(flow_def): return len(flow_def.get("steps", [])) def _upsert(tpl): name = tpl["template_name"] exists = frappe.db.exists("Flow Template", {"template_name": name}) flow_def_json = json.dumps(tpl["flow_definition"], ensure_ascii=False, indent=2) step_count = _count_steps(tpl["flow_definition"]) if exists: doc = frappe.get_doc("Flow Template", exists) # Only update is_system=1 seeds automatically if not doc.is_system: print(f" SKIP {name!r} — exists as user-edited (is_system=0)") return doc.update({ "category": tpl["category"], "applies_to": tpl["applies_to"], "icon": tpl.get("icon", "account_tree"), "description": tpl.get("description", ""), "trigger_event": tpl.get("trigger_event", "manual"), "trigger_condition": tpl.get("trigger_condition", ""), "flow_definition": flow_def_json, "step_count": step_count, "version": (doc.version or 0) + 1, }) doc.save(ignore_permissions=True) print(f" UPDATED {name!r} -> v{doc.version}, {step_count} steps") return doc = frappe.get_doc({ "doctype": "Flow Template", "template_name": name, "category": tpl["category"], "applies_to": tpl["applies_to"], "icon": tpl.get("icon", "account_tree"), "is_active": 1, "is_system": tpl.get("is_system", 0), "version": 1, "description": tpl.get("description", ""), "trigger_event": tpl.get("trigger_event", "manual"), "trigger_condition": tpl.get("trigger_condition", ""), "flow_definition": flow_def_json, "step_count": step_count, }) doc.insert(ignore_permissions=True) print(f" CREATED {name!r} -> {doc.name}, {step_count} steps") def seed_all(): for factory in SEEDS: tpl = factory() _upsert(tpl) frappe.db.commit() print(f"\n[OK] Seeded {len(SEEDS)} Flow Templates.") if __name__ == "__main__": seed_all()