""" Simulate importing missing payments for Expro Transit Inc (account 3673). DRY RUN — reads legacy data, shows what would be created, and produces a visual timeline of invoice vs payment balance. Run inside erpnext-backend-1: /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/simulate_payment_import.py """ import frappe import pymysql import os import json from datetime import datetime os.chdir("/home/frappe/frappe-bench/sites") frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() 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" CUSTOMER_NAME = "Expro Transit Inc." # ═══════════════════════════════════════════════════════════════ # STEP 1: Load all legacy payments + allocations # ═══════════════════════════════════════════════════════════════ with conn.cursor() as cur: cur.execute(""" SELECT p.id, p.date_orig, p.amount, p.applied_amt, 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: payment_id -> [{invoice_id, amount}] 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) }) # Which PEs already exist in ERPNext? erp_pes = frappe.db.sql(""" SELECT name FROM "tabPayment Entry" WHERE party = %s AND docstatus = 1 """, (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 # ═══════════════════════════════════════════════════════════════ # STEP 2: Load all ERPNext invoices # ═══════════════════════════════════════════════════════════════ erp_invoices = frappe.db.sql(""" SELECT name, posting_date, grand_total, outstanding_amount, status FROM "tabSales Invoice" WHERE customer = %s AND docstatus = 1 ORDER BY posting_date ASC """, (CUSTOMER,), as_dict=True) inv_map = {} for inv in erp_invoices: inv_map[inv["name"]] = { "date": str(inv["posting_date"]), "total": float(inv["grand_total"]), "outstanding": float(inv["outstanding_amount"]), "status": inv["status"], } # ═══════════════════════════════════════════════════════════════ # STEP 3: Determine which payments to create # ═══════════════════════════════════════════════════════════════ to_create = [] for p in legacy_payments: if p["id"] in erp_pe_ids: continue # Already exists 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() # Map legacy type to ERPNext mode mode_map = { "paiement direct": "Virement", "cheque": "Chèque", "carte credit": "Carte de crédit", "credit": "Note de crédit", "reversement": "Note de crédit", } mode = mode_map.get(ptype, ptype) # Get allocations allocations = alloc_map.get(p["id"], []) refs = [] for a in allocations: sinv_name = "SINV-{}".format(a["invoice_id"]) refs.append({ "reference_doctype": "Sales Invoice", "reference_name": sinv_name, "allocated_amount": a["amount"], }) to_create.append({ "pe_name": "PE-{}".format(p["id"]), "legacy_id": p["id"], "date": dt, "amount": amount, "type": ptype, "mode": mode, "reference": ref, "allocations": refs, }) print("\n" + "=" * 70) print("PAYMENT IMPORT SIMULATION — {} ({})".format(CUSTOMER_NAME, CUSTOMER)) print("=" * 70) print("Legacy payments: {}".format(len(legacy_payments))) print("Already in ERPNext: {}".format(len(erp_pe_ids))) print("TO CREATE: {}".format(len(to_create))) total_to_create = sum(p["amount"] for p in to_create) print("Total to import: ${:,.2f}".format(total_to_create)) # ═══════════════════════════════════════════════════════════════ # STEP 4: Build timeline showing running balance # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("VISUAL BALANCE TIMELINE (with new payments)") print("=" * 70) # Combine all events: invoices and payments (existing + to-create) events = [] # Add invoices for inv in erp_invoices: events.append({ "date": str(inv["posting_date"]), "type": "INV", "name": inv["name"], "amount": float(inv["grand_total"]), "detail": inv["status"], }) # Add existing ERPNext payments existing_pes = frappe.db.sql(""" SELECT name, posting_date, paid_amount FROM "tabPayment Entry" WHERE party = %s AND docstatus = 1 """, (CUSTOMER,), as_dict=True) for pe in existing_pes: events.append({ "date": str(pe["posting_date"]), "type": "PAY", "name": pe["name"], "amount": float(pe["paid_amount"]), "detail": "existing", }) # Add new (simulated) payments for p in to_create: events.append({ "date": p["date"] or "2012-01-01", "type": "PAY", "name": p["pe_name"], "amount": p["amount"], "detail": "NEW " + p["mode"], }) # Sort by date, then INV before PAY on same date events.sort(key=lambda e: (e["date"], 0 if e["type"] == "INV" else 1)) # Print timeline with running balance balance = 0.0 total_invoiced = 0.0 total_paid = 0.0 # Group by year for readability current_year = None print("\n{:<12} {:<5} {:<16} {:>12} {:>12} {}".format( "DATE", "TYPE", "DOCUMENT", "AMOUNT", "BALANCE", "DETAIL")) print("-" * 85) for e in events: year = e["date"][:4] if year != current_year: if current_year: print(" {:>55} {:>12}".format("--- year-end ---", "${:,.2f}".format(balance))) current_year = year print("\n ── {} ──".format(year)) if e["type"] == "INV": balance += e["amount"] total_invoiced += e["amount"] sign = "+" else: balance -= e["amount"] total_paid += e["amount"] sign = "-" marker = " ◀ NEW" if "NEW" in e.get("detail", "") else "" print("{:<12} {:<5} {:<16} {:>12} {:>12} {}{}".format( e["date"], e["type"], e["name"], "{}${:,.2f}".format(sign, abs(e["amount"])), "${:,.2f}".format(balance), e["detail"], marker, )) print("-" * 85) print("\nFINAL SUMMARY:") print(" Total invoiced: ${:,.2f}".format(total_invoiced)) print(" Total paid: ${:,.2f}".format(total_paid)) print(" Final balance: ${:,.2f}".format(balance)) print(" Should be: $0.00" if abs(balance) < 0.02 else " ⚠ MISMATCH!") # ═══════════════════════════════════════════════════════════════ # STEP 5: Show sample of what the PE documents would look like # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("SAMPLE PAYMENT ENTRY DOCUMENTS (first 5)") print("=" * 70) for p in to_create[:5]: print("\n PE Name: {}".format(p["pe_name"])) print(" Date: {}".format(p["date"])) print(" Amount: ${:,.2f}".format(p["amount"])) print(" Mode: {}".format(p["mode"])) print(" Reference: {}".format(p["reference"] or "—")) print(" Allocations:") for a in p["allocations"]: print(" → {} = ${:,.2f}".format(a["reference_name"], a["allocated_amount"])) # ═══════════════════════════════════════════════════════════════ # STEP 6: Identify potential issues # ═══════════════════════════════════════════════════════════════ print("\n" + "=" * 70) print("POTENTIAL ISSUES") print("=" * 70) # Check for payments referencing invoices that don't exist in ERPNext missing_invs = set() for p in to_create: for a in p["allocations"]: if a["reference_name"] not in inv_map: missing_invs.add(a["reference_name"]) if missing_invs: print("⚠ {} payments reference invoices NOT in ERPNext:".format(len(missing_invs))) for m in sorted(missing_invs)[:10]: print(" {}".format(m)) else: print("✓ All payment allocations reference existing invoices") # Check for credits (negative-type payments) credits = [p for p in to_create if p["type"] in ("credit", "reversement")] if credits: print("\n⚠ {} credit/reversement entries (not regular payments):".format(len(credits))) for c in credits: print(" {} date={} amount=${:,.2f} type={}".format(c["pe_name"], c["date"], c["amount"], c["type"])) else: print("✓ No credit entries to handle specially") # Payments without allocations no_alloc = [p for p in to_create if not p["allocations"]] if no_alloc: print("\n⚠ {} payments without invoice allocations:".format(len(no_alloc))) for p in no_alloc: print(" {} date={} amount=${:,.2f}".format(p["pe_name"], p["date"], p["amount"])) else: print("✓ All payments have invoice allocations") print("\n" + "=" * 70) print("DRY RUN COMPLETE — no changes made") print("=" * 70)