Backend services: - targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas, extract dispatch scoring weights, trim section dividers across 9 files - modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(), consolidate DM query factory, fix duplicate username fill bug, trim headers (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%) Frontend: - useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into 6 focused helpers (processOnlineStatus, processWanIPs, processRadios, processMeshNodes, processClients, checkRadioIssues) - EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments Documentation (17 → 13 files, -1,400 lines): - New consolidated README.md (architecture, services, dependencies, auth) - Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md - Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md - Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md - Update ROADMAP.md with current phase status - Delete CONTEXT.md (absorbed into README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
462 lines
17 KiB
Python
462 lines
17 KiB
Python
"""
|
|
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="legacy-db", 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")
|