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>
175 lines
6.0 KiB
Python
175 lines
6.0 KiB
Python
"""
|
|
Update ERPNext Item descriptions from legacy product data.
|
|
Uses hijack_desc from services and product_translate for French names.
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/update_item_descriptions.py
|
|
"""
|
|
import frappe
|
|
import pymysql
|
|
import os
|
|
import html
|
|
|
|
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)
|
|
|
|
conn = pymysql.connect(
|
|
host="legacy-db",
|
|
user="facturation",
|
|
password="*******",
|
|
database="gestionclient",
|
|
cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 1: Build a description map from legacy data
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 1: BUILD DESCRIPTION MAP")
|
|
print("=" * 60)
|
|
|
|
with conn.cursor() as cur:
|
|
# Get all products with French translations
|
|
cur.execute("""
|
|
SELECT p.id, p.sku, p.price, p.category,
|
|
pt.name as fr_name, pt.description_short as fr_desc
|
|
FROM product p
|
|
LEFT JOIN product_translate pt ON pt.product_id = p.id AND pt.language_id = 'fr'
|
|
WHERE p.active = 1
|
|
""")
|
|
products = cur.fetchall()
|
|
|
|
# Get category names
|
|
cur.execute("SELECT id, name FROM product_cat")
|
|
cats = {r["id"]: html.unescape(r["name"]) for r in cur.fetchall()}
|
|
|
|
# Get most common hijack_desc per product (the custom description used on services)
|
|
cur.execute("""
|
|
SELECT product_id, hijack_desc, COUNT(*) as cnt
|
|
FROM service
|
|
WHERE hijack = 1 AND hijack_desc IS NOT NULL AND hijack_desc != ''
|
|
GROUP BY product_id, hijack_desc
|
|
ORDER BY product_id, cnt DESC
|
|
""")
|
|
hijack_descs = {}
|
|
for r in cur.fetchall():
|
|
pid = r["product_id"]
|
|
if pid not in hijack_descs:
|
|
hijack_descs[pid] = r["hijack_desc"]
|
|
|
|
conn.close()
|
|
|
|
# Build the best description for each SKU
|
|
desc_map = {}
|
|
for p in products:
|
|
sku = p["sku"]
|
|
if not sku:
|
|
continue
|
|
|
|
# Priority: French translation > hijack_desc > category name
|
|
desc = p["fr_name"] or hijack_descs.get(p["id"]) or ""
|
|
cat = cats.get(p["category"], "")
|
|
|
|
desc_map[sku] = {
|
|
"description": desc.strip() if desc else "",
|
|
"item_group": cat,
|
|
"price": float(p["price"] or 0),
|
|
}
|
|
|
|
print("Products mapped: {}".format(len(desc_map)))
|
|
|
|
# Show samples
|
|
for sku in ["FTTB1000I", "TELEPMENS", "RAB24M", "HVIPFIXE", "FTT_HFAR", "CSERV", "RAB2X", "FTTH_LOCMOD"]:
|
|
d = desc_map.get(sku, {})
|
|
print(" {} -> desc='{}' group='{}' price={}".format(sku, d.get("description", ""), d.get("item_group", ""), d.get("price", "")))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 2: Update ERPNext Items
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 2: UPDATE ERPNEXT ITEMS")
|
|
print("=" * 60)
|
|
|
|
# Get all items that currently have "Legacy product ID" as description
|
|
items = frappe.db.sql("""
|
|
SELECT name, item_name, description, item_group
|
|
FROM "tabItem"
|
|
WHERE description LIKE 'Legacy product ID%%'
|
|
""", as_dict=True)
|
|
|
|
print("Items with legacy placeholder description: {}".format(len(items)))
|
|
|
|
updated = 0
|
|
for item in items:
|
|
sku = item["name"]
|
|
legacy = desc_map.get(sku)
|
|
if not legacy:
|
|
continue
|
|
|
|
new_desc = legacy["description"]
|
|
new_name = new_desc if new_desc else sku
|
|
|
|
if new_desc:
|
|
frappe.db.sql("""
|
|
UPDATE "tabItem"
|
|
SET description = %s, item_name = %s
|
|
WHERE name = %s
|
|
""", (new_desc, new_name, sku))
|
|
updated += 1
|
|
|
|
frappe.db.commit()
|
|
print("Updated: {} items".format(updated))
|
|
|
|
# Also update Subscription Plan descriptions (plan_name)
|
|
plans = frappe.db.sql("""
|
|
SELECT name, plan_name, item, cost FROM "tabSubscription Plan"
|
|
""", as_dict=True)
|
|
|
|
plan_updated = 0
|
|
for plan in plans:
|
|
item_sku = plan["item"]
|
|
legacy = desc_map.get(item_sku)
|
|
if not legacy or not legacy["description"]:
|
|
continue
|
|
|
|
new_plan_name = "PLAN-{}".format(item_sku)
|
|
frappe.db.sql("""
|
|
UPDATE "tabSubscription Plan"
|
|
SET plan_name = %s
|
|
WHERE name = %s
|
|
""", (new_plan_name, plan["name"]))
|
|
plan_updated += 1
|
|
|
|
frappe.db.commit()
|
|
print("Updated: {} subscription plans".format(plan_updated))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 3: VERIFY
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 3: VERIFY")
|
|
print("=" * 60)
|
|
|
|
# Sample items
|
|
sample = frappe.db.sql("""
|
|
SELECT name, item_name, description, item_group
|
|
FROM "tabItem"
|
|
WHERE name IN ('FTTB1000I', 'TELEPMENS', 'RAB24M', 'HVIPFIXE', 'FTT_HFAR', 'CSERV', 'RAB2X')
|
|
ORDER BY name
|
|
""", as_dict=True)
|
|
for s in sample:
|
|
print(" {} | name={} | group={} | desc={}".format(
|
|
s["name"], s["item_name"], s["item_group"], (s["description"] or "")[:80]))
|
|
|
|
# Count remaining legacy descriptions
|
|
remaining = frappe.db.sql("""
|
|
SELECT COUNT(*) FROM "tabItem" WHERE description LIKE 'Legacy product ID%%'
|
|
""")[0][0]
|
|
print("\nRemaining with legacy placeholder: {}".format(remaining))
|
|
|
|
frappe.clear_cache()
|
|
print("Done — cache cleared")
|