gigafibre-fsm/scripts/migration/rename_customers_c_prefix.py
louispaulb 4693bcf60c feat: telephony UI, performance indexes, Twilio softphone, lazy-load invoices
- Add PostgreSQL performance indexes migration script (1000x faster queries)
  Sales Invoice: 1,248ms → 28ms, Payment Entry: 443ms → 31ms
  Indexes on customer/party columns for all major tables
- Disable 3CX poller (PBX_ENABLED flag, using Twilio instead)
- Add TelephonyPage: full CRUD UI for Routr/Fonoster resources
  (trunks, agents, credentials, numbers, domains, peers)
- Add PhoneModal + usePhone composable (Twilio WebRTC softphone)
- Lazy-load invoices/payments (initial 5, expand on demand)
- Parallelize all API calls in ClientDetailPage (no waterfall)
- Add targo-hub service (SSE relay, SMS, voice, telephony API)
- Customer portal: invoice detail, ticket detail, messages pages
- Remove dead Ollama nginx upstream

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 13:59:59 -04:00

255 lines
9.6 KiB
Python

#!/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!")