gigafibre-fsm/scripts/migration/import_soumissions.py
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
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>
2026-04-13 08:39:58 -04:00

185 lines
6.3 KiB
Python

"""
Import soumissions (quotes) from legacy as ERPNext Quotation.
Deserializes PHP-serialized materiel/mensuel arrays into Quotation Items.
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_soumissions.py
"""
import frappe
import pymysql
import os, sys, time, re
from html import unescape
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
)
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")
# Ensure SVC item exists (catch-all for unmapped SKUs)
if not frappe.db.exists("Item", "SVC"):
frappe.get_doc({"doctype": "Item", "item_code": "SVC", "item_name": "Service",
"item_group": "All Item Groups", "stock_uom": "Nos"}).insert()
frappe.db.commit()
print(" Created catch-all item SVC")
def parse_php_serialized(data):
"""Simple parser for PHP serialized arrays of items.
Returns list of dicts with keys: sku, desc, amt, qte, tot"""
if not data or data in ('a:0:{}', 'N;'):
return []
items = []
# Extract items using regex — handles nested a:N:{...} structures
# Pattern: s:3:"sku";s:N:"VALUE";...s:4:"desc";s:N:"VALUE";...s:3:"amt";s:N:"VALUE";
item_pattern = re.compile(
r's:3:"sku";s:\d+:"([^"]*)";'
r's:4:"desc";s:\d+:"([^"]*)";'
r's:3:"amt";s:\d+:"([^"]*)";'
r's:3:"qte";s:\d+:"([^"]*)";'
r's:3:"tot";s:\d+:"([^"]*)";'
)
for m in item_pattern.finditer(data):
sku, desc, amt, qte, tot = m.groups()
desc = unescape(desc).strip()
try:
amount = float(tot) if tot else (float(amt) * float(qte) if amt and qte else 0)
qty = float(qte) if qte else 1
rate = float(amt) if amt else (amount / qty if qty else 0)
except ValueError:
amount = 0
qty = 1
rate = 0
if desc or amount: # skip empty rows
items.append({"sku": sku, "desc": desc, "rate": rate, "qty": qty, "amount": amount})
return items
def parse_date(date_str):
"""Parse dd-mm-yyyy to yyyy-mm-dd"""
if not date_str:
return None
try:
parts = date_str.strip().split('-')
if len(parts) == 3:
return f"{parts[2]}-{parts[1]}-{parts[0]}"
except Exception:
pass
return None
# Load soumissions
print("\n=== Loading legacy soumissions ===")
with legacy.cursor() as cur:
cur.execute("SELECT * FROM soumission ORDER BY id")
rows = cur.fetchall()
legacy.close()
print(f" {len(rows)} soumissions to import")
# Add custom fields BEFORE cleanup check (column must exist first)
for fdef in [
{"dt": "Quotation", "fieldname": "custom_legacy_soumission_id", "fieldtype": "Int",
"label": "Legacy Soumission ID", "insert_after": "order_type"},
{"dt": "Quotation", "fieldname": "custom_po_number", "fieldtype": "Data",
"label": "PO Number", "insert_after": "custom_legacy_soumission_id"},
]:
if not frappe.db.exists("Custom Field", {"dt": fdef["dt"], "fieldname": fdef["fieldname"]}):
frappe.get_doc({"doctype": "Custom Field", **fdef}).insert(ignore_permissions=True)
frappe.db.commit()
print(f" Added custom field {fdef['fieldname']}")
# Clear existing legacy quotations
existing = frappe.db.sql("SELECT COUNT(*) FROM \"tabQuotation\" WHERE custom_legacy_soumission_id > 0")[0][0]
if existing:
# Delete items first, then quotations
frappe.db.sql("""
DELETE FROM "tabQuotation Item"
WHERE parent IN (SELECT name FROM "tabQuotation" WHERE custom_legacy_soumission_id > 0)
""")
frappe.db.sql('DELETE FROM "tabQuotation" WHERE custom_legacy_soumission_id > 0')
frappe.db.commit()
print(f" Cleared {existing} existing legacy Quotations")
T0 = time.time()
created = skipped = empty = 0
for r in rows:
cust = cust_map.get(r['account_id'])
if not cust:
skipped += 1
continue
materiel = parse_php_serialized(r.get('materiel') or '')
mensuel = parse_php_serialized(r.get('mensuel') or '')
all_items = materiel + mensuel
if not all_items:
# Create with single placeholder item
all_items = [{"sku": "SVC", "desc": (r['name'] or 'Soumission'), "rate": 0, "qty": 1, "amount": 0}]
empty += 1
posting_date = parse_date(r.get('date'))
items = []
for idx, item in enumerate(all_items):
items.append({
"item_code": "SVC",
"item_name": item['desc'][:140] if item['desc'] else f"Item {idx+1}",
"description": item['desc'] or '',
"qty": item['qty'] or 1,
"rate": item['rate'] or 0,
"uom": "Nos",
})
doc = frappe.get_doc({
"doctype": "Quotation",
"quotation_to": "Customer",
"party_name": cust,
"transaction_date": posting_date or "2026-01-01",
"valid_till": posting_date or "2026-01-01",
"company": "TARGO",
"currency": "CAD",
"order_type": "Sales",
"title": (r['name'] or f"Soumission {r['id']}")[:140],
"terms": unescape(r.get('text') or ''),
"custom_legacy_soumission_id": r['id'],
"custom_po_number": r.get('po') or '',
"items": items,
})
try:
doc.insert(ignore_if_duplicate=True)
created += 1
except Exception as e:
print(f" ERR soumission {r['id']}: {str(e)[:80]}")
skipped += 1
if created % 100 == 0:
frappe.db.commit()
print(f" [{created}/{len(rows)}] [{time.time()-T0:.0f}s]")
frappe.db.commit()
# ── Verify ──
print(f"\n=== Summary ===")
total = frappe.db.sql('SELECT COUNT(*) FROM "tabQuotation" WHERE custom_legacy_soumission_id > 0')[0][0]
print(f" Created: {created} Quotations")
print(f" Empty items (placeholder): {empty}")
print(f" Skipped: {skipped}")
print(f" Total: {total}")
print(f" Time: {time.time()-T0:.0f}s")
frappe.destroy()
print("Done!")