gigafibre-fsm/scripts/migration/import_items.py
louispaulb 93dd7a525f feat: migration legacy → ERPNext phases 1-4 complete
Phase 1: 833 Items + 34 Item Groups + custom fields (ISP speeds, RADIUS, legacy IDs)
Phase 2: 6,667 Customers + Contacts + Addresses via direct PG (~30s)
Phase 3: Tax template QC TPS+TVQ + 92 Subscription Plans
Phase 4: 21,876 Subscriptions with RADIUS data

CRITICAL: ERPNext scheduler is PAUSED — do not reactivate without explicit go.

Includes:
- ARCHITECTURE-COMPARE.md: full schema mapping legacy vs ERPNext
- CHANGELOG.md: detailed migration log
- MIGRATION-PLAN.md: strategy and next steps
- scripts/migration/: idempotent Python scripts (direct PG method)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:35:02 -04:00

137 lines
4.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Migration Script: Legacy product -> ERPNext Item
Python 3.5 compatible (no f-strings)
"""
import json
import sys
import requests
import pymysql
try:
from html import unescape
except ImportError:
try:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
except:
unescape = lambda x: x
LEGACY = {
"host": "10.100.80.100",
"user": "facturation",
"password": "VD67owoj",
"database": "gestionclient",
}
ERP_URL = "https://erp.gigafibre.ca"
ERP_TOKEN = "token b273a666c86d2d0:06120709db5e414"
CAT_MAP = {
1: u"Garantie prolongée",
2: u"Intérêts et frais divers",
3: u"Impression",
4: u"Mensualités sans fil",
5: u"Technicien",
6: u"Frais d'activation",
7: u"Equipement internet sans fil",
8: u"Installation et équipement internet sans fil",
9: u"Téléphonie",
10: u"Site internet",
11: u"Nom de domaine",
12: u"Services informatique",
13: u"Location d'espace",
14: u"Pièces informatique",
15: u"Hébergement",
16: u"Téléchargement supplémentaire",
17: u"Adresse IP Fixe",
18: u"Infographie",
19: u"Revenus - Frais de recouvrement",
20: u"Créances irrécouvrables",
21: u"Location point à point",
22: u"Frais pour irrécouvrables",
23: u"Internet camping",
24: u"Transport",
25: u"Frais divers taxables",
26: u"Installation et équipement fibre",
27: u"SPECIAL",
28: u"Quincaillerie",
29: u"Equipement internet fibre",
30: u"Location espace cloud",
31: u"Honoraires",
32: u"Mensualités fibre",
33: u"Mensualités télévision",
34: u"Installation et équipement télé",
}
SERVICE_CATS = {1,2,4,6,9,11,12,13,15,16,17,18,19,20,21,22,23,24,25,27,30,31,32,33}
def erp_post(doctype, data):
url = "{}/api/resource/{}".format(ERP_URL, doctype)
headers = {"Authorization": ERP_TOKEN, "Content-Type": "application/json"}
r = requests.post(url, headers=headers, json={"data": json.dumps(data)}, timeout=30, verify=True)
try:
return r.status_code, r.json()
except:
return r.status_code, {"error": r.text[:200]}
def main():
conn = pymysql.connect(**LEGACY)
cur = conn.cursor(pymysql.cursors.DictCursor)
cur.execute("""
SELECT p.*,
(SELECT COUNT(*) FROM service s WHERE s.product_id = p.id AND s.status = 1) as active_services
FROM product p
ORDER BY p.id
""")
products = cur.fetchall()
created = 0
skipped = 0
errors = 0
for p in products:
sku = p["sku"] or "LEGACY-{}".format(p["id"])
cat_id = p["category"] or 4
item_group = CAT_MAP.get(cat_id, "Products")
sku = unescape(sku).strip()
is_service = cat_id in SERVICE_CATS
disabled = 1 if (not p["active"] and p["active_services"] == 0) else 0
item_data = {
"doctype": "Item",
"item_code": sku,
"item_name": sku,
"item_group": item_group,
"stock_uom": "Nos",
"is_stock_item": 0 if is_service else 1,
"disabled": disabled,
"description": "Legacy product ID: {}".format(p["id"]),
"standard_rate": float(p["price"] or 0),
}
status, resp = erp_post("Item", item_data)
resp_str = str(resp)
if status == 200 and resp.get("data"):
created += 1
print(" OK {:<30s} ${:>8.2f} grp={:<25s} svc={}".format(
sku, float(p["price"] or 0), item_group[:25], p["active_services"]))
elif "DuplicateEntryError" in resp_str or "already exists" in resp_str.lower():
skipped += 1
print(" SKIP {:<30s} (already exists)".format(sku))
else:
errors += 1
err_msg = str(resp.get("exc_type", resp.get("message", resp_str)))[:80]
print(" ERR {:<30s} -> {}".format(sku, err_msg))
conn.close()
print("\n=== DONE: {} created, {} skipped, {} errors (total {}) ===".format(
created, skipped, errors, len(products)))
if __name__ == "__main__":
main()