""" Rename hex-based document IDs to human-readable names: - Customer: CUST-{hex} → CUST-{legacy_account_id} - Service Location: LOC-{hex} → "{address_line}, {city}" or LOC-{legacy_delivery_id} - Service Equipment: EQ-{hex} → EQ-{legacy_device_id} Uses direct SQL for speed (frappe.rename_doc is too slow for 15k+ records). Updates all foreign key references. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/rename_to_readable_ids.py """ import frappe import os import time import re from datetime import datetime 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() def batch_rename(table, old_to_new, ref_tables, label): """Rename documents and update all foreign key references.""" if not old_to_new: print(" Nothing to rename for {}".format(label)) return print(" Renaming {} {} records...".format(len(old_to_new), label)) t0 = time.time() # Build temp mapping table for efficient bulk UPDATE # Process in batches to avoid memory issues batch_size = 2000 items = list(old_to_new.items()) for batch_start in range(0, len(items), batch_size): batch = items[batch_start:batch_start + batch_size] # Update main table name for old_name, new_name in batch: frappe.db.sql( 'UPDATE "{}" SET name = %s WHERE name = %s'.format(table), (new_name, old_name) ) # Update all foreign key references for ref_table, ref_col in ref_tables: for old_name, new_name in batch: frappe.db.sql( 'UPDATE "{}" SET {} = %s WHERE {} = %s'.format(ref_table, ref_col, ref_col), (new_name, old_name) ) frappe.db.commit() done = min(batch_start + batch_size, len(items)) print(" {}/{}...".format(done, len(items))) elapsed = time.time() - t0 print(" Done {} in {:.1f}s".format(label, elapsed)) # ═══════════════════════════════════════════════════════════════ # PHASE 1: RENAME CUSTOMERS # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 1: RENAME CUSTOMERS") print("="*60) customers = frappe.db.sql(""" SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL AND legacy_account_id > 0 ORDER BY legacy_account_id """, as_dict=True) cust_rename = {} seen_cust = set() for c in customers: new_name = "CUST-{}".format(c["legacy_account_id"]) if new_name == c["name"]: continue # already correct if new_name in seen_cust: new_name = "CUST-{}-b".format(c["legacy_account_id"]) seen_cust.add(new_name) cust_rename[c["name"]] = new_name print("Customers to rename: {} (of {})".format(len(cust_rename), len(customers))) # All tables with a 'customer' Link field pointing to Customer CUST_REFS = [ ("tabService Location", "customer"), ("tabService Subscription", "customer"), ("tabService Equipment", "customer"), ("tabDispatch Job", "customer"), ("tabIssue", "customer"), ("tabSales Invoice", "customer"), ("tabSales Order", "customer"), ("tabDelivery Note", "customer"), ("tabSerial No", "customer"), ("tabProject", "customer"), ("tabWarranty Claim", "customer"), ("tabMaintenance Visit", "customer"), ("tabMaintenance Schedule", "customer"), ("tabLoyalty Point Entry", "customer"), ("tabPOS Invoice", "customer"), ("tabPOS Invoice Reference", "customer"), ("tabMaterial Request", "customer"), ("tabTimesheet", "customer"), ("tabBlanket Order", "customer"), ("tabDunning", "customer"), ("tabInstallation Note", "customer"), ("tabDelivery Stop", "customer"), ("tabPricing Rule", "customer"), ("tabTax Rule", "customer"), ("tabCall Log", "customer"), ("tabProcess Statement Of Accounts Customer", "customer"), ] # Also update customer_name references in child tables where parent=customer name CUST_PARENT_REFS = [ ("tabHas Role", "parent"), ("tabDynamic Link", "link_name"), ] all_cust_refs = CUST_REFS + CUST_PARENT_REFS batch_rename("tabCustomer", cust_rename, all_cust_refs, "Customer") # ═══════════════════════════════════════════════════════════════ # PHASE 2: RENAME SERVICE LOCATIONS # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 2: RENAME SERVICE LOCATIONS") print("="*60) locations = frappe.db.sql(""" SELECT name, address_line, city, legacy_delivery_id FROM "tabService Location" WHERE legacy_delivery_id IS NOT NULL AND legacy_delivery_id > 0 ORDER BY legacy_delivery_id """, as_dict=True) loc_rename = {} seen_loc = set() for loc in locations: addr = (loc["address_line"] or "").strip() city = (loc["city"] or "").strip() if addr and city: # Clean up address for use as document name new_name = "{}, {}".format(addr, city) # Frappe name max is 140 chars, keep it reasonable if len(new_name) > 120: new_name = new_name[:120] # Remove chars that cause issues in URLs new_name = new_name.replace("/", "-").replace("\\", "-") elif addr: new_name = addr[:120] else: new_name = "LOC-{}".format(loc["legacy_delivery_id"]) # Handle duplicates (same address, different delivery) if new_name in seen_loc: new_name = "{} [{}]".format(new_name, loc["legacy_delivery_id"]) seen_loc.add(new_name) if new_name != loc["name"]: loc_rename[loc["name"]] = new_name print("Locations to rename: {} (of {})".format(len(loc_rename), len(locations))) LOC_REFS = [ ("tabService Subscription", "service_location"), ("tabService Equipment", "service_location"), ("tabDispatch Job", "service_location"), ("tabIssue", "service_location"), ] batch_rename("tabService Location", loc_rename, LOC_REFS, "Service Location") # ═══════════════════════════════════════════════════════════════ # PHASE 3: RENAME SERVICE EQUIPMENT # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 3: RENAME SERVICE EQUIPMENT") print("="*60) equipment = frappe.db.sql(""" SELECT name, legacy_device_id FROM "tabService Equipment" WHERE legacy_device_id IS NOT NULL AND legacy_device_id > 0 ORDER BY legacy_device_id """, as_dict=True) eq_rename = {} seen_eq = set() for eq in equipment: new_name = "EQ-{}".format(eq["legacy_device_id"]) if new_name == eq["name"]: continue if new_name in seen_eq: new_name = "EQ-{}-b".format(eq["legacy_device_id"]) seen_eq.add(new_name) eq_rename[eq["name"]] = new_name print("Equipment to rename: {} (of {})".format(len(eq_rename), len(equipment))) EQ_REFS = [ ("tabService Subscription", "device"), ] batch_rename("tabService Equipment", eq_rename, EQ_REFS, "Service Equipment") # ═══════════════════════════════════════════════════════════════ # PHASE 4: VERIFY # ═══════════════════════════════════════════════════════════════ print("\n" + "="*60) print("PHASE 4: VERIFY") print("="*60) # Sample customers print("\nSample Customers:") sample_c = frappe.db.sql(""" SELECT name, customer_name FROM "tabCustomer" WHERE disabled = 0 ORDER BY name LIMIT 10 """, as_dict=True) for c in sample_c: print(" {} → {}".format(c["name"], c["customer_name"])) # Sample locations print("\nSample Service Locations:") sample_l = frappe.db.sql(""" SELECT name, customer, city FROM "tabService Location" WHERE status = 'Active' ORDER BY name LIMIT 10 """, as_dict=True) for l in sample_l: print(" {} → customer={}".format(l["name"], l["customer"])) # Sample equipment print("\nSample Service Equipment:") sample_e = frappe.db.sql(""" SELECT name, equipment_type, serial_number, customer FROM "tabService Equipment" ORDER BY name LIMIT 10 """, as_dict=True) for e in sample_e: print(" {} → {} sn={} customer={}".format( e["name"], e["equipment_type"], e["serial_number"], e["customer"])) # Check for orphaned references print("\nOrphan check:") orphan_sub = frappe.db.sql(""" SELECT COUNT(*) FROM "tabService Subscription" ss WHERE ss.customer IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "tabCustomer" c WHERE c.name = ss.customer) """)[0][0] print(" Subscriptions with invalid customer ref: {}".format(orphan_sub)) orphan_eq = frappe.db.sql(""" SELECT COUNT(*) FROM "tabService Equipment" eq WHERE eq.service_location IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "tabService Location" sl WHERE sl.name = eq.service_location) """)[0][0] print(" Equipment with invalid location ref: {}".format(orphan_eq)) frappe.clear_cache() elapsed = time.time() - T_TOTAL print("\n" + "="*60) print("DONE in {:.1f}s — cache cleared".format(elapsed)) print("="*60)