- 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>
255 lines
9.6 KiB
Python
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!")
|