""" Import missing payments for Expro Transit Inc (account 3673). Creates Payment Entry documents in ERPNext from legacy data. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_expro_payments.py """ import frappe import pymysql import os 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) conn = pymysql.connect( host="legacy-db", user="facturation", password="*******", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) ACCOUNT_ID = 3673 CUSTOMER = "CUST-cbf03814b9" COMPANY = "TARGO" # ═══════════════════════════════════════════════════════════════ # STEP 1: Load legacy data # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 1: LOAD LEGACY DATA") print("=" * 60) with conn.cursor() as cur: cur.execute(""" SELECT p.id, p.date_orig, p.amount, p.type, p.reference FROM payment p WHERE p.account_id = %s ORDER BY p.date_orig ASC """, (ACCOUNT_ID,)) legacy_payments = cur.fetchall() cur.execute(""" SELECT pi.payment_id, pi.invoice_id, pi.amount FROM payment_item pi JOIN payment p ON p.id = pi.payment_id WHERE p.account_id = %s """, (ACCOUNT_ID,)) legacy_allocs = cur.fetchall() conn.close() # Build allocation map alloc_map = {} for a in legacy_allocs: pid = a["payment_id"] if pid not in alloc_map: alloc_map[pid] = [] alloc_map[pid].append({ "invoice_id": a["invoice_id"], "amount": float(a["amount"] or 0) }) print("Legacy payments: {}".format(len(legacy_payments))) print("Legacy allocations: {}".format(len(legacy_allocs))) # ═══════════════════════════════════════════════════════════════ # STEP 2: Find which ones already exist # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 2: FIND MISSING PAYMENTS") print("=" * 60) erp_pes = frappe.db.sql(""" SELECT name FROM "tabPayment Entry" WHERE party = %s """, (CUSTOMER,), as_dict=True) erp_pe_ids = set() for pe in erp_pes: try: erp_pe_ids.add(int(pe["name"].split("-")[1])) except: pass # Check which invoices exist erp_invs = frappe.db.sql(""" SELECT name FROM "tabSales Invoice" WHERE customer = %s AND docstatus = 1 """, (CUSTOMER,), as_dict=True) erp_inv_names = set(i["name"] for i in erp_invs) to_create = [] skipped_exists = 0 skipped_no_inv = 0 for p in legacy_payments: if p["id"] in erp_pe_ids: skipped_exists += 1 continue dt = datetime.fromtimestamp(p["date_orig"]).strftime("%Y-%m-%d") if p["date_orig"] else None amount = float(p["amount"] or 0) ptype = (p["type"] or "").strip() ref = (p["reference"] or "").strip() # Get allocations and verify invoices exist allocations = alloc_map.get(p["id"], []) valid_allocs = [] for a in allocations: sinv_name = "SINV-{}".format(a["invoice_id"]) if sinv_name in erp_inv_names: valid_allocs.append({ "reference_doctype": "Sales Invoice", "reference_name": sinv_name, "allocated_amount": a["amount"], }) else: skipped_no_inv += 1 to_create.append({ "name": "PE-{}".format(p["id"]), "date": dt, "amount": amount, "type": ptype, "reference": ref, "allocations": valid_allocs, }) print("Already exists: {}".format(skipped_exists)) print("TO CREATE: {}".format(len(to_create))) print("Allocs skipped (inv not found): {}".format(skipped_no_inv)) # ═══════════════════════════════════════════════════════════════ # STEP 3: Get/create accounts # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 3: RESOLVE ACCOUNTS") print("=" * 60) # Use the same accounts as existing Payment Entries for this customer existing_pe = frappe.db.sql(""" SELECT paid_from, paid_to FROM "tabPayment Entry" WHERE party = %s AND docstatus = 1 LIMIT 1 """, (CUSTOMER,), as_dict=True) if existing_pe: receivable = existing_pe[0]["paid_from"] paid_to = existing_pe[0]["paid_to"] else: receivable = "Comptes clients - T" paid_to = "Banque - T" print("Receivable account (paid_from): {}".format(receivable)) print("Bank account (paid_to): {}".format(paid_to)) if not receivable or not paid_to: print("ERROR: Missing accounts!") exit() # ═══════════════════════════════════════════════════════════════ # STEP 4: CREATE PAYMENT ENTRIES VIA DIRECT SQL # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 4: CREATE PAYMENT ENTRIES") print("=" * 60) created = 0 errors = 0 for p in to_create: try: pe_name = p["name"] posting_date = p["date"] or "2012-01-01" amount = p["amount"] # Insert Payment Entry frappe.db.sql(""" INSERT INTO "tabPayment Entry" ( name, creation, modified, modified_by, owner, docstatus, naming_series, payment_type, posting_date, company, party_type, party, party_name, paid_from, paid_to, paid_amount, received_amount, target_exchange_rate, source_exchange_rate, paid_from_account_currency, paid_to_account_currency, reference_no, reference_date, mode_of_payment, status ) VALUES ( %s, %s, %s, 'Administrator', 'Administrator', 1, 'ACC-PAY-.YYYY.-', 'Receive', %s, %s, 'Customer', %s, %s, %s, %s, %s, %s, 1.0, 1.0, 'CAD', 'CAD', %s, %s, %s, 'Submitted' ) """, ( pe_name, posting_date, posting_date, posting_date, COMPANY, CUSTOMER, "Expro Transit Inc.", receivable, paid_to, amount, amount, p["reference"] or pe_name, posting_date, None, )) # Insert Payment Entry References (allocations) for idx, alloc in enumerate(p["allocations"], 1): ref_name = "{}-ref-{}".format(pe_name, idx) frappe.db.sql(""" INSERT INTO "tabPayment Entry Reference" ( name, creation, modified, modified_by, owner, docstatus, idx, parent, parentfield, parenttype, reference_doctype, reference_name, allocated_amount, total_amount, outstanding_amount, exchange_rate ) VALUES ( %s, %s, %s, 'Administrator', 'Administrator', 1, %s, %s, 'references', 'Payment Entry', %s, %s, %s, 0, 0, 1.0 ) """, ( ref_name, posting_date, posting_date, idx, pe_name, alloc["reference_doctype"], alloc["reference_name"], alloc["allocated_amount"], )) created += 1 except Exception as e: errors += 1 if errors <= 5: print(" ERROR on {}: {}".format(p["name"], str(e)[:100])) if created % 50 == 0 and created > 0: frappe.db.commit() print(" Created {}/{}...".format(created, len(to_create))) frappe.db.commit() print("\nCreated: {}".format(created)) print("Errors: {}".format(errors)) # ═══════════════════════════════════════════════════════════════ # STEP 5: VERIFY # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 60) print("STEP 5: VERIFY") print("=" * 60) # Count payments now pe_after = frappe.db.sql(""" SELECT COUNT(*) as cnt, COALESCE(SUM(paid_amount), 0) as total FROM "tabPayment Entry" WHERE party = %s AND docstatus = 1 """, (CUSTOMER,), as_dict=True)[0] print("Payment Entries after import: {} for ${:,.2f}".format(pe_after["cnt"], float(pe_after["total"]))) # Check a few samples samples = frappe.db.sql(""" SELECT pe.name, pe.posting_date, pe.paid_amount, (SELECT COUNT(*) FROM "tabPayment Entry Reference" per WHERE per.parent = pe.name) as ref_count FROM "tabPayment Entry" pe WHERE pe.party = %s AND pe.docstatus = 1 ORDER BY pe.posting_date DESC LIMIT 10 """, (CUSTOMER,), as_dict=True) print("\nRecent payments:") for s in samples: print(" {} date={} amount={:,.2f} refs={}".format(s["name"], s["posting_date"], float(s["paid_amount"]), s["ref_count"])) frappe.clear_cache() print("\nDone — cache cleared")