gigafibre-fsm/scripts/migration/rename_to_readable_ids.py
louispaulb 101faa21f1 feat: inline editing, search, notifications + full repo cleanup
- InlineField component + useInlineEdit composable for Odoo-style dblclick editing
- Client search by name, account ID, and legacy_customer_id (or_filters)
- SMS/Email notification panel on ContactCard via n8n webhooks
- Ticket reply thread via Communication docs
- All migration scripts (51 files) now tracked
- Client portal and field tech app added to monorepo
- README rewritten with full feature list, migration summary, architecture
- CHANGELOG updated with all recent work
- ROADMAP updated with current completion status
- Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN)
- .gitignore updated (docker/, .claude/, exports/, .quasar/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 07:34:41 -04:00

273 lines
9.8 KiB
Python

"""
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)