""" Analyze pricing cleanup: show catalog price for services, keep rebates as-is, adjust biggest rebate to absorb the difference when hijack < catalog. Rules: 1. Positive products: show MAX(hijack_price, base_price) — catalog or higher 2. Negative products (rebates): show hijack_price as-is 3. If hijack_price < base_price on a positive product, the difference gets added to the biggest rebate at that delivery address 4. Total per delivery must stay the same Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/analyze_pricing_cleanup.py """ import frappe import pymysql import os from datetime import datetime 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="*******", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) # ═══════════════════════════════════════════════════════════════ # LOAD LEGACY DATA # ═══════════════════════════════════════════════════════════════ print("Loading legacy data...") with conn.cursor() as cur: # All active services with product info 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, 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 ORDER BY s.delivery_id, p.price DESC """) all_services = cur.fetchall() conn.close() print("Active services loaded: {}".format(len(all_services))) # Group by delivery_id 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 # Product is inherently a rebate (SKU like RAB24M, RAB2X) 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 "", "account_id": s["account_id"], }) print("Deliveries with active services: {}".format(len(deliveries))) # ═══════════════════════════════════════════════════════════════ # APPLY PRICING RULES # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("PRICING ANALYSIS") print("=" * 70) total_deliveries_affected = 0 total_services_adjusted = 0 adjustments = [] # [(delivery_id, account_id, old_total, new_total, services)] for did, services in deliveries.items(): old_total = sum(s["actual_price"] for s in services) # Step 1: Determine new display prices discount_to_absorb = 0.0 for s in services: if s["is_rebate"]: # Rebate product: keep actual price s["new_price"] = s["actual_price"] else: # Positive product if s["actual_price"] < s["base_price"]: # Hijack made it cheaper → show catalog, track discount discount_to_absorb += (s["base_price"] - s["actual_price"]) s["new_price"] = s["base_price"] s["adjusted"] = True else: # Hijack same or higher → keep actual (custom service) s["new_price"] = s["actual_price"] s["adjusted"] = False # Step 2: Find biggest rebate and add discount_to_absorb to it rebate_adjusted = False if discount_to_absorb > 0.005: # Find the rebate with the most negative price rebates = [s for s in services if s["is_rebate"]] if rebates: biggest_rebate = min(rebates, key=lambda s: s["new_price"]) biggest_rebate["new_price"] -= discount_to_absorb biggest_rebate["absorbed"] = discount_to_absorb rebate_adjusted = True total_deliveries_affected += 1 else: # No rebate exists — need to create one? Or leave as-is # For now, mark as needing attention pass new_total = sum(s["new_price"] for s in services) # Verify totals match if abs(old_total - new_total) > 0.02: adjustments.append((did, services[0]["account_id"], old_total, new_total, services, "MISMATCH")) elif rebate_adjusted: adjustments.append((did, services[0]["account_id"], old_total, new_total, services, "OK")) for s in services: if s.get("adjusted"): total_services_adjusted += 1 print("\nDeliveries affected (rebate adjusted): {}".format(total_deliveries_affected)) print("Individual services price-restored to catalog: {}".format(total_services_adjusted)) # Count mismatches mismatches = [a for a in adjustments if a[5] == "MISMATCH"] ok_adjustments = [a for a in adjustments if a[5] == "OK"] print("Clean adjustments (total preserved): {}".format(len(ok_adjustments))) print("MISMATCHES (total changed): {}".format(len(mismatches))) # ═══════════════════════════════════════════════════════════════ # SHOW DETAILED EXAMPLES # ═══════════════════════════════════════════════════════════════ # Find Expro Transit (account 3673) deliveries expro = [a for a in adjustments if a[1] == 3673] print("\n" + "=" * 70) print("EXPRO TRANSIT (account 3673) — {} deliveries affected".format(len(expro))) print("=" * 70) for did, acct_id, old_total, new_total, services, status in expro: print("\n Delivery {} — {} [total: {:.2f}]".format(did, status, old_total)) print(" {:<14} {:>10} {:>10} {:>10} {}".format( "SKU", "BASE", "ACTUAL", "NEW", "NOTE")) print(" " + "-" * 66) for s in sorted(services, key=lambda x: (x["is_rebate"], -x["new_price"])): note = "" if s.get("adjusted"): note = "← restored to catalog (+{:.2f} to rebate)".format(s["base_price"] - s["actual_price"]) if s.get("absorbed"): note = "← absorbed {:.2f} discount".format(s["absorbed"]) marker = " " if not s["is_rebate"] else " " print(" {}{:<12} {:>10.2f} {:>10.2f} {:>10.2f} {}".format( marker, s["sku"], s["base_price"], s["actual_price"], s["new_price"], note)) # ═══════════════════════════════════════════════════════════════ # SHOW OTHER SAMPLE CUSTOMERS # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("SAMPLE OTHER CUSTOMERS") print("=" * 70) # Get customer names for sample accounts sample_accts = set() for a in ok_adjustments[:20]: sample_accts.add(a[1]) if sample_accts: acct_list = list(sample_accts)[:5] for acct_id in acct_list: cust = frappe.db.sql(""" SELECT name, customer_name FROM "tabCustomer" WHERE legacy_account_id = %s LIMIT 1 """, (acct_id,), as_dict=True) cust_name = cust[0]["customer_name"] if cust else "account {}".format(acct_id) acct_adjustments = [a for a in adjustments if a[1] == acct_id] print("\n {} (account {}) — {} deliveries".format(cust_name, acct_id, len(acct_adjustments))) for did, _, old_total, new_total, services, status in acct_adjustments[:2]: print(" Delivery {} [total: {:.2f}] {}".format(did, old_total, status)) for s in sorted(services, key=lambda x: (x["is_rebate"], -x["new_price"])): if s.get("adjusted") or s.get("absorbed"): marker = " " if not s["is_rebate"] else " " note = "" if s.get("adjusted"): note = "catalog:{:.2f} actual:{:.2f} → NEW:{:.2f}".format( s["base_price"], s["actual_price"], s["new_price"]) if s.get("absorbed"): note = "absorbed {:.2f} → NEW:{:.2f}".format(s["absorbed"], s["new_price"]) print(" {}{:<12} {}".format(marker, s["sku"], note)) # ═══════════════════════════════════════════════════════════════ # DELIVERIES WITHOUT REBATES (discount but no rebate to absorb) # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("PROBLEM CASES: discount but NO rebate to absorb it") print("=" * 70) no_rebate_discount = 0 for did, services in deliveries.items(): has_discount = False has_rebate = False for s in services: if not s["is_rebate"] and s["actual_price"] < s["base_price"] - 0.01: has_discount = True if s["is_rebate"]: has_rebate = True if has_discount and not has_rebate: no_rebate_discount += 1 if no_rebate_discount <= 5: print(" Delivery {}: services with discount but no rebate product".format(did)) for s in services: if s["actual_price"] < s["base_price"] - 0.01: print(" {} base={:.2f} actual={:.2f} diff={:.2f}".format( s["sku"], s["base_price"], s["actual_price"], s["base_price"] - s["actual_price"])) print("\nTotal deliveries with discount but no rebate: {}".format(no_rebate_discount)) # ═══════════════════════════════════════════════════════════════ # GLOBAL STATS # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("GLOBAL SUMMARY") print("=" * 70) total_svc = len(all_services) hijacked = sum(1 for s in all_services if s["hijack"]) hijacked_lower = sum(1 for did, svcs in deliveries.items() for s in svcs if not s["is_rebate"] and s["actual_price"] < s["base_price"] - 0.01) hijacked_higher = sum(1 for did, svcs in deliveries.items() for s in svcs if not s["is_rebate"] and s["actual_price"] > s["base_price"] + 0.01 and s["hijack"]) print("Total active services: {:,}".format(total_svc)) print("Hijacked (custom price): {:,}".format(hijacked)) print(" ↳ cheaper than catalog: {:,} (restore to catalog + absorb in rebate)".format(hijacked_lower)) print(" ↳ more expensive than catalog: {:,} (keep actual — custom service)".format(hijacked_higher)) print("Deliveries needing rebate adjustment: {:,}".format(total_deliveries_affected)) print("Deliveries with no rebate to absorb: {:,}".format(no_rebate_discount)) print("\n" + "=" * 70) print("ANALYSIS COMPLETE — no changes made") print("=" * 70)