""" Add churn/cancel reason tracking fields to Customer and Issue doctypes. Enables AI-powered retention intelligence and win-back campaigns. Run: docker cp add_churn_fields.py erpnext-backend-1:/tmp/ docker exec erpnext-backend-1 bench --site erp.gigafibre.ca execute "exec(open('/tmp/add_churn_fields.py').read())" """ import frappe def add_custom_fields(doctype, fields): count = 0 for f in fields: fieldname = f["fieldname"] exists = frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": fieldname}) if exists: print(f" EXISTS: {doctype}.{fieldname}") continue doc = frappe.get_doc({"doctype": "Custom Field", "dt": doctype, **f}) doc.insert(ignore_permissions=True) count += 1 print(f" ADDED: {doctype}.{fieldname}") return count # ── Customer: Churn/Retention fields ───────────────────────────────────────── customer_fields = [ # Section break for churn tracking {"fieldname": "churn_section", "fieldtype": "Section Break", "label": "Rétention / Désabonnement", "insert_after": "notes_internal", "collapsible": 1}, # Cancel status {"fieldname": "churn_status", "fieldtype": "Select", "label": "Statut rétention", "options": "\nActif\nÀ risque\nDésabonné\nRécupéré", "insert_after": "churn_section"}, {"fieldname": "churn_cb1", "fieldtype": "Column Break", "insert_after": "churn_status"}, # Cancel date {"fieldname": "cancel_date", "fieldtype": "Date", "label": "Date désabonnement", "insert_after": "churn_cb1"}, # Cancel reason {"fieldname": "cancel_reason", "fieldtype": "Select", "label": "Raison désabonnement", "options": "\nCompétiteur - Promotion\nCompétiteur - Prix\nCompétiteur - Service\nPrix trop élevé\nQualité WiFi\nPannes fréquentes\nService client\nDéménagement hors zone\nDéménagement dans zone\nDécès\nAutre", "insert_after": "cancel_date"}, {"fieldname": "churn_cb2", "fieldtype": "Column Break", "insert_after": "cancel_reason"}, # Competitor info {"fieldname": "cancel_competitor", "fieldtype": "Data", "label": "Compétiteur", "description": "Bell, Vidéotron, Fizz, etc.", "insert_after": "churn_cb2", "depends_on": "eval:['Compétiteur - Promotion','Compétiteur - Prix','Compétiteur - Service'].includes(doc.cancel_reason)"}, {"fieldname": "cancel_competitor_offer", "fieldtype": "Small Text", "label": "Offre compétiteur", "description": "Ex: 6 mois gratuit, 49.99$/mois fibre 1Gbps", "insert_after": "cancel_competitor", "depends_on": "eval:['Compétiteur - Promotion','Compétiteur - Prix','Compétiteur - Service'].includes(doc.cancel_reason)"}, # Notes {"fieldname": "cancel_notes", "fieldtype": "Small Text", "label": "Notes rétention", "description": "Contexte: ce que le client a dit, offre proposée, etc.", "insert_after": "cancel_competitor_offer"}, # Win-back tracking {"fieldname": "winback_section", "fieldtype": "Section Break", "label": "Récupération", "insert_after": "cancel_notes", "collapsible": 1, "depends_on": "eval:doc.churn_status=='Désabonné'"}, {"fieldname": "winback_attempts", "fieldtype": "Int", "label": "Tentatives récupération", "default": "0", "insert_after": "winback_section"}, {"fieldname": "winback_last_date", "fieldtype": "Date", "label": "Dernière tentative", "insert_after": "winback_attempts"}, {"fieldname": "winback_cb", "fieldtype": "Column Break", "insert_after": "winback_last_date"}, {"fieldname": "winback_date", "fieldtype": "Date", "label": "Date récupération", "insert_after": "winback_cb", "depends_on": "eval:doc.churn_status=='Récupéré'"}, {"fieldname": "winback_offer", "fieldtype": "Small Text", "label": "Offre accordée", "description": "Offre qui a convaincu le client de revenir", "insert_after": "winback_date", "depends_on": "eval:doc.churn_status=='Récupéré'"}, # Risk score (AI-populated) {"fieldname": "churn_risk_score", "fieldtype": "Int", "label": "Score risque désabonnement", "description": "0-100, calculé par l'IA basé sur le comportement", "insert_after": "winback_offer", "read_only": 1}, ] # ── Issue: Enhanced categorization ─────────────────────────────────────────── issue_fields = [ # Customer link (if not already present) {"fieldname": "customer", "fieldtype": "Link", "label": "Client", "options": "Customer", "insert_after": "naming_series"}, # Outage tracking {"fieldname": "outage_section", "fieldtype": "Section Break", "label": "Panne / Outage", "insert_after": "resolution_details", "collapsible": 1}, {"fieldname": "outage_type", "fieldtype": "Select", "label": "Type de panne", "options": "\nPanne isolée\nPanne secteur\nCoupure fibre\nPanne OLT\nPanne backbone\nPanne courant", "insert_after": "outage_section"}, {"fieldname": "outage_cb", "fieldtype": "Column Break", "insert_after": "outage_type"}, {"fieldname": "affected_count", "fieldtype": "Int", "label": "Clients affectés", "insert_after": "outage_cb"}, {"fieldname": "olt_name", "fieldtype": "Data", "label": "OLT", "insert_after": "affected_count"}, {"fieldname": "olt_port", "fieldtype": "Data", "label": "Port OLT", "insert_after": "olt_name"}, # AI diagnosis {"fieldname": "ai_diagnosis", "fieldtype": "Small Text", "label": "Diagnostic IA", "insert_after": "olt_port", "read_only": 1}, # Cancel reason (for support tickets about cancellation) {"fieldname": "cancel_intent", "fieldtype": "Check", "label": "Intention de désabonnement", "insert_after": "ai_diagnosis"}, ] # ── Apply ──────────────────────────────────────────────────────────────────── print("Adding churn/retention fields to Customer...") c1 = add_custom_fields("Customer", customer_fields) print("\nAdding outage/categorization fields to Issue...") c2 = add_custom_fields("Issue", issue_fields) frappe.db.commit() print(f"\nDone: {c1} Customer fields + {c2} Issue fields added")