""" add_missing_custom_fields.py — Add all missing custom fields to ERPNext doctypes. Covers the complete field gap identified in LEGACY-FIELD-GAP-ANALYSIS.md. Adds ~73 custom fields across Customer, Service Subscription, Service Equipment, Service Location, and Dispatch Job. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/add_missing_custom_fields.py """ import frappe import os os.chdir("/home/frappe/frappe-bench/sites") frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() frappe.local.flags.ignore_permissions = True print("Connected:", frappe.local.site) def add_custom_fields(doctype, fields): """Add a list of custom fields to a doctype, skipping existing ones.""" count = 0 for f in fields: fieldname = f["fieldname"] exists = frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": fieldname}) if exists: continue try: doc = frappe.get_doc({ "doctype": "Custom Field", "dt": doctype, **f, }) doc.insert(ignore_permissions=True) count += 1 print(" + {}.{}".format(doctype, fieldname)) except Exception as e: print(" ! {}.{} — ERROR: {}".format(doctype, fieldname, str(e)[:100])) frappe.db.commit() return count # ═══════════════════════════════════════════════════════════════════════════ # 1. CUSTOMER — PPA, Stripe, Termination, VIP, Portal, Marketing # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("CUSTOMER — Adding missing fields") print("=" * 60) customer_fields = [ # ── PPA (Pre-Authorized Payment) Section ── {"fieldname": "ppa_section", "label": "Paiement pré-autorisé (PPA)", "fieldtype": "Section Break", "insert_after": "notes_internal", "collapsible": 1}, {"fieldname": "ppa_enabled", "label": "PPA activé", "fieldtype": "Check", "insert_after": "ppa_section"}, {"fieldname": "ppa_name", "label": "Nom du payeur", "fieldtype": "Data", "insert_after": "ppa_enabled", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_code", "label": "Transit bancaire", "fieldtype": "Data", "insert_after": "ppa_name", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_branch", "label": "Succursale", "fieldtype": "Data", "insert_after": "ppa_code", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_col_break", "fieldtype": "Column Break", "insert_after": "ppa_branch"}, {"fieldname": "ppa_account", "label": "Numéro de compte", "fieldtype": "Data", "insert_after": "ppa_col_break", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_amount", "label": "Montant PPA", "fieldtype": "Currency", "insert_after": "ppa_account", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_amount_buffer", "label": "Montant tampon", "fieldtype": "Currency", "insert_after": "ppa_amount", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_fixed", "label": "Montant fixe", "fieldtype": "Check", "insert_after": "ppa_amount_buffer", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_cc", "label": "PPA par carte de crédit", "fieldtype": "Check", "insert_after": "ppa_fixed", "depends_on": "eval:doc.ppa_enabled"}, {"fieldname": "ppa_all_invoice", "label": "Appliquer à toutes les factures", "fieldtype": "Check", "insert_after": "ppa_cc", "depends_on": "eval:doc.ppa_enabled"}, # ── Stripe Section ── {"fieldname": "stripe_section", "label": "Stripe", "fieldtype": "Section Break", "insert_after": "ppa_all_invoice", "collapsible": 1}, {"fieldname": "stripe_customer_id", "label": "Stripe Customer ID", "fieldtype": "Data", "insert_after": "stripe_section", "description": "cus_XXXXX"}, {"fieldname": "stripe_ppa_enabled", "label": "Paiement auto Stripe", "fieldtype": "Check", "insert_after": "stripe_customer_id"}, # ── Flags ── {"fieldname": "is_vip", "label": "Client VIP", "fieldtype": "Check", "insert_after": "is_bad_payer"}, {"fieldname": "is_land_owner", "label": "Propriétaire terrain", "fieldtype": "Check", "insert_after": "is_vip"}, {"fieldname": "marketing_optin", "label": "Accepte marketing", "fieldtype": "Check", "insert_after": "is_land_owner"}, {"fieldname": "call_contact", "label": "Contact téléphonique", "fieldtype": "Check", "insert_after": "marketing_optin"}, # ── Termination Section ── {"fieldname": "termination_section", "label": "Résiliation", "fieldtype": "Section Break", "insert_after": "stripe_ppa_enabled", "collapsible": 1}, {"fieldname": "terminate_date", "label": "Date résiliation", "fieldtype": "Date", "insert_after": "termination_section"}, {"fieldname": "terminate_reason", "label": "Raison", "fieldtype": "Small Text", "insert_after": "terminate_date"}, {"fieldname": "terminate_col", "fieldtype": "Column Break", "insert_after": "terminate_reason"}, {"fieldname": "terminate_cie", "label": "Compétiteur", "fieldtype": "Data", "insert_after": "terminate_col"}, {"fieldname": "terminate_note", "label": "Notes résiliation", "fieldtype": "Small Text", "insert_after": "terminate_cie"}, # ── Client-visible notes ── {"fieldname": "notes_client", "label": "Notes client (visible)", "fieldtype": "Small Text", "insert_after": "notes_internal", "description": "Notes visibles par le client"}, # ── Portal ── {"fieldname": "portal_section", "label": "Portail client (legacy)", "fieldtype": "Section Break", "insert_after": "terminate_note", "collapsible": 1}, {"fieldname": "portal_username", "label": "Nom d'utilisateur", "fieldtype": "Data", "insert_after": "portal_section"}, {"fieldname": "portal_password_hash", "label": "Hash mot de passe (MD5)", "fieldtype": "Data", "insert_after": "portal_username", "read_only": 1}, # ── Misc ── {"fieldname": "middle_name", "label": "Deuxième prénom", "fieldtype": "Data", "insert_after": "customer_name"}, {"fieldname": "salutation_legacy", "label": "Titre (legacy)", "fieldtype": "Data", "insert_after": "middle_name"}, {"fieldname": "search_keyword", "label": "Mot-clé recherche", "fieldtype": "Data", "insert_after": "portal_password_hash"}, ] add_custom_fields("Customer", customer_fields) # ═══════════════════════════════════════════════════════════════════════════ # 2. SERVICE SUBSCRIPTION — Custom pricing, dates, quotas, IP # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("SERVICE SUBSCRIPTION — Adding missing fields") print("=" * 60) subscription_fields = [ # ── Custom pricing ── {"fieldname": "custom_pricing_section", "label": "Tarification personnalisée", "fieldtype": "Section Break", "insert_after": "notes", "collapsible": 1}, {"fieldname": "is_custom_pricing", "label": "Prix personnalisé (hijack)", "fieldtype": "Check", "insert_after": "custom_pricing_section"}, {"fieldname": "custom_pricing_desc", "label": "Description prix custom", "fieldtype": "Data", "insert_after": "is_custom_pricing", "depends_on": "eval:doc.is_custom_pricing"}, {"fieldname": "custom_col", "fieldtype": "Column Break", "insert_after": "custom_pricing_desc"}, {"fieldname": "quota_day_gb", "label": "Quota jour (Go)", "fieldtype": "Float", "insert_after": "custom_col"}, {"fieldname": "quota_night_gb", "label": "Quota nuit (Go)", "fieldtype": "Float", "insert_after": "quota_day_gb"}, # ── Extended dates ── {"fieldname": "dates_extra_section", "label": "Dates supplémentaires", "fieldtype": "Section Break", "insert_after": "quota_night_gb", "collapsible": 1}, {"fieldname": "date_suspended", "label": "Date suspension", "fieldtype": "Date", "insert_after": "dates_extra_section"}, {"fieldname": "active_until", "label": "Actif jusqu'au", "fieldtype": "Date", "insert_after": "date_suspended"}, {"fieldname": "dates_extra_col", "fieldtype": "Column Break", "insert_after": "active_until"}, {"fieldname": "next_invoice_date", "label": "Prochaine facturation", "fieldtype": "Date", "insert_after": "dates_extra_col"}, # ── Network ── {"fieldname": "network_section", "label": "Réseau", "fieldtype": "Section Break", "insert_after": "next_invoice_date", "collapsible": 1}, {"fieldname": "static_ip", "label": "IP fixe", "fieldtype": "Data", "insert_after": "network_section"}, {"fieldname": "forfait_internet", "label": "Forfait internet", "fieldtype": "Check", "insert_after": "static_ip"}, {"fieldname": "radius_consumption", "label": "Consommation RADIUS", "fieldtype": "Data", "insert_after": "forfait_internet"}, ] add_custom_fields("Service Subscription", subscription_fields) # ═══════════════════════════════════════════════════════════════════════════ # 3. SERVICE EQUIPMENT — Management, Provisioning, ACS, IPTV # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("SERVICE EQUIPMENT — Adding missing fields") print("=" * 60) # Note: Service Equipment is a custom doctype, so we add fields directly # to the DocType definition (not as Custom Field). We use ALTER TABLE. # But if it was created via setup_fsm_doctypes.py, we can still use Custom Field pattern. equipment_fields = [ # ── Management Access ── {"fieldname": "mgmt_section", "label": "Accès de gestion", "fieldtype": "Section Break", "insert_after": "login_password", "collapsible": 1}, {"fieldname": "manage_url", "label": "URL de gestion", "fieldtype": "Data", "insert_after": "mgmt_section", "description": "http://172.17.x.x:8080 ou IP simple"}, {"fieldname": "manage_port", "label": "Port gestion", "fieldtype": "Int", "insert_after": "manage_url"}, {"fieldname": "manage_protocol", "label": "Protocole gestion", "fieldtype": "Select", "options": "\nHTTP\nHTTPS\nSSH\nTelnet\nSNMP", "insert_after": "manage_port"}, {"fieldname": "mgmt_col", "fieldtype": "Column Break", "insert_after": "manage_protocol"}, {"fieldname": "cli_ip", "label": "IP CLI", "fieldtype": "Data", "insert_after": "mgmt_col"}, {"fieldname": "cli_port", "label": "Port CLI", "fieldtype": "Int", "insert_after": "cli_ip"}, {"fieldname": "cli_protocol", "label": "Protocole CLI", "fieldtype": "Select", "options": "\nSSH\nTelnet", "insert_after": "cli_port"}, # ── Device hierarchy ── {"fieldname": "parent_device", "label": "Équipement parent", "fieldtype": "Link", "options": "Service Equipment", "insert_after": "cli_protocol", "description": "ONT parent pour routeurs/décodeurs, ou OLT pour ONTs"}, {"fieldname": "legacy_category", "label": "Catégorie legacy", "fieldtype": "Data", "insert_after": "parent_device", "read_only": 1}, {"fieldname": "device_name_legacy", "label": "Nom legacy", "fieldtype": "Data", "insert_after": "legacy_category", "read_only": 1}, # ── WiFi Provisioning ── {"fieldname": "wifi_section", "label": "WiFi (provisionnement)", "fieldtype": "Section Break", "insert_after": "device_name_legacy", "collapsible": 1}, {"fieldname": "wifi_ssid", "label": "SSID WiFi", "fieldtype": "Data", "insert_after": "wifi_section"}, {"fieldname": "wifi_password", "label": "Mot de passe WiFi", "fieldtype": "Password", "insert_after": "wifi_ssid"}, {"fieldname": "wifi_col", "fieldtype": "Column Break", "insert_after": "wifi_password"}, {"fieldname": "wifi_ssid_5g", "label": "SSID WiFi 5GHz", "fieldtype": "Data", "insert_after": "wifi_col", "description": "Si différent du 2.4GHz"}, {"fieldname": "wifi_enabled", "label": "WiFi activé", "fieldtype": "Check", "insert_after": "wifi_ssid_5g", "default": "1"}, # ── VoIP Provisioning ── {"fieldname": "voip_section", "label": "VoIP (provisionnement)", "fieldtype": "Section Break", "insert_after": "wifi_enabled", "collapsible": 1}, {"fieldname": "sip_username", "label": "SIP Username", "fieldtype": "Data", "insert_after": "voip_section"}, {"fieldname": "sip_password", "label": "SIP Password", "fieldtype": "Password", "insert_after": "sip_username"}, {"fieldname": "voip_col", "fieldtype": "Column Break", "insert_after": "sip_password"}, {"fieldname": "sip_line", "label": "Ligne SIP", "fieldtype": "Int", "insert_after": "voip_col", "default": "1"}, # ── GPON / ACS ── {"fieldname": "gpon_section", "label": "GPON / ACS", "fieldtype": "Section Break", "insert_after": "sip_line", "collapsible": 1}, {"fieldname": "gpon_serial", "label": "GPON Serial (physique)", "fieldtype": "Data", "insert_after": "gpon_section", "description": "RCMG/TPLG sur l'étiquette"}, {"fieldname": "cwmp_serial", "label": "CWMP Serial (GenieACS)", "fieldtype": "Data", "insert_after": "gpon_serial", "description": "Serial interne TR-069"}, {"fieldname": "acs_device_id", "label": "ACS Device ID", "fieldtype": "Data", "insert_after": "cwmp_serial", "description": "ID complet GenieACS/Oktopus"}, {"fieldname": "gpon_col", "fieldtype": "Column Break", "insert_after": "acs_device_id"}, {"fieldname": "fibre_line_profile", "label": "Line Profile ID", "fieldtype": "Data", "insert_after": "gpon_col"}, {"fieldname": "fibre_service_profile", "label": "Service Profile ID", "fieldtype": "Data", "insert_after": "fibre_line_profile"}, {"fieldname": "acs_last_inform", "label": "Dernier inform ACS", "fieldtype": "Datetime", "insert_after": "fibre_service_profile", "read_only": 1}, {"fieldname": "acs_online", "label": "En ligne (ACS)", "fieldtype": "Check", "insert_after": "acs_last_inform", "read_only": 1}, {"fieldname": "wan_ip", "label": "IP WAN", "fieldtype": "Data", "insert_after": "acs_online", "read_only": 1}, # ── IPTV ── {"fieldname": "iptv_section", "label": "IPTV", "fieldtype": "Section Break", "insert_after": "wan_ip", "collapsible": 1}, {"fieldname": "iptv_subscription_id", "label": "Ministra/IPTV Sub ID", "fieldtype": "Data", "insert_after": "iptv_section"}, {"fieldname": "mac_addresses_json", "label": "MACs par interface (JSON)", "fieldtype": "Long Text", "insert_after": "iptv_subscription_id", "read_only": 1, "description": '{"eth0":"AA:BB:CC:DD:EE:FF","eth1":"..."}'}, ] # For Service Equipment (custom doctype), we need to check if these should be # Custom Fields or direct DocType field additions. # Since it was created as custom=1, Custom Fields work. add_custom_fields("Service Equipment", equipment_fields) # ═══════════════════════════════════════════════════════════════════════════ # 4. SERVICE LOCATION — Fibre infrastructure, VLANs, legacy IDs # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("SERVICE LOCATION — Adding missing fields") print("=" * 60) location_fields = [ # ── OLT / Fibre detail ── {"fieldname": "fibre_detail_section", "label": "Détails fibre", "fieldtype": "Section Break", "insert_after": "network_notes", "collapsible": 1}, {"fieldname": "legacy_fibre_id", "label": "Fibre ID (legacy)", "fieldtype": "Int", "insert_after": "fibre_detail_section"}, {"fieldname": "ont_serial", "label": "ONT Serial", "fieldtype": "Data", "insert_after": "legacy_fibre_id"}, {"fieldname": "olt_ip", "label": "OLT IP", "fieldtype": "Data", "insert_after": "ont_serial"}, {"fieldname": "olt_name", "label": "Nom OLT", "fieldtype": "Data", "insert_after": "olt_ip"}, {"fieldname": "fibre_col1", "fieldtype": "Column Break", "insert_after": "olt_name"}, {"fieldname": "ont_id", "label": "ONT ID", "fieldtype": "Int", "insert_after": "fibre_col1"}, {"fieldname": "terrain_type", "label": "Type terrain", "fieldtype": "Data", "insert_after": "ont_id"}, {"fieldname": "fibre_distance_m", "label": "Distance fibre (m)", "fieldtype": "Float", "insert_after": "terrain_type"}, {"fieldname": "fibre_spans", "label": "Nombre de portées", "fieldtype": "Int", "insert_after": "fibre_distance_m"}, {"fieldname": "install_time_estimate", "label": "Temps d'installation estimé", "fieldtype": "Data", "insert_after": "fibre_spans"}, # ── VLANs (individual, replacing concatenated network_id) ── {"fieldname": "vlan_section", "label": "VLANs", "fieldtype": "Section Break", "insert_after": "install_time_estimate", "collapsible": 1}, {"fieldname": "vlan_manage", "label": "VLAN Gestion", "fieldtype": "Int", "insert_after": "vlan_section"}, {"fieldname": "vlan_internet", "label": "VLAN Internet", "fieldtype": "Int", "insert_after": "vlan_manage"}, {"fieldname": "vlan_col", "fieldtype": "Column Break", "insert_after": "vlan_internet"}, {"fieldname": "vlan_telephone", "label": "VLAN Téléphone", "fieldtype": "Int", "insert_after": "vlan_col"}, {"fieldname": "vlan_tv", "label": "VLAN Télévision", "fieldtype": "Int", "insert_after": "vlan_telephone"}, # ── Building / Apartment ── {"fieldname": "building_section", "label": "Immeuble", "fieldtype": "Section Break", "insert_after": "vlan_tv", "collapsible": 1}, {"fieldname": "address_line_2", "label": "Adresse ligne 2", "fieldtype": "Data", "insert_after": "building_section"}, {"fieldname": "apartment_number", "label": "Numéro d'appartement", "fieldtype": "Data", "insert_after": "address_line_2"}, {"fieldname": "is_apartment", "label": "Est un appartement", "fieldtype": "Check", "insert_after": "apartment_number"}, {"fieldname": "apartment_building_id", "label": "ID immeuble", "fieldtype": "Data", "insert_after": "is_apartment"}, {"fieldname": "box_not_installed", "label": "Boîtier pas installé", "fieldtype": "Check", "insert_after": "apartment_building_id"}, # ── Legacy service IDs (for cross-reference) ── {"fieldname": "legacy_ids_section", "label": "IDs legacy (référence)", "fieldtype": "Section Break", "insert_after": "box_not_installed", "collapsible": 1, "hidden": 1}, {"fieldname": "manage_service_id", "label": "Service gestion ID", "fieldtype": "Int", "insert_after": "legacy_ids_section"}, {"fieldname": "internet_service_id", "label": "Service internet ID", "fieldtype": "Int", "insert_after": "manage_service_id"}, {"fieldname": "telephone_service_id", "label": "Service téléphone ID", "fieldtype": "Int", "insert_after": "internet_service_id"}, {"fieldname": "tv_service_id", "label": "Service TV ID", "fieldtype": "Int", "insert_after": "telephone_service_id"}, {"fieldname": "placemarks_id", "label": "Placemarks ID", "fieldtype": "Data", "insert_after": "tv_service_id"}, {"fieldname": "delivery_notes", "label": "Notes adresse", "fieldtype": "Small Text", "insert_after": "access_notes"}, ] add_custom_fields("Service Location", location_fields) # ═══════════════════════════════════════════════════════════════════════════ # 5. DISPATCH JOB — Second tech + time tracking # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("DISPATCH JOB — Adding missing fields") print("=" * 60) dispatch_fields = [ {"fieldname": "second_technician", "label": "Deuxième technicien", "fieldtype": "Data", "insert_after": "actual_end"}, {"fieldname": "tech1_arrival", "label": "Arrivée tech 1", "fieldtype": "Datetime", "insert_after": "second_technician"}, {"fieldname": "tech1_departure", "label": "Départ tech 1", "fieldtype": "Datetime", "insert_after": "tech1_arrival"}, {"fieldname": "tech2_arrival", "label": "Arrivée tech 2", "fieldtype": "Datetime", "insert_after": "tech1_departure"}, {"fieldname": "tech2_departure", "label": "Départ tech 2", "fieldtype": "Datetime", "insert_after": "tech2_arrival"}, ] add_custom_fields("Dispatch Job", dispatch_fields) # ═══════════════════════════════════════════════════════════════════════════ # DONE # ═══════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("ALL CUSTOM FIELDS ADDED") print("=" * 60) print("Next steps:") print(" 1. Run migrate_missing_data.py to populate the new fields") print(" 2. Run migrate_provisioning_data.py for WiFi/VoIP from GenieACS")