Backend services: - targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas, extract dispatch scoring weights, trim section dividers across 9 files - modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(), consolidate DM query factory, fix duplicate username fill bug, trim headers (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%) Frontend: - useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into 6 focused helpers (processOnlineStatus, processWanIPs, processRadios, processMeshNodes, processClients, checkRadioIssues) - EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments Documentation (17 → 13 files, -1,400 lines): - New consolidated README.md (architecture, services, dependencies, auth) - Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md - Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md - Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md - Update ROADMAP.md with current phase status - Delete CONTEXT.md (absorbed into README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
240 lines
7.9 KiB
Python
240 lines
7.9 KiB
Python
"""
|
|
Import VoIP lines from legacy: pbx + phone_addr (911) + phone_provisioning (ATA).
|
|
Creates VoIP Line records linked to Customer + Service Location.
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_voip_lines.py
|
|
"""
|
|
import frappe
|
|
import pymysql
|
|
import os, sys, time
|
|
|
|
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
|
|
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)
|
|
|
|
legacy = pymysql.connect(
|
|
host="legacy-db", user="facturation", password="VD67owoj",
|
|
database="gestionclient", cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
# Build mappings
|
|
cust_map = {}
|
|
for r in frappe.db.sql('SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id > 0', as_dict=True):
|
|
cust_map[int(r['legacy_account_id'])] = r['name']
|
|
print(f" {len(cust_map)} customers mapped")
|
|
|
|
loc_map = {}
|
|
for r in frappe.db.sql('SELECT name, legacy_delivery_id FROM "tabService Location" WHERE legacy_delivery_id > 0', as_dict=True):
|
|
loc_map[int(r['legacy_delivery_id'])] = r['name']
|
|
print(f" {len(loc_map)} locations mapped")
|
|
|
|
sub_map = {}
|
|
for r in frappe.db.sql('SELECT name, legacy_service_id FROM "tabService Subscription" WHERE legacy_service_id > 0', as_dict=True):
|
|
sub_map[int(r['legacy_service_id'])] = r['name']
|
|
print(f" {len(sub_map)} subscriptions mapped")
|
|
|
|
# ── Load all 3 legacy tables ──
|
|
print("\n=== Loading legacy VoIP data ===")
|
|
|
|
with legacy.cursor() as cur:
|
|
# PBX lines
|
|
cur.execute("SELECT * FROM pbx ORDER BY id")
|
|
pbx_rows = cur.fetchall()
|
|
print(f" {len(pbx_rows)} PBX lines")
|
|
|
|
# 911 addresses indexed by (account_id, phone)
|
|
cur.execute("SELECT * FROM phone_addr")
|
|
e911_rows = cur.fetchall()
|
|
e911_map = {}
|
|
for r in e911_rows:
|
|
key = (r['account_id'], r['phone'])
|
|
e911_map[key] = r
|
|
print(f" {len(e911_rows)} 911 addresses ({len(e911_map)} unique)")
|
|
|
|
# ATA provisioning indexed by (account_id, phone)
|
|
cur.execute("SELECT * FROM phone_provisioning")
|
|
prov_rows = cur.fetchall()
|
|
prov_map = {}
|
|
for r in prov_rows:
|
|
key = (r['account_id'], r['phone'])
|
|
prov_map[key] = r
|
|
print(f" {len(prov_rows)} ATA provisioning records")
|
|
|
|
legacy.close()
|
|
|
|
# Clear existing
|
|
existing = frappe.db.sql('SELECT COUNT(*) FROM "tabVoIP Line"')[0][0]
|
|
if existing:
|
|
frappe.db.sql('DELETE FROM "tabVoIP Line"')
|
|
frappe.db.commit()
|
|
print(f" Cleared {existing} existing VoIP Lines")
|
|
|
|
# ── Import PBX lines with matched 911 + ATA ──
|
|
print("\n=== Importing VoIP Lines ===")
|
|
T0 = time.time()
|
|
created = skipped = 0
|
|
with_911 = without_911 = 0
|
|
|
|
for pbx in pbx_rows:
|
|
cust = cust_map.get(pbx['account_id'])
|
|
if not cust:
|
|
skipped += 1
|
|
continue
|
|
|
|
loc = loc_map.get(pbx['delivery_id'], '')
|
|
sub = sub_map.get(pbx['service_id'], '')
|
|
phone = pbx['phone']
|
|
|
|
# Match 911 address
|
|
e911 = e911_map.get((pbx['account_id'], phone), {})
|
|
if e911:
|
|
with_911 += 1
|
|
else:
|
|
without_911 += 1
|
|
|
|
# Match ATA provisioning
|
|
prov = prov_map.get((pbx['account_id'], phone), {})
|
|
|
|
# Format DID as xxx-xxx-xxxx if 10 digits
|
|
did = phone
|
|
if len(did) == 10:
|
|
did = f"{did[:3]}-{did[3:6]}-{did[6:]}"
|
|
|
|
doc = frappe.get_doc({
|
|
"doctype": "VoIP Line",
|
|
"did": did,
|
|
"customer": cust,
|
|
"service_location": loc,
|
|
"subscription": sub,
|
|
"status": "Active",
|
|
|
|
# SIP config
|
|
"caller_id": (pbx.get('name') or '').strip(),
|
|
"sip_password": pbx.get('password') or '',
|
|
"extension": pbx.get('int_code') or '',
|
|
"sip_context": pbx.get('user_context') or '',
|
|
"max_calls": pbx.get('max_calls') or 2,
|
|
"call_timeout": pbx.get('call_timeout') or 30,
|
|
"language": pbx.get('language') or 'fr',
|
|
"country_whitelist": pbx.get('country_whitelist') or '',
|
|
|
|
# Voicemail
|
|
"voicemail_enabled": 1 if pbx.get('has_vm') else 0,
|
|
"vm_password": pbx.get('vm_password') or '',
|
|
"vm_email": pbx.get('vm_email') or '',
|
|
"vm_keep_msg": 1 if pbx.get('keep_msg') else 0,
|
|
"vm_disk_quota": pbx.get('vm_disk_quota') or 180,
|
|
|
|
# 911 address
|
|
"e911_first_name": e911.get('first_name', ''),
|
|
"e911_last_name": e911.get('last_name', ''),
|
|
"e911_street_number": e911.get('street_number', ''),
|
|
"e911_apt": e911.get('apt', ''),
|
|
"e911_street_name": e911.get('street_name', ''),
|
|
"e911_city": e911.get('city', ''),
|
|
"e911_state": e911.get('state', ''),
|
|
"e911_zip": e911.get('zip', ''),
|
|
"e911_cauca_code": e911.get('code_cauca', ''),
|
|
"e911_class": e911.get('class_service', 'RES'),
|
|
"e911_enhanced": 1 if e911.get('enhanced_capable') == 'Y' else 0,
|
|
"e911_synced": 1 if e911 else 0,
|
|
"e911_info": e911.get('info', ''),
|
|
|
|
# ATA
|
|
"ata_model": prov.get('app', ''),
|
|
"ata_mac": prov.get('mac', ''),
|
|
"ata_password": prov.get('password', ''),
|
|
"international_enabled": 1 if prov.get('internationnal') else 0,
|
|
|
|
# Legacy refs
|
|
"legacy_pbx_id": pbx['id'],
|
|
"legacy_account_id": pbx['account_id'],
|
|
"legacy_delivery_id": pbx.get('delivery_id') or 0,
|
|
"legacy_service_id": pbx.get('service_id') or 0,
|
|
})
|
|
try:
|
|
doc.insert(ignore_if_duplicate=True)
|
|
created += 1
|
|
except Exception as e:
|
|
print(f" ERR PBX {pbx['id']} DID {did}: {str(e)[:80]}")
|
|
frappe.db.rollback()
|
|
skipped += 1
|
|
|
|
if created % 100 == 0:
|
|
frappe.db.commit()
|
|
print(f" [{created}/{len(pbx_rows)}] [{time.time()-T0:.0f}s]")
|
|
|
|
frappe.db.commit()
|
|
|
|
# ── Import orphan 911 addresses (no PBX line) ──
|
|
print("\n=== Importing orphan 911 addresses ===")
|
|
orphan_count = 0
|
|
pbx_phones = {(r['account_id'], r['phone']) for r in pbx_rows}
|
|
|
|
for key, e911 in e911_map.items():
|
|
if key in pbx_phones:
|
|
continue # already imported with PBX line
|
|
|
|
cust = cust_map.get(e911['account_id'])
|
|
if not cust:
|
|
continue
|
|
|
|
phone = e911['phone']
|
|
did = phone
|
|
if len(did) == 10:
|
|
did = f"{did[:3]}-{did[3:6]}-{did[6:]}"
|
|
|
|
doc = frappe.get_doc({
|
|
"doctype": "VoIP Line",
|
|
"did": did,
|
|
"customer": cust,
|
|
"status": "Inactive",
|
|
"caller_id": f"{e911.get('first_name', '')} {e911.get('last_name', '')}".strip(),
|
|
|
|
# 911 only
|
|
"e911_first_name": e911.get('first_name', ''),
|
|
"e911_last_name": e911.get('last_name', ''),
|
|
"e911_street_number": e911.get('street_number', ''),
|
|
"e911_apt": e911.get('apt', ''),
|
|
"e911_street_name": e911.get('street_name', ''),
|
|
"e911_city": e911.get('city', ''),
|
|
"e911_state": e911.get('state', ''),
|
|
"e911_zip": e911.get('zip', ''),
|
|
"e911_cauca_code": e911.get('code_cauca', ''),
|
|
"e911_class": e911.get('class_service', 'RES'),
|
|
"e911_enhanced": 1 if e911.get('enhanced_capable') == 'Y' else 0,
|
|
"e911_synced": 1, # was synced when line was active
|
|
"e911_info": e911.get('info', ''),
|
|
|
|
"legacy_account_id": e911['account_id'],
|
|
})
|
|
try:
|
|
doc.insert(ignore_if_duplicate=True)
|
|
orphan_count += 1
|
|
except Exception as e:
|
|
if "Duplicate" not in str(e):
|
|
print(f" WARN: DID {did} — {str(e)[:60]}")
|
|
|
|
frappe.db.commit()
|
|
print(f" {orphan_count} orphan 911 lines created")
|
|
|
|
# ── Verify ──
|
|
print(f"\n=== Summary ===")
|
|
total = frappe.db.sql('SELECT COUNT(*) FROM "tabVoIP Line"')[0][0]
|
|
dist = frappe.db.sql('SELECT status, COUNT(*) FROM "tabVoIP Line" GROUP BY status')
|
|
for r in dist:
|
|
print(f" {r[0]:15s} {r[1]}")
|
|
print(f" With 911 address: {with_911}")
|
|
print(f" Without 911: {without_911}")
|
|
print(f" Orphan 911 (inactive): {orphan_count}")
|
|
print(f" Total: {total} VoIP Lines")
|
|
print(f" Skipped (no customer): {skipped}")
|
|
print(f" Time: {time.time()-T0:.0f}s")
|
|
|
|
frappe.destroy()
|
|
print("Done!")
|