#!/usr/bin/env python3 """ Prepend C- to all customer names. Current state: customers have raw legacy_customer_id as name e.g. LPB4, 114796350603272, DOMIL5149490230 After: C-LPB4, C-114796350603272, C-DOMIL5149490230 New customers: C-10000000034941+ (naming_series C-.##############) Two-phase rename to avoid PK collisions: Phase A: old → _TMP_C-old Phase B: _TMP_C-old → C-old """ import os, sys, time os.chdir("/home/frappe/frappe-bench/sites") import frappe frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() frappe.local.flags.ignore_permissions = True print(f"Connected: {frappe.local.site}") DRY_RUN = "--dry-run" in sys.argv if DRY_RUN: print("*** DRY RUN ***") # ═══════════════════════════════════════════════════════════════ # STEP 1: Build mapping — prepend C- to every customer name # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 1: BUILD MAPPING") print("=" * 60) customers = frappe.db.sql(""" SELECT name FROM "tabCustomer" ORDER BY name """, as_dict=True) mapping = {} # old → new new_used = set() skipped = 0 for c in customers: old = c["name"] # Skip if already has C- prefix (idempotent) if old.startswith("C-"): skipped += 1 continue new = f"C-{old}" if new in new_used: new = f"{new}-dup{len(new_used)}" new_used.add(new) mapping[old] = new print(f"Total customers: {len(customers)}") print(f"Will rename: {len(mapping)}") print(f"Already C- prefixed (skip): {skipped}") # Samples for old, new in list(mapping.items())[:15]: print(f" {old:40s} → {new}") if DRY_RUN: print("\n*** DRY RUN complete ***") sys.exit(0) if not mapping: print("Nothing to rename!") sys.exit(0) # ═══════════════════════════════════════════════════════════════ # STEP 2: Create temp mapping table # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 2: TEMP MAPPING TABLE") print("=" * 60) frappe.db.sql('DROP TABLE IF EXISTS _cust_cpre_map') frappe.db.sql(""" CREATE TEMP TABLE _cust_cpre_map ( old_name VARCHAR(140) PRIMARY KEY, new_name VARCHAR(140) NOT NULL ) """) items = list(mapping.items()) for start in range(0, len(items), 1000): batch = items[start:start + 1000] frappe.db.sql( "INSERT INTO _cust_cpre_map (old_name, new_name) VALUES " + ",".join(["(%s, %s)"] * len(batch)), [v for pair in batch for v in pair] ) frappe.db.commit() print(f" {len(mapping)} mappings loaded") # ═══════════════════════════════════════════════════════════════ # STEP 3: Update all FK references # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 3: UPDATE FK REFERENCES") print("=" * 60) fk_tables = [ ("tabSales Invoice", "customer", ""), ("tabPayment Entry", "party", ""), ("tabGL Entry", "party", ""), ("tabPayment Ledger Entry", "party", ""), ("tabIssue", "customer", ""), ("tabService Location", "customer", ""), ("tabService Subscription", "customer", ""), ("tabSubscription", "party", ""), ("tabService Equipment", "customer", ""), ("tabDispatch Job", "customer", ""), ("tabDynamic Link", "link_name", "AND t.link_doctype = 'Customer'"), ("tabComment", "reference_name", "AND t.reference_doctype = 'Customer'"), ("tabCommunication", "reference_name", "AND t.reference_doctype = 'Customer'"), ("tabVersion", "docname", "AND t.ref_doctype = 'Customer'"), ] for table, col, extra in fk_tables: t0 = time.time() try: frappe.db.sql(f""" UPDATE "{table}" t SET "{col}" = m.new_name FROM _cust_cpre_map m WHERE t."{col}" = m.old_name {extra} """) frappe.db.commit() print(f" {table:35s} {col:20s} [{time.time()-t0:.1f}s]") except Exception as e: frappe.db.rollback() err = str(e)[:80] if "does not exist" not in err: print(f" {table:35s} ERR: {err}") # ═══════════════════════════════════════════════════════════════ # STEP 4: Two-phase rename customers # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 4: RENAME CUSTOMERS (two-phase)") print("=" * 60) # Phase A: old → _TMP_C-old t0 = time.time() for start in range(0, len(items), 500): batch = items[start:start + 500] cases = " ".join(f"WHEN '{old}' THEN '_TMP_{new}'" for old, new in batch) old_names = "','".join(old for old, _ in batch) frappe.db.sql(f""" UPDATE "tabCustomer" SET name = CASE name {cases} END WHERE name IN ('{old_names}') """) if (start + 500) % 5000 < 500: frappe.db.commit() print(f" A: {min(start+500, len(items))}/{len(items)}") frappe.db.commit() print(f" Phase A done [{time.time()-t0:.1f}s]") # Phase B: _TMP_C-old → C-old t0 = time.time() for start in range(0, len(items), 500): batch = items[start:start + 500] cases = " ".join(f"WHEN '_TMP_{new}' THEN '{new}'" for _, new in batch) temp_names = "','".join(f"_TMP_{new}" for _, new in batch) frappe.db.sql(f""" UPDATE "tabCustomer" SET name = CASE name {cases} END WHERE name IN ('{temp_names}') """) if (start + 500) % 5000 < 500: frappe.db.commit() print(f" B: {min(start+500, len(items))}/{len(items)}") frappe.db.commit() print(f" Phase B done [{time.time()-t0:.1f}s]") frappe.db.sql('DROP TABLE IF EXISTS _cust_cpre_map') frappe.db.commit() # ═══════════════════════════════════════════════════════════════ # STEP 5: Set naming series — C-.############## → C-10000000034941+ # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 5: NAMING SERIES") print("=" * 60) # Update Customer doctype naming_series options frappe.db.sql(""" UPDATE "tabDocField" SET options = 'C-.##############', "default" = 'C-.##############' WHERE parent = 'Customer' AND fieldname = 'naming_series' """) frappe.db.commit() print(" naming_series options: C-.##############") # Remove old C series (no dash), set new C- series # Counter 10000000034940 means next = C-10000000034941 frappe.db.sql("DELETE FROM \"tabSeries\" WHERE name = 'C'") series = frappe.db.sql("SELECT current FROM \"tabSeries\" WHERE name = 'C-'", as_dict=True) if series: frappe.db.sql("UPDATE \"tabSeries\" SET current = 10000000034940 WHERE name = 'C-'") else: frappe.db.sql("INSERT INTO \"tabSeries\" (name, current) VALUES ('C-', 10000000034940)") frappe.db.commit() print(" C- series counter: 10000000034940") print(" Next new customer: C-10000000034941") # ═══════════════════════════════════════════════════════════════ # STEP 6: Verify # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("VERIFICATION") print("=" * 60) total = frappe.db.sql('SELECT COUNT(*) FROM "tabCustomer"')[0][0] with_c = frappe.db.sql("SELECT COUNT(*) FROM \"tabCustomer\" WHERE name LIKE 'C-%%'")[0][0] without_c = total - with_c print(f" Customers total: {total}") print(f" With C- prefix: {with_c}") print(f" Without C- prefix: {without_c} (should be 0)") # Samples samples = frappe.db.sql(""" SELECT name, customer_name, legacy_customer_id FROM "tabCustomer" ORDER BY name LIMIT 15 """, as_dict=True) for s in samples: print(f" {s['name']:30s} {s.get('legacy_customer_id',''):20s} {s['customer_name']}") # Spot checks for lid, label in [(4, "LPB4"), (13814, "Vegpro")]: c = frappe.db.sql('SELECT name, legacy_customer_id FROM "tabCustomer" WHERE legacy_account_id = %s', (lid,), as_dict=True) if c: print(f" {label}: {c[0]['name']} (bank ref: {c[0].get('legacy_customer_id','')})") # FK checks for table, col in [("tabSales Invoice", "customer"), ("tabSubscription", "party"), ("tabIssue", "customer")]: orphans = frappe.db.sql(f""" SELECT COUNT(*) FROM "{table}" t WHERE t."{col}" IS NOT NULL AND t."{col}" != '' AND NOT EXISTS (SELECT 1 FROM "tabCustomer" c WHERE c.name = t."{col}") """)[0][0] print(f" {table}.{col}: {'OK ✓' if orphans == 0 else f'ORPHANS: {orphans}'}") frappe.clear_cache() print("\nDone!")