- InlineField component + useInlineEdit composable for Odoo-style dblclick editing - Client search by name, account ID, and legacy_customer_id (or_filters) - SMS/Email notification panel on ContactCard via n8n webhooks - Ticket reply thread via Communication docs - All migration scripts (51 files) now tracked - Client portal and field tech app added to monorepo - README rewritten with full feature list, migration summary, architecture - CHANGELOG updated with all recent work - ROADMAP updated with current completion status - Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN) - .gitignore updated (docker/, .claude/, exports/, .quasar/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
"""
|
|
Fix subscription details: add actual_price, custom_description from legacy hijack data.
|
|
Also populate item_code and item_group for display.
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/fix_subscription_details.py
|
|
"""
|
|
import frappe
|
|
import pymysql
|
|
import os
|
|
import html
|
|
|
|
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)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 1: Add custom fields if they don't exist
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 1: ADD CUSTOM FIELDS")
|
|
print("=" * 60)
|
|
|
|
fields_to_add = [
|
|
("actual_price", "Decimal", "Actual Price"),
|
|
("custom_description", "Small Text", "Custom Description"),
|
|
("item_code", "Data", "Item Code"),
|
|
("item_group", "Data", "Item Group"),
|
|
("billing_frequency", "Data", "Billing Frequency"),
|
|
]
|
|
|
|
for fieldname, fieldtype, label in fields_to_add:
|
|
existing = frappe.db.sql("""
|
|
SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = 'tabSubscription' AND column_name = %s
|
|
""", (fieldname,))
|
|
if not existing:
|
|
# Add column directly
|
|
if fieldtype == "Decimal":
|
|
frappe.db.sql('ALTER TABLE "tabSubscription" ADD COLUMN {} DECIMAL(18,6) DEFAULT 0'.format(fieldname))
|
|
else:
|
|
frappe.db.sql('ALTER TABLE "tabSubscription" ADD COLUMN {} VARCHAR(512)'.format(fieldname))
|
|
print(" Added column: {}".format(fieldname))
|
|
else:
|
|
print(" Column exists: {}".format(fieldname))
|
|
|
|
frappe.db.commit()
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 2: Load legacy service data (hijack prices + descriptions)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 2: LOAD LEGACY SERVICE DATA")
|
|
print("=" * 60)
|
|
|
|
conn = pymysql.connect(
|
|
host="10.100.80.100",
|
|
user="facturation",
|
|
password="*******",
|
|
database="gestionclient",
|
|
cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
with conn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT s.id, s.hijack, s.hijack_price, s.hijack_desc,
|
|
p.sku, p.price as base_price, p.category,
|
|
pc.name as cat_name
|
|
FROM service s
|
|
JOIN product p ON p.id = s.product_id
|
|
LEFT JOIN product_cat pc ON pc.id = p.category
|
|
WHERE s.id > 0
|
|
""")
|
|
legacy_services = {}
|
|
for r in cur.fetchall():
|
|
actual_price = float(r["hijack_price"]) if r["hijack"] else float(r["base_price"] or 0)
|
|
desc = r["hijack_desc"] if r["hijack"] and r["hijack_desc"] else ""
|
|
cat = html.unescape(r["cat_name"]) if r["cat_name"] else ""
|
|
legacy_services[r["id"]] = {
|
|
"actual_price": actual_price,
|
|
"description": desc.strip(),
|
|
"sku": r["sku"],
|
|
"category": cat,
|
|
}
|
|
conn.close()
|
|
|
|
print("Legacy services loaded: {}".format(len(legacy_services)))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 3: Load ERPNext Item info
|
|
# ═══════════════════════════════════════════════════════════════
|
|
items = frappe.db.sql("""
|
|
SELECT name, item_name, item_group FROM "tabItem"
|
|
""", as_dict=True)
|
|
item_map = {i["name"]: i for i in items}
|
|
print("Items loaded: {}".format(len(item_map)))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 4: Update subscriptions
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 3: UPDATE SUBSCRIPTIONS")
|
|
print("=" * 60)
|
|
|
|
# Get all subscriptions with their plan details
|
|
subs = frappe.db.sql("""
|
|
SELECT s.name, s.legacy_service_id,
|
|
spd.plan, sp.item, sp.cost, sp.billing_interval
|
|
FROM "tabSubscription" s
|
|
LEFT JOIN "tabSubscription Plan Detail" spd ON spd.parent = s.name
|
|
LEFT JOIN "tabSubscription Plan" sp ON sp.plan_name = spd.plan
|
|
ORDER BY s.name
|
|
""", as_dict=True)
|
|
|
|
print("Total subscription rows: {}".format(len(subs)))
|
|
|
|
updated = 0
|
|
batch_size = 2000
|
|
for i, sub in enumerate(subs):
|
|
legacy_id = sub.get("legacy_service_id")
|
|
item_code = sub.get("item") or ""
|
|
plan_cost = float(sub.get("cost") or 0)
|
|
|
|
# Get actual price from legacy
|
|
leg = legacy_services.get(legacy_id) if legacy_id else None
|
|
if leg:
|
|
actual_price = leg["actual_price"]
|
|
custom_desc = leg["description"]
|
|
item_code = leg["sku"] or item_code
|
|
item_group = leg["category"]
|
|
else:
|
|
actual_price = plan_cost
|
|
custom_desc = ""
|
|
item_group = item_map.get(item_code, {}).get("item_group", "") if item_code else ""
|
|
|
|
# Billing frequency
|
|
billing_freq = sub.get("billing_interval") or "Month"
|
|
freq_label = "M" if billing_freq == "Month" else "A" if billing_freq == "Year" else billing_freq[:1]
|
|
|
|
frappe.db.sql("""
|
|
UPDATE "tabSubscription"
|
|
SET actual_price = %s, custom_description = %s, item_code = %s, item_group = %s, billing_frequency = %s
|
|
WHERE name = %s
|
|
""", (actual_price, custom_desc, item_code, item_group, freq_label, sub["name"]))
|
|
updated += 1
|
|
|
|
if updated % batch_size == 0:
|
|
frappe.db.commit()
|
|
print(" Updated {}/{}...".format(updated, len(subs)))
|
|
|
|
frappe.db.commit()
|
|
print("Updated: {} subscriptions".format(updated))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STEP 4: VERIFY with Expro
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("STEP 4: VERIFY (Expro Transit)")
|
|
print("=" * 60)
|
|
|
|
expro = frappe.db.sql("""
|
|
SELECT name, item_code, actual_price, custom_description, item_group, billing_frequency,
|
|
service_location, radius_user, status
|
|
FROM "tabSubscription"
|
|
WHERE party = 'CUST-cbf03814b9'
|
|
ORDER BY service_location, actual_price DESC
|
|
""", as_dict=True)
|
|
|
|
for s in expro:
|
|
is_rebate = float(s["actual_price"] or 0) < 0
|
|
indent = " " if is_rebate else " "
|
|
desc = s["custom_description"] or ""
|
|
print("{}{} {:>8.2f} {} {} {}".format(
|
|
indent, (s["item_code"] or "")[:14].ljust(14),
|
|
float(s["actual_price"] or 0),
|
|
(s["billing_frequency"] or "M"),
|
|
s["status"],
|
|
desc[:50]))
|
|
|
|
frappe.clear_cache()
|
|
print("\nDone — cache cleared")
|