""" 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="10.100.80.100", 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}")