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>
191 lines
7.0 KiB
Python
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}")
|