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>
137 lines
4.1 KiB
Python
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()
|