""" Import missing services — categories excluded from the original migration. Original migration (phase 3) only imported categories: 4,9,17,21,32,33 This imports ALL remaining active services from other categories. For each missing service: 1. Ensure Item exists in ERPNext 2. Ensure Subscription Plan exists 3. Create Subscription linked to correct customer + service_location Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_missing_services.py """ import frappe import pymysql import os from datetime import datetime, timezone DRY_RUN = False os.chdir("/home/frappe/frappe-bench/sites") frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() print("Connected:", frappe.local.site) conn = pymysql.connect( host="10.100.80.100", user="facturation", password="VD67owoj", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) # ═══════════════════════════════════════════════════════════════ # LOAD LEGACY DATA # ═══════════════════════════════════════════════════════════════ print("Loading legacy data...") with conn.cursor() as cur: # All active services from excluded categories cur.execute(""" SELECT s.id, s.delivery_id, s.product_id, s.status, s.hijack, s.hijack_price, s.hijack_desc, s.payment_recurrence, s.date_orig, s.date_next_invoice, s.radius_user, s.radius_pwd, p.sku, p.price as base_price, p.category, d.account_id FROM service s JOIN product p ON p.id = s.product_id JOIN delivery d ON d.id = s.delivery_id WHERE s.status = 1 AND p.category NOT IN (4,9,17,21,32,33) ORDER BY d.account_id, s.id """) missing_services = cur.fetchall() # All products from these categories cur.execute(""" SELECT DISTINCT p.id, p.sku, p.price, p.category FROM product p JOIN service s ON s.product_id = p.id WHERE s.status = 1 AND p.category NOT IN (4,9,17,21,32,33) """) products = cur.fetchall() # Category names (hardcoded from legacy) categories = { 1: "Installation initiale", 7: "Location serveur", 8: "Location équipement", 11: "Nom de domaine", 13: "Location espace", 15: "Hébergement", 16: "Support", 23: "Hotspot camping", 26: "Installation et équipement fibre", 28: "Quotidien pro", 34: "Installation et équipement télé", } conn.close() print("Missing active services: {}".format(len(missing_services))) print("Distinct products: {}".format(len(products))) # Show breakdown by category cat_counts = {} for s in missing_services: cat = s["category"] cat_name = categories.get(cat, "cat={}".format(cat)) if cat_name not in cat_counts: cat_counts[cat_name] = 0 cat_counts[cat_name] += 1 for name, cnt in sorted(cat_counts.items(), key=lambda x: -x[1]): print(" {}: {}".format(name, cnt)) # ═══════════════════════════════════════════════════════════════ # LOAD ERPNEXT DATA # ═══════════════════════════════════════════════════════════════ print("\nLoading ERPNext data...") # Existing items existing_items = set() items = frappe.db.sql('SELECT name FROM "tabItem"', as_dict=True) for i in items: existing_items.add(i["name"]) print("Existing items: {}".format(len(existing_items))) # Existing subscription plans existing_plans = set() plans = frappe.db.sql('SELECT plan_name FROM "tabSubscription Plan"', as_dict=True) for p in plans: existing_plans.add(p["plan_name"]) print("Existing plans: {}".format(len(existing_plans))) # Item details: sku → {item_name, item_group} item_details = {} item_rows = frappe.db.sql('SELECT name, item_name, item_group FROM "tabItem"', as_dict=True) for i in item_rows: item_details[i["name"]] = {"item_name": i["item_name"], "item_group": i["item_group"]} # Existing subscriptions by legacy_service_id existing_subs = set() subs = frappe.db.sql('SELECT legacy_service_id FROM "tabSubscription" WHERE legacy_service_id IS NOT NULL', as_dict=True) for s in subs: existing_subs.add(s["legacy_service_id"]) print("Existing subscriptions: {}".format(len(existing_subs))) # Customer mapping: legacy_account_id → ERPNext customer name cust_map = {} custs = frappe.db.sql('SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL AND legacy_account_id > 0', as_dict=True) for c in custs: cust_map[c["legacy_account_id"]] = c["name"] print("Customer mapping: {}".format(len(cust_map))) # Service location mapping: look up by customer + delivery address # We'll find the service_location from existing subscriptions for the same delivery loc_map = {} # (account_id, delivery_id) → service_location from existing subs loc_subs = frappe.db.sql(""" SELECT s.legacy_service_id, s.service_location, s.party FROM "tabSubscription" s WHERE s.service_location IS NOT NULL AND s.legacy_service_id IS NOT NULL """, as_dict=True) # Build delivery → location map from legacy with pymysql.connect(host="10.100.80.100", user="facturation", password="VD67owoj", database="gestionclient", cursorclass=pymysql.cursors.DictCursor) as conn2: with conn2.cursor() as cur2: cur2.execute(""" SELECT s.id as service_id, s.delivery_id, d.account_id FROM service s JOIN delivery d ON d.id = s.delivery_id WHERE s.status = 1 """) svc_delivery = {r["service_id"]: (r["account_id"], r["delivery_id"]) for r in cur2.fetchall()} # Map delivery_id → service_location from existing subscriptions delivery_loc = {} for ls in loc_subs: sid = ls["legacy_service_id"] if sid in svc_delivery: acct, did = svc_delivery[sid] if did not in delivery_loc and ls["service_location"]: delivery_loc[did] = ls["service_location"] print("Delivery→location mappings: {}".format(len(delivery_loc))) # Category to item_group mapping CATEGORY_GROUP = { 26: "Installation et équipement fibre", 34: "Installation et équipement télé", 8: "Installation et équipement fibre", 15: "Hébergement", 11: "Nom de domaine", 13: "Installation et équipement fibre", 1: "Installation et équipement fibre", 16: "Installation et équipement fibre", 28: "Installation et équipement fibre", 7: "Installation et équipement fibre", 23: "Installation et équipement fibre", } TAX_TEMPLATE = "Canada - Résidentiel - TC" COMPANY = "Targo Communications" def ts_to_date(ts): if not ts or ts <= 0: return None try: return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d") except (ValueError, OSError): return None # ═══════════════════════════════════════════════════════════════ # PREPARE: Items and Plans # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("ITEMS & PLANS") print("=" * 70) items_to_create = [] plans_to_create = [] for p in products: sku = p["sku"] if sku not in existing_items: items_to_create.append({ "sku": sku, "price": float(p["price"]), "category": p["category"], "group": CATEGORY_GROUP.get(p["category"], "Installation et équipement fibre"), }) plan_name = "PLAN-" + sku if plan_name not in existing_plans: plans_to_create.append({ "plan_name": plan_name, "item": sku, "cost": float(p["price"]), }) print("Items to create: {}".format(len(items_to_create))) for i in items_to_create: print(" {} @ {:.2f} → {}".format(i["sku"], i["price"], i["group"])) print("Plans to create: {}".format(len(plans_to_create))) for p in plans_to_create: print(" {} (item: {}, cost: {:.2f})".format(p["plan_name"], p["item"], p["cost"])) # ═══════════════════════════════════════════════════════════════ # PREPARE: Subscriptions # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("SUBSCRIPTIONS") print("=" * 70) subs_to_create = [] skipped = {"no_customer": 0, "already_exists": 0, "no_location": 0} for s in missing_services: sid = s["id"] if sid in existing_subs: skipped["already_exists"] += 1 continue acct_id = s["account_id"] cust_name = cust_map.get(acct_id) if not cust_name: skipped["no_customer"] += 1 continue sku = s["sku"] plan_name = "PLAN-" + sku delivery_id = s["delivery_id"] service_location = delivery_loc.get(delivery_id) if not service_location: skipped["no_location"] += 1 # Still create — just without location pass price = float(s["hijack_price"]) if s["hijack"] else float(s["base_price"]) freq = "A" if s["payment_recurrence"] == 0 else "M" start_date = ts_to_date(s["date_orig"]) or "2020-01-01" idet = item_details.get(sku, {}) subs_to_create.append({ "legacy_id": sid, "customer": cust_name, "plan": plan_name, "sku": sku, "price": price, "freq": freq, "start_date": start_date, "service_location": service_location, "hijack_desc": s["hijack_desc"] or "", "category": s["category"], "item_name": idet.get("item_name", sku), "item_group": idet.get("item_group", ""), }) print("Subscriptions to create: {}".format(len(subs_to_create))) print("Skipped: {}".format(skipped)) # Show samples by category for cat_name in sorted(cat_counts.keys(), key=lambda x: -cat_counts[x]): samples = [s for s in subs_to_create if categories.get(s["category"]) == cat_name][:3] if samples: print("\n {} ({} total):".format(cat_name, sum(1 for s in subs_to_create if categories.get(s["category"]) == cat_name))) for s in samples: print(" svc#{} {} {} {:.2f} → {} at {}".format( s["legacy_id"], s["sku"], s["freq"], s["price"], s["customer"], s["service_location"] or "NO_LOC")) # ═══════════════════════════════════════════════════════════════ # APPLY # ═══════════════════════════════════════════════════════════════ if DRY_RUN: print("\n*** DRY RUN — no changes made ***") print("Set DRY_RUN = False to create {} items, {} plans, {} subscriptions".format( len(items_to_create), len(plans_to_create), len(subs_to_create))) else: print("\n" + "=" * 70) print("APPLYING CHANGES") print("=" * 70) # Step 1: Create missing Items for i in items_to_create: try: item_groups = frappe.db.sql('SELECT name FROM "tabItem Group" WHERE name = %s', (i["group"],)) if not item_groups: i["group"] = "All Item Groups" doc = frappe.get_doc({ "doctype": "Item", "item_code": i["sku"], "item_name": i["sku"], "item_group": i["group"], "stock_uom": "Nos", "is_stock_item": 0, }) doc.insert(ignore_permissions=True) print(" Created item: {}".format(i["sku"])) except Exception as e: print(" ERR item {}: {}".format(i["sku"], str(e)[:100])) frappe.db.commit() # Step 2: Create missing Subscription Plans for p in plans_to_create: try: doc = frappe.get_doc({ "doctype": "Subscription Plan", "plan_name": p["plan_name"], "item": p["item"], "price_determination": "Fixed Rate", "cost": p["cost"], "currency": "CAD", "billing_interval": "Month", "billing_interval_count": 1, }) doc.insert(ignore_permissions=True) print(" Created plan: {}".format(p["plan_name"])) except Exception as e: print(" ERR plan {}: {}".format(p["plan_name"], str(e)[:100])) frappe.db.commit() # Step 3: Create subscriptions created = 0 errors = 0 for s in subs_to_create: try: sub = frappe.get_doc({ "doctype": "Subscription", "party_type": "Customer", "party": s["customer"], "company": COMPANY, "status": "Active", "start_date": s["start_date"], "generate_invoice_at": "Beginning of the current subscription period", "days_until_due": 30, "follow_calendar_months": 1, "generate_new_invoices_past_due_date": 1, "submit_invoice": 0, "cancel_at_period_end": 0, "legacy_service_id": s["legacy_id"], "service_location": s["service_location"], "actual_price": s["price"], "custom_description": s["hijack_desc"] if s["hijack_desc"] else None, "item_code": s["sku"], "item_name": s["item_name"], "item_group": s["item_group"], "billing_frequency": s["freq"], "plans": [{ "plan": s["plan"], "qty": 1, }], }) sub.flags.ignore_validate = True sub.flags.ignore_links = True sub.insert(ignore_permissions=True) created += 1 if created % 500 == 0: frappe.db.commit() print(" Progress: {}/{}".format(created, len(subs_to_create))) except Exception as e: errors += 1 if errors <= 20: print(" ERR svc#{}: {}".format(s["legacy_id"], str(e)[:150])) frappe.db.commit() print("\nCreated: {} subscriptions".format(created)) print("Errors: {}".format(errors)) print("\n" + "=" * 70) print("DONE") print("=" * 70)