- 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>
93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
"""
|
|
Disable customers with no active Service Subscriptions.
|
|
These are legacy accounts (moved, cancelled, etc.) that were imported as enabled.
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/cleanup_customer_status.py
|
|
"""
|
|
import frappe
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
|
|
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)
|
|
|
|
T_TOTAL = time.time()
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 1: Identify customers to disable
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 1: IDENTIFY INACTIVE CUSTOMERS")
|
|
print("="*60)
|
|
|
|
# Customers that are active but have NO active subscriptions
|
|
to_disable = frappe.db.sql("""
|
|
SELECT c.name FROM "tabCustomer" c
|
|
WHERE c.disabled = 0
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM "tabService Subscription" ss
|
|
WHERE ss.customer = c.name AND ss.status = %s
|
|
)
|
|
""", ("Actif",))
|
|
|
|
to_disable_names = [r[0] for r in to_disable]
|
|
print("Customers to disable (no active subscriptions): {}".format(len(to_disable_names)))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 2: Disable them
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 2: DISABLE CUSTOMERS")
|
|
print("="*60)
|
|
|
|
if to_disable_names:
|
|
# Batch update in chunks of 1000
|
|
for i in range(0, len(to_disable_names), 1000):
|
|
batch = to_disable_names[i:i+1000]
|
|
placeholders = ", ".join(["%s"] * len(batch))
|
|
frappe.db.sql("""
|
|
UPDATE "tabCustomer" SET disabled = 1
|
|
WHERE name IN ({})
|
|
""".format(placeholders), tuple(batch))
|
|
frappe.db.commit()
|
|
print(" Disabled batch {}-{}".format(i+1, min(i+1000, len(to_disable_names))))
|
|
|
|
print("Disabled {} customers".format(len(to_disable_names)))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 3: VERIFY
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 3: VERIFY")
|
|
print("="*60)
|
|
|
|
by_status = frappe.db.sql("""
|
|
SELECT disabled, COUNT(*) as cnt FROM "tabCustomer"
|
|
GROUP BY disabled ORDER BY disabled
|
|
""", as_dict=True)
|
|
print("Customer status after cleanup:")
|
|
for r in by_status:
|
|
label = "Active (Abonné)" if r["disabled"] == 0 else "Disabled (Inactif)"
|
|
print(" {}: {}".format(label, r["cnt"]))
|
|
|
|
# Cross-check: all active customers should have at least one active sub
|
|
active_no_sub = frappe.db.sql("""
|
|
SELECT COUNT(*) FROM "tabCustomer" c
|
|
WHERE c.disabled = 0
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM "tabService Subscription" ss
|
|
WHERE ss.customer = c.name AND ss.status = %s
|
|
)
|
|
""", ("Actif",))[0][0]
|
|
print("\nActive customers with no active subscription: {} (should be 0)".format(active_no_sub))
|
|
|
|
elapsed = time.time() - T_TOTAL
|
|
print("\n" + "="*60)
|
|
print("DONE in {:.1f}s".format(elapsed))
|
|
print("="*60)
|