""" Fix deliveries: restore catalog prices + create RAB-PROMO for discount absorption. Handles BOTH: A) Deliveries with existing rebates (catalog restore + adjust rebate) B) Deliveries with NO rebate (catalog restore + create RAB-PROMO) TEST MODE: Only runs on specific test accounts. Set TEST_ACCOUNTS = None to run on ALL accounts. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/fix_no_rebate_discounts.py """ import frappe import pymysql import os from datetime import datetime DRY_RUN = False # Set False to actually write # Test on specific accounts only — set to None for all # 3673 = Expro Transit, others from the 310 no-rebate list TEST_ACCOUNTS = {3673, 263, 343, 264, 166} 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) print("DRY_RUN:", DRY_RUN) conn = pymysql.connect( host="10.100.80.100", user="facturation", password="VD67owoj", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) # ═══════════════════════════════════════════════════════════════ # STEP 1: Load legacy data # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 1: LOAD LEGACY DATA") print("=" * 60) with conn.cursor() as cur: cur.execute(""" SELECT s.id, s.delivery_id, s.product_id, s.hijack, s.hijack_price, s.hijack_desc, p.sku, p.price as base_price, 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 ORDER BY s.delivery_id, p.price DESC """) all_services = cur.fetchall() conn.close() # Group by delivery deliveries = {} for s in all_services: did = s["delivery_id"] if did not in deliveries: deliveries[did] = [] base = float(s["base_price"] or 0) actual = float(s["hijack_price"]) if s["hijack"] else base is_rebate = base < 0 deliveries[did].append({ "svc_id": s["id"], "sku": s["sku"], "base_price": base, "actual_price": actual, "is_rebate": is_rebate, "hijack": s["hijack"], "hijack_desc": (s["hijack_desc"] or "").strip(), "account_id": s["account_id"], }) # Classify deliveries no_rebate_cases = [] has_rebate_cases = [] for did, services in deliveries.items(): acct_id = services[0]["account_id"] if TEST_ACCOUNTS and acct_id not in TEST_ACCOUNTS: continue has_discount = has_rebate = False discount_total = 0.0 discount_services = [] for s in services: if not s["is_rebate"] and s["actual_price"] < s["base_price"] - 0.01: has_discount = True discount_total += s["base_price"] - s["actual_price"] discount_services.append(s) if s["is_rebate"]: has_rebate = True if not has_discount: continue entry = { "delivery_id": did, "account_id": acct_id, "discount_total": round(discount_total, 2), "discount_services": discount_services, "all_services": services, } if has_rebate: has_rebate_cases.append(entry) else: no_rebate_cases.append(entry) print("Test accounts: {}".format(TEST_ACCOUNTS or "ALL")) print("Deliveries with existing rebate to adjust: {}".format(len(has_rebate_cases))) print("Deliveries needing RAB-PROMO creation: {}".format(len(no_rebate_cases))) # ═══════════════════════════════════════════════════════════════ # STEP 2: Ensure RAB-PROMO Item exists # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 2: ENSURE RAB-PROMO ITEM") print("=" * 60) rab_exists = frappe.db.sql(""" SELECT name FROM "tabItem" WHERE name = 'RAB-PROMO' """) if not rab_exists: if not DRY_RUN: frappe.db.sql(""" INSERT INTO "tabItem" ( name, creation, modified, modified_by, owner, docstatus, item_code, item_name, item_group, description, is_stock_item, has_variants, disabled ) VALUES ( 'RAB-PROMO', NOW(), NOW(), 'Administrator', 'Administrator', 0, 'RAB-PROMO', 'Rabais promotionnel', 'Rabais', 'Rabais promotionnel — créé automatiquement pour les services sans rabais existant', 0, 0, 0 ) """) frappe.db.commit() print(" Created RAB-PROMO item") else: print(" [DRY RUN] Would create RAB-PROMO item") else: print(" RAB-PROMO already exists") # Also ensure Subscription Plan exists plan_exists = frappe.db.sql(""" SELECT name FROM "tabSubscription Plan" WHERE name = 'PLAN-RAB-PROMO' """) if not plan_exists: if not DRY_RUN: frappe.db.sql(""" INSERT INTO "tabSubscription Plan" ( name, creation, modified, modified_by, owner, docstatus, plan_name, item, cost, billing_interval, billing_interval_count, currency, price_determination ) VALUES ( 'PLAN-RAB-PROMO', NOW(), NOW(), 'Administrator', 'Administrator', 0, 'PLAN-RAB-PROMO', 'RAB-PROMO', 0, 'Month', 1, 'CAD', 'Fixed Rate' ) """) frappe.db.commit() print(" Created PLAN-RAB-PROMO subscription plan") else: print(" [DRY RUN] Would create PLAN-RAB-PROMO subscription plan") else: print(" PLAN-RAB-PROMO already exists") # ═══════════════════════════════════════════════════════════════ # STEP 3: Map delivery_id → ERPNext Subscription + Customer # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 3: MAP LEGACY → ERPNEXT") print("=" * 60) # Get all subscriptions with legacy_service_id subs = frappe.db.sql(""" SELECT name, legacy_service_id, party, service_location, actual_price, item_code, status FROM "tabSubscription" WHERE legacy_service_id IS NOT NULL """, as_dict=True) sub_by_legacy = {} for s in subs: lid = s.get("legacy_service_id") if lid: sub_by_legacy[lid] = s print("Mapped ERPNext subscriptions: {}".format(len(sub_by_legacy))) # Map account_id → customer customers = frappe.db.sql(""" SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL """, as_dict=True) cust_by_acct = {} for c in customers: aid = c.get("legacy_account_id") if aid: cust_by_acct[int(aid)] = c["name"] print("Mapped customers: {}".format(len(cust_by_acct))) # ═══════════════════════════════════════════════════════════════ # STEP 4: Process each delivery # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 4: CREATE RAB-PROMO SUBSCRIPTIONS") print("=" * 60) created = 0 updated = 0 skipped_no_customer = 0 skipped_no_sub = 0 errors = 0 for case in no_rebate_cases: did = case["delivery_id"] acct_id = case["account_id"] discount = case["discount_total"] # Find customer + service_location from existing subscriptions (not from legacy_account_id) service_location = None customer = None any_sub = None for s in case["all_services"]: erp_sub = sub_by_legacy.get(s["svc_id"]) if erp_sub: service_location = erp_sub.get("service_location") customer = erp_sub.get("party") any_sub = erp_sub break if not customer: # Fallback to legacy_account_id mapping customer = cust_by_acct.get(acct_id) if not customer: skipped_no_customer += 1 continue if not any_sub: skipped_no_sub += 1 continue # Build description from hijack_desc of discount services descs = [] for s in case["discount_services"]: if s["hijack_desc"]: descs.append(s["hijack_desc"]) description = "; ".join(descs) if descs else "Rabais loyauté" # Step 4a: Update positive products to catalog price for s in case["discount_services"]: erp_sub = sub_by_legacy.get(s["svc_id"]) if erp_sub: # If it's a "fake rebate" (positive product with negative actual), restore to 0 # If it's a discounted positive, restore to base_price if s["actual_price"] < 0: new_price = 0 # Was used as a discount line, zero it out else: new_price = s["base_price"] if not DRY_RUN: frappe.db.sql(""" UPDATE "tabSubscription" SET actual_price = %s WHERE name = %s """, (new_price, erp_sub["name"])) updated += 1 # Step 4b: Create RAB-PROMO subscription rabais_name = "SUB-RAB-{}-{}".format(did, acct_id) # Check if already exists existing = frappe.db.sql(""" SELECT name FROM "tabSubscription" WHERE name = %s """, (rabais_name,)) if existing: continue if not DRY_RUN: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") today = datetime.now().strftime("%Y-%m-%d") # Insert subscription frappe.db.sql(""" INSERT INTO "tabSubscription" ( name, creation, modified, modified_by, owner, docstatus, party_type, party, service_location, status, actual_price, custom_description, item_code, item_group, item_name, billing_frequency, start_date ) VALUES ( %s, %s, %s, 'Administrator', 'Administrator', 0, 'Customer', %s, %s, 'Active', %s, %s, 'RAB-PROMO', 'Rabais', 'Rabais promotionnel', 'M', %s ) """, ( rabais_name, now, now, customer, service_location, -discount, description, today, )) # Insert Subscription Plan Detail child spd_name = "{}-plan".format(rabais_name) frappe.db.sql(""" INSERT INTO "tabSubscription Plan Detail" ( name, creation, modified, modified_by, owner, docstatus, parent, parentfield, parenttype, idx, plan, qty ) VALUES ( %s, %s, %s, 'Administrator', 'Administrator', 0, %s, 'plans', 'Subscription', 1, 'PLAN-RAB-PROMO', 1 ) """, (spd_name, now, now, rabais_name)) created += 1 if created % 50 == 0 and created > 0 and not DRY_RUN: frappe.db.commit() print(" Processed {}/{}...".format(created, len(no_rebate_cases))) if not DRY_RUN: frappe.db.commit() print("\nRAB-PROMO subscriptions created: {}".format(created)) print("Positive products price-restored: {}".format(updated)) print("Skipped (no customer in ERP): {}".format(skipped_no_customer)) print("Skipped (no subscription in ERP): {}".format(skipped_no_sub)) print("Errors: {}".format(errors)) # ═══════════════════════════════════════════════════════════════ # STEP 5: FIX DELIVERIES WITH EXISTING REBATES # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 5: FIX DELIVERIES WITH EXISTING REBATES") print("=" * 60) clean_adjusted = 0 rebates_adjusted = 0 for case in has_rebate_cases: services = case["all_services"] discount_to_absorb = case["discount_total"] biggest_rebate = min( [s for s in services if s["is_rebate"]], key=lambda s: s["actual_price"] ) # Update positive products to catalog price for s in case["discount_services"]: erp_sub = sub_by_legacy.get(s["svc_id"]) if erp_sub: new_price = s["base_price"] if s["actual_price"] >= 0 else 0 if not DRY_RUN: frappe.db.sql(""" UPDATE "tabSubscription" SET actual_price = %s WHERE name = %s """, (new_price, erp_sub["name"])) clean_adjusted += 1 # Update biggest rebate to absorb difference rebate_sub = sub_by_legacy.get(biggest_rebate["svc_id"]) if rebate_sub: new_rebate_price = biggest_rebate["actual_price"] - discount_to_absorb if not DRY_RUN: frappe.db.sql(""" UPDATE "tabSubscription" SET actual_price = %s WHERE name = %s """, (round(new_rebate_price, 2), rebate_sub["name"])) rebates_adjusted += 1 if not DRY_RUN: frappe.db.commit() print("Positive products restored to catalog: {}".format(clean_adjusted)) print("Rebates adjusted to absorb discount: {}".format(rebates_adjusted)) # ═══════════════════════════════════════════════════════════════ # STEP 6: VERIFY ALL TEST ACCOUNTS # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 6: VERIFY TEST ACCOUNTS") print("=" * 60) if TEST_ACCOUNTS: for acct_id in sorted(TEST_ACCOUNTS): # Find customer from subscriptions (more reliable than legacy_account_id) customer = None for did_check, svcs_check in deliveries.items(): if svcs_check[0]["account_id"] == acct_id: for sc in svcs_check: erp_sc = sub_by_legacy.get(sc["svc_id"]) if erp_sc: customer = erp_sc["party"] break if customer: break if not customer: customer = cust_by_acct.get(acct_id) if not customer: print("\n Account {} — no customer in ERP".format(acct_id)) continue cust_info = frappe.db.sql(""" SELECT customer_name FROM "tabCustomer" WHERE name = %s """, (customer,), as_dict=True) cust_name = cust_info[0]["customer_name"] if cust_info else customer subs_list = frappe.db.sql(""" SELECT name, item_code, item_name, actual_price, custom_description, service_location, status FROM "tabSubscription" WHERE party = %s ORDER BY service_location, actual_price DESC """, (customer,), as_dict=True) print("\n {} (account {}) — {} subs".format(cust_name, acct_id, len(subs_list))) current_loc = None loc_total = 0 grand_total = 0 for s in subs_list: loc = s.get("service_location") or "?" if loc != current_loc: if current_loc: print(" SUBTOTAL: ${:.2f}".format(loc_total)) current_loc = loc loc_total = 0 print(" [{}]".format(loc[:60])) price = float(s["actual_price"] or 0) loc_total += price grand_total += price is_rab = price < 0 indent = " " if is_rab else " " desc = s.get("custom_description") or "" print(" {}{:<14} {:>8.2f} {}{}".format( indent, (s["item_code"] or "")[:14], price, (s["item_name"] or "")[:40], " [{}]".format(desc[:40]) if desc else "")) if current_loc: print(" SUBTOTAL: ${:.2f}".format(loc_total)) print(" GRAND TOTAL: ${:.2f}".format(grand_total)) # Global stats total_subs = frappe.db.sql('SELECT COUNT(*) FROM "tabSubscription"')[0][0] rab_promo_count = frappe.db.sql( 'SELECT COUNT(*) FROM "tabSubscription" WHERE item_code = %s', ('RAB-PROMO',) )[0][0] print("\nTotal subscriptions: {}".format(total_subs)) print("RAB-PROMO subscriptions: {}".format(rab_promo_count)) frappe.clear_cache() print("\nDone — cache cleared")