gigafibre-fsm/scripts/migration/fix_invoice_customer_names.py
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
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>
2026-04-13 08:39:58 -04:00

191 lines
7.0 KiB
Python

"""
Fix customer_name on all Sales Invoices + Payment Entries.
During migration, customer_name was set to CUST-xxx instead of the actual name.
This script updates it from the Customer doctype.
Also imports legacy invoice.notes as Comments on Sales Invoice
(e.g. "Renversement de la facture #635893 - Sera facturé dans fiche personnelle")
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/fix_invoice_customer_names.py
"""
import os, sys, time
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
os.chdir("/home/frappe/frappe-bench/sites")
import frappe
frappe.init(site="erp.gigafibre.ca", sites_path=".")
frappe.connect()
print("Connected:", frappe.local.site)
DRY_RUN = False
# ═══════════════════════════════════════════════════════════════
# PHASE 1: Fix customer_name on Sales Invoices
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 60)
print("PHASE 1: Fix customer_name on Sales Invoices")
print("=" * 60)
t0 = time.time()
# Build customer name map
cust_map = {}
rows = frappe.db.sql('SELECT name, customer_name FROM "tabCustomer"', as_dict=True)
for r in rows:
cust_map[r['name']] = r['customer_name']
print(f" Loaded {len(cust_map)} customer names")
# Count broken invoices
broken = frappe.db.sql(
"""SELECT COUNT(*) FROM "tabSales Invoice" WHERE customer_name LIKE 'CUST-%%'"""
)[0][0]
print(f" Invoices with CUST-xxx name: {broken}")
if not DRY_RUN and broken > 0:
# Bulk update using a single UPDATE ... FROM
updated = frappe.db.sql("""
UPDATE "tabSales Invoice" si
SET customer_name = c.customer_name
FROM "tabCustomer" c
WHERE si.customer = c.name
AND si.customer_name LIKE 'CUST-%%'
""")
frappe.db.commit()
print(f" Updated Sales Invoices [{time.time()-t0:.0f}s]")
# ═══════════════════════════════════════════════════════════════
# PHASE 2: Fix customer_name on Payment Entries
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 60)
print("PHASE 2: Fix customer_name on Payment Entries")
print("=" * 60)
t0 = time.time()
broken_pe = frappe.db.sql(
"""SELECT COUNT(*) FROM "tabPayment Entry" WHERE party_name LIKE 'CUST-%%'"""
)[0][0]
print(f" Payment Entries with CUST-xxx name: {broken_pe}")
if not DRY_RUN and broken_pe > 0:
frappe.db.sql("""
UPDATE "tabPayment Entry" pe
SET party_name = c.customer_name
FROM "tabCustomer" c
WHERE pe.party = c.name
AND pe.party_name LIKE 'CUST-%%'
""")
frappe.db.commit()
print(f" Updated Payment Entries [{time.time()-t0:.0f}s]")
# ═══════════════════════════════════════════════════════════════
# PHASE 3: Import legacy invoice notes as Comments
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 60)
print("PHASE 3: Import invoice notes as Comments")
print("=" * 60)
t0 = time.time()
import pymysql
legacy = pymysql.connect(
host="legacy-db", user="facturation", password="VD67owoj",
database="gestionclient", cursorclass=pymysql.cursors.DictCursor
)
# Get all invoices with notes
with legacy.cursor() as cur:
cur.execute("""
SELECT id, notes, account_id, FROM_UNIXTIME(date_orig) as date_created
FROM invoice
WHERE notes IS NOT NULL AND notes != '' AND TRIM(notes) != ''
ORDER BY id
""")
noted_invoices = cur.fetchall()
print(f" Legacy invoices with notes: {len(noted_invoices)}")
# Check which SINV exist in ERPNext
existing_sinv = set()
rows = frappe.db.sql('SELECT name FROM "tabSales Invoice"')
for r in rows:
existing_sinv.add(r[0])
print(f" Existing SINVs in ERPNext: {len(existing_sinv)}")
# Check existing comments to avoid duplicates
existing_comments = set()
rows = frappe.db.sql(
"""SELECT reference_name FROM "tabComment"
WHERE reference_doctype = 'Sales Invoice' AND comment_type = 'Comment'"""
)
for r in rows:
existing_comments.add(r[0])
imported = 0
skipped = 0
batch = []
now = frappe.utils.now()
for inv in noted_invoices:
sinv_name = f"SINV-{inv['id']}"
if sinv_name not in existing_sinv:
skipped += 1
continue
if sinv_name in existing_comments:
skipped += 1
continue
notes = inv['notes'].strip()
if not notes:
continue
creation = str(inv['date_created']) if inv['date_created'] else now
batch.append({
'name': f"inv-note-{inv['id']}",
'comment_type': 'Comment',
'reference_doctype': 'Sales Invoice',
'reference_name': sinv_name,
'content': notes,
'owner': 'Administrator',
'comment_by': 'Système legacy',
'creation': creation,
'modified': creation,
'modified_by': 'Administrator',
})
imported += 1
print(f" Notes to import: {imported}, skipped: {skipped}")
if not DRY_RUN and batch:
# Insert row by row using frappe.db.sql (PostgreSQL compatible)
CHUNK = 5000
cols = list(batch[0].keys())
col_names = ", ".join([f'"{c}"' for c in cols])
placeholders = ", ".join(["%s"] * len(cols))
sql = f'INSERT INTO "tabComment" ({col_names}) VALUES ({placeholders}) ON CONFLICT ("name") DO NOTHING'
for i, row in enumerate(batch):
vals = tuple(row[c] for c in cols)
frappe.db.sql(sql, vals, as_dict=False)
if (i + 1) % CHUNK == 0:
frappe.db.commit()
elapsed = time.time() - t0
rate = (i + 1) / elapsed
print(f" Progress: {i+1}/{len(batch)} ({rate:.0f}/s) [{elapsed:.0f}s]")
frappe.db.commit()
print(f" Imported {imported} invoice notes [{time.time()-t0:.0f}s]")
legacy.close()
# ═══════════════════════════════════════════════════════════════
# SUMMARY
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 60)
print("DONE")
print("=" * 60)
if DRY_RUN:
print(" ** DRY RUN — no changes made **")
print(f" Sales Invoices customer_name fixed: {broken}")
print(f" Payment Entries party_name fixed: {broken_pe}")
print(f" Invoice notes imported as Comments: {imported}")