gigafibre-fsm/scripts/migration/update_item_descriptions.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

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="10.100.80.100",
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")