""" Import active services as Service Subscriptions and enrich Customer records with full account details (phone, email, stripe, PPA, notes). Also adds custom fields to Service Subscription for RADIUS/legacy data. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_services_and_enrich_customers.py """ import frappe import pymysql import os import time import hashlib from datetime import datetime from decimal import Decimal 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) T_TOTAL = time.time() now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") conn = pymysql.connect( host="10.100.80.100", user="facturation", password="*******", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) # ═══════════════════════════════════════════════════════════════ # PHASE 1: Add custom fields to Service Subscription # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 1: ADD CUSTOM FIELDS") print("="*60) custom_fields = [ ("legacy_service_id", "Legacy Service ID", "Int", 30), ("radius_user", "RADIUS User", "Data", 31), ("radius_password", "RADIUS Password", "Data", 32), ("product_sku", "Product SKU", "Data", 33), ("device", "Device", "Link", 34), # options = Service Equipment ] for fname, label, ftype, idx in custom_fields: exists = frappe.db.sql(""" SELECT name FROM "tabDocField" WHERE parent = 'Service Subscription' AND fieldname = %s """, (fname,)) if not exists: opts = "Service Equipment" if fname == "device" else None frappe.db.sql(""" INSERT INTO "tabDocField" ( name, creation, modified, modified_by, owner, docstatus, idx, parent, parentfield, parenttype, fieldname, label, fieldtype, options, reqd, read_only, hidden ) VALUES ( %(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, %(idx)s, 'Service Subscription', 'fields', 'DocType', %(fname)s, %(label)s, %(ftype)s, %(opts)s, 0, 0, 0 ) """, { "name": "ss-{}-{}".format(fname, int(time.time())), "now": now_str, "idx": idx, "fname": fname, "label": label, "ftype": ftype, "opts": opts, }) # Add column to table col_type = "bigint" if ftype == "Int" else "varchar(140)" try: frappe.db.sql('ALTER TABLE "tabService Subscription" ADD COLUMN {} {}'.format(fname, col_type)) except Exception as e: if "already exists" not in str(e).lower(): raise frappe.db.commit() print(" Added field: {}".format(fname)) else: print(" Field exists: {}".format(fname)) # ═══════════════════════════════════════════════════════════════ # PHASE 2: Build lookup maps # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 2: BUILD LOOKUP MAPS") print("="*60) # delivery_id → Service Location name loc_map = {} rows = frappe.db.sql(""" SELECT name, legacy_delivery_id FROM "tabService Location" WHERE legacy_delivery_id IS NOT NULL AND legacy_delivery_id > 0 """, as_dict=True) for r in rows: loc_map[r["legacy_delivery_id"]] = r["name"] print("Location map: {} entries".format(len(loc_map))) # account_id → Customer name cust_map = {} rows = frappe.db.sql(""" SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL AND legacy_account_id > 0 """, as_dict=True) for r in rows: cust_map[r["legacy_account_id"]] = r["name"] print("Customer map: {} entries".format(len(cust_map))) # device_id → Service Equipment name dev_map = {} rows = frappe.db.sql(""" SELECT name, legacy_device_id FROM "tabService Equipment" WHERE legacy_device_id IS NOT NULL AND legacy_device_id > 0 """, as_dict=True) for r in rows: dev_map[r["legacy_device_id"]] = r["name"] print("Device map: {} entries".format(len(dev_map))) # delivery_id → account_id with conn.cursor() as cur: cur.execute("SELECT id, account_id FROM delivery") del_acct = {} for r in cur.fetchall(): del_acct[r["id"]] = r["account_id"] print("Delivery→account map: {} entries".format(len(del_acct))) # ═══════════════════════════════════════════════════════════════ # PHASE 3: Import services as Service Subscriptions # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 3: IMPORT SERVICE SUBSCRIPTIONS") print("="*60) # Clear existing existing_subs = frappe.db.sql('SELECT COUNT(*) FROM "tabService Subscription"')[0][0] if existing_subs > 0: frappe.db.sql('DELETE FROM "tabService Subscription"') frappe.db.commit() print("Deleted {} existing subscriptions".format(existing_subs)) # Product category → service_category mapping # Legacy: Mensualités fibre, Installation fibre, Mensualités sans fil, Téléphonie, # Mensualités télévision, Installation télé, Adresse IP Fixe, Hébergement, etc. PROD_CAT_MAP = { 4: "Internet", # Mensualités sans fil 32: "Internet", # Mensualités fibre 8: "Internet", # Installation et équipement internet sans fil 26: "Internet", # Installation et équipement fibre 29: "Internet", # Equipement internet fibre 7: "Internet", # Equipement internet sans fil 23: "Internet", # Internet camping 17: "Internet", # Adresse IP Fixe 16: "Internet", # Téléchargement supplémentaire 21: "Internet", # Location point à point 33: "IPTV", # Mensualités télévision 34: "IPTV", # Installation et équipement télé 9: "VoIP", # Téléphonie 15: "Hébergement", # Hébergement 11: "Hébergement", # Nom de domaine 30: "Hébergement", # Location espace cloud 10: "Autre", # Site internet 13: "Autre", # Location d'espace } # payment_recurrence → billing_cycle RECUR_MAP = { 1: "Mensuel", 2: "Mensuel", 3: "Trimestriel", 4: "Annuel", } with conn.cursor() as cur: cur.execute(""" SELECT s.id, s.delivery_id, s.device_id, s.product_id, s.status, s.comment, s.payment_recurrence, s.hijack, s.hijack_price, s.hijack_download_speed, s.hijack_upload_speed, s.date_orig, s.date_suspended, s.date_next_invoice, s.date_end_contract, s.forfait_internet, s.radius_user, s.radius_pwd, p.sku, p.price, p.download_speed, p.upload_speed, p.category as prod_cat FROM service s LEFT JOIN product p ON s.product_id = p.id WHERE s.status = 1 ORDER BY s.id """) services = cur.fetchall() # Also get product names cur.execute(""" SELECT pt.product_id, pt.name as prod_name FROM product_translate pt WHERE pt.language_id = 'francais' """) prod_names = {} for r in cur.fetchall(): prod_names[r["product_id"]] = r["prod_name"] print("Active services to import: {}".format(len(services))) inserted = 0 no_location = 0 for svc in services: # Resolve customer via delivery → account customer = None service_location = loc_map.get(svc["delivery_id"]) if svc["delivery_id"] else None if not service_location: no_location += 1 continue # Skip services without a service location (required field) if svc["delivery_id"] and svc["delivery_id"] in del_acct: customer = cust_map.get(del_acct[svc["delivery_id"]]) if not customer: no_location += 1 continue # Skip services without a customer (required field) # Service category prod_cat = svc["prod_cat"] or 0 service_category = PROD_CAT_MAP.get(prod_cat, "Autre") # Plan name plan_name = prod_names.get(svc["product_id"], svc["sku"] or "Unknown") # Speed (legacy stores in kbps, convert to Mbps) speed_down = 0 speed_up = 0 if svc["hijack"] and svc["hijack_download_speed"]: speed_down = int(svc["hijack_download_speed"]) // 1024 speed_up = int(svc["hijack_upload_speed"] or 0) // 1024 elif svc["download_speed"]: speed_down = int(svc["download_speed"]) // 1024 speed_up = int(svc["upload_speed"] or 0) // 1024 # Price price = float(svc["hijack_price"] or 0) if svc["hijack"] else float(svc["price"] or 0) # Billing cycle billing_cycle = RECUR_MAP.get(svc["payment_recurrence"], "Mensuel") # Start date start_date = None if svc["date_orig"]: try: start_date = datetime.fromtimestamp(int(svc["date_orig"])).strftime("%Y-%m-%d") except (ValueError, OSError): pass if not start_date: start_date = "2020-01-01" # End date (contract) end_date = None if svc["date_end_contract"]: try: end_date = datetime.fromtimestamp(int(svc["date_end_contract"])).strftime("%Y-%m-%d") except (ValueError, OSError): pass # Device link device = dev_map.get(svc["device_id"]) if svc["device_id"] else None # Generate name sub_name = "SUB-{}".format(svc["id"]) inserted += 1 frappe.db.sql(""" INSERT INTO "tabService Subscription" ( name, creation, modified, modified_by, owner, docstatus, idx, customer, service_location, status, service_category, plan_name, speed_down, speed_up, monthly_price, billing_cycle, start_date, end_date, notes, legacy_service_id, radius_user, radius_password, product_sku, device ) VALUES ( %(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0, %(customer)s, %(service_location)s, 'Actif', %(service_category)s, %(plan_name)s, %(speed_down)s, %(speed_up)s, %(monthly_price)s, %(billing_cycle)s, %(start_date)s, %(end_date)s, %(notes)s, %(legacy_service_id)s, %(radius_user)s, %(radius_password)s, %(product_sku)s, %(device)s ) """, { "name": sub_name, "now": now_str, "customer": customer, "service_location": service_location, "service_category": service_category, "plan_name": plan_name, "speed_down": speed_down, "speed_up": speed_up, "monthly_price": price, "billing_cycle": billing_cycle, "start_date": start_date, "end_date": end_date, "notes": svc["comment"] if svc["comment"] else None, "legacy_service_id": svc["id"], "radius_user": svc["radius_user"], "radius_password": svc["radius_pwd"], "product_sku": svc["sku"], "device": device, }) if inserted % 5000 == 0: frappe.db.commit() print(" Inserted {}...".format(inserted)) frappe.db.commit() print("Inserted {} Service Subscriptions ({} skipped - no location/customer)".format(inserted, no_location)) # ═══════════════════════════════════════════════════════════════ # PHASE 4: Enrich Customer records with account details # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 4: ENRICH CUSTOMER RECORDS") print("="*60) with conn.cursor() as cur: cur.execute(""" SELECT id, customer_id, email, email_autre, tel_home, cell, stripe_id, ppa, ppa_name, ppa_code, ppa_branch, ppa_account, notes_client, language_id, commercial, vip, mauvais_payeur, invoice_delivery, company, contact FROM account WHERE status = 1 """) accounts = cur.fetchall() print("Active accounts to enrich: {}".format(len(accounts))) updated = 0 for acct in accounts: cust_name = cust_map.get(acct["id"]) if not cust_name: continue # Build update fields updates = {} sets = [] # Email if acct["email"] and acct["email"].strip(): updates["email_id"] = acct["email"].strip() sets.append('email_id = %(email_id)s') # Mobile cell = (acct["cell"] or "").strip() if not cell: cell = (acct["tel_home"] or "").strip() if cell: updates["mobile_no"] = cell sets.append('mobile_no = %(mobile_no)s') # Stripe ID if acct["stripe_id"] and acct["stripe_id"].strip(): updates["stripe_id"] = acct["stripe_id"].strip() sets.append('stripe_id = %(stripe_id)s') # PPA enabled if acct["ppa"]: updates["ppa_enabled"] = 1 sets.append('ppa_enabled = %(ppa_enabled)s') # Language lang = "fr" if acct["language_id"] == "francais" else "en" updates["language"] = lang sets.append('language = %(language)s') # Customer details (notes + contact) details_parts = [] if acct["notes_client"] and acct["notes_client"].strip(): details_parts.append(acct["notes_client"].strip()) if acct["contact"] and acct["contact"].strip(): details_parts.append("Contact: " + acct["contact"].strip()) if acct["vip"]: details_parts.append("[VIP]") if acct["mauvais_payeur"]: details_parts.append("[MAUVAIS PAYEUR]") if acct["commercial"]: details_parts.append("[COMMERCIAL]") if details_parts: updates["customer_details"] = "\n".join(details_parts) sets.append('customer_details = %(customer_details)s') if sets: updates["cust_name"] = cust_name frappe.db.sql( 'UPDATE "tabCustomer" SET {} WHERE name = %(cust_name)s'.format(", ".join(sets)), updates ) updated += 1 if updated % 2000 == 0 and updated > 0: frappe.db.commit() print(" Updated {}...".format(updated)) frappe.db.commit() print("Updated {} Customer records".format(updated)) conn.close() # ═══════════════════════════════════════════════════════════════ # PHASE 5: VERIFY # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 5: VERIFY") print("="*60) total_subs = frappe.db.sql('SELECT COUNT(*) FROM "tabService Subscription"')[0][0] by_cat = frappe.db.sql(""" SELECT service_category, COUNT(*) as cnt FROM "tabService Subscription" GROUP BY service_category ORDER BY cnt DESC """, as_dict=True) print("Total Service Subscriptions: {}".format(total_subs)) for c in by_cat: print(" {}: {}".format(c["service_category"], c["cnt"])) with_device = frappe.db.sql('SELECT COUNT(*) FROM "tabService Subscription" WHERE device IS NOT NULL')[0][0] with_radius = frappe.db.sql('SELECT COUNT(*) FROM "tabService Subscription" WHERE radius_user IS NOT NULL')[0][0] print("\nWith device link: {}".format(with_device)) print("With RADIUS credentials: {}".format(with_radius)) # Customer enrichment cust_with_email = frappe.db.sql("SELECT COUNT(*) FROM \"tabCustomer\" WHERE email_id IS NOT NULL")[0][0] cust_with_phone = frappe.db.sql("SELECT COUNT(*) FROM \"tabCustomer\" WHERE mobile_no IS NOT NULL")[0][0] cust_with_stripe = frappe.db.sql("SELECT COUNT(*) FROM \"tabCustomer\" WHERE stripe_id IS NOT NULL")[0][0] cust_with_ppa = frappe.db.sql('SELECT COUNT(*) FROM "tabCustomer" WHERE ppa_enabled = 1')[0][0] print("\nCustomer enrichment:") print(" With email: {}".format(cust_with_email)) print(" With phone: {}".format(cust_with_phone)) print(" With Stripe: {}".format(cust_with_stripe)) print(" With PPA: {}".format(cust_with_ppa)) # Sample subscriptions samples = frappe.db.sql(""" SELECT name, customer, service_category, plan_name, speed_down, speed_up, monthly_price, radius_user, device, legacy_service_id FROM "tabService Subscription" LIMIT 10 """, as_dict=True) print("\nSample subscriptions:") for s in samples: print(" {} cat={} plan={} {}↓/{}↑ ${} radius={} dev={}".format( s["name"], s["service_category"], (s["plan_name"] or "")[:30], s["speed_down"], s["speed_up"], s["monthly_price"], s["radius_user"] or "-", s["device"] or "-")) elapsed = time.time() - T_TOTAL print("\n" + "="*60) print("DONE in {:.1f}s".format(elapsed)) print("="*60) frappe.clear_cache()