""" Fix: Remove 'reversement' Payment Entries that were incorrectly imported. These are system-generated reversal payments in legacy — NOT real customer payments. They double-count with the credit note PLE entries we created in fix_reversals.py. """ import frappe, os, time os.chdir("/home/frappe/frappe-bench/sites") frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() print("Connected:", frappe.local.site) t0 = time.time() # Find all Payment Entries that came from 'reversement' type payments # These have legacy_payment_id set, and we can identify them via legacy DB # But simpler: check which PEs are allocated to invoices that are targets of our reversal matches # First, let's find reversement PEs by checking legacy import pymysql legacy = pymysql.connect( host="legacy-db", user="facturation", password="*******", database="gestionclient", cursorclass=pymysql.cursors.DictCursor ) with legacy.cursor() as cur: cur.execute(""" SELECT id FROM payment WHERE type = 'reversement' AND date_orig >= UNIX_TIMESTAMP('2024-04-08') """) rev_ids = [str(r['id']) for r in cur.fetchall()] legacy.close() print("Legacy reversement payments: {}".format(len(rev_ids))) # Map to ERPNext PE names (PE-{hex10}) pe_names = [] for rid in rev_ids: pe_names.append("PE-{:010x}".format(int(rid))) # Verify these exist existing = frappe.db.sql(""" SELECT name FROM "tabPayment Entry" WHERE name IN ({}) """.format(",".join(["'{}'".format(n) for n in pe_names]))) existing_names = [r[0] for r in existing] print("Existing reversement PEs in ERPNext: {}".format(len(existing_names))) if not existing_names: print("Nothing to delete.") exit() # Load into temp table for efficient joins frappe.db.sql("DROP TABLE IF EXISTS _tmp_rev_pe") frappe.db.commit() frappe.db.sql(""" CREATE TABLE _tmp_rev_pe ( pe_name VARCHAR(140) PRIMARY KEY ) """) frappe.db.commit() for i in range(0, len(existing_names), 5000): batch = existing_names[i:i+5000] values = ",".join(["('{}')".format(n) for n in batch]) frappe.db.sql("INSERT INTO _tmp_rev_pe (pe_name) VALUES {}".format(values)) frappe.db.commit() # Delete PLE entries for these payment entries deleted_ple = frappe.db.sql(""" DELETE FROM "tabPayment Ledger Entry" WHERE voucher_type = 'Payment Entry' AND voucher_no IN (SELECT pe_name FROM _tmp_rev_pe) """) frappe.db.commit() ple_count = frappe.db.sql(""" SELECT COUNT(*) FROM "tabPayment Ledger Entry" WHERE voucher_type = 'Payment Entry' AND voucher_no IN (SELECT pe_name FROM _tmp_rev_pe) """)[0][0] print("Remaining PLE for reversement PEs: {} (should be 0)".format(ple_count)) # Delete GL entries for these payment entries frappe.db.sql(""" DELETE FROM "tabGL Entry" WHERE voucher_type = 'Payment Entry' AND voucher_no IN (SELECT pe_name FROM _tmp_rev_pe) """) frappe.db.commit() # Delete Payment Entry References frappe.db.sql(""" DELETE FROM "tabPayment Entry Reference" WHERE parent IN (SELECT pe_name FROM _tmp_rev_pe) """) frappe.db.commit() # Delete Payment Entries themselves frappe.db.sql(""" DELETE FROM "tabPayment Entry" WHERE name IN (SELECT pe_name FROM _tmp_rev_pe) """) frappe.db.commit() print("Deleted {} reversement Payment Entries and their GL/PLE".format(len(existing_names))) # Recalculate outstanding on ALL invoices that were targets of reversals # (These are the invoices that had both a payment AND a credit PLE) frappe.db.sql(""" UPDATE "tabSales Invoice" si SET outstanding_amount = COALESCE(( SELECT SUM(ple.amount) FROM "tabPayment Ledger Entry" ple WHERE ple.against_voucher_type = 'Sales Invoice' AND ple.against_voucher_no = si.name AND ple.delinked = 0 ), si.grand_total) WHERE si.docstatus = 1 AND si.is_return != 1 """) frappe.db.commit() # Update statuses frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Paid' WHERE docstatus = 1 AND is_return != 1 AND ROUND(outstanding_amount::numeric, 2) = 0""") frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Overdue' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount > 0.005 AND COALESCE(due_date, posting_date) < CURRENT_DATE""") frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Unpaid' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount > 0.005 AND COALESCE(due_date, posting_date) >= CURRENT_DATE""") frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Credit Note Issued' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount < -0.005""") frappe.db.commit() # Cleanup frappe.db.sql("DROP TABLE IF EXISTS _tmp_rev_pe") frappe.db.commit() # Verification print("\n=== Verification ===") outstanding = frappe.db.sql(""" SELECT ROUND(SUM(CASE WHEN outstanding_amount > 0.005 THEN outstanding_amount ELSE 0 END)::numeric, 2) as owed, ROUND(SUM(CASE WHEN outstanding_amount < -0.005 THEN outstanding_amount ELSE 0 END)::numeric, 2) as overpaid FROM "tabSales Invoice" WHERE docstatus = 1 """, as_dict=True)[0] print(" Outstanding: ${} owed | ${} overpaid".format(outstanding['owed'], outstanding['overpaid'])) statuses = frappe.db.sql(""" SELECT status, COUNT(*) as cnt FROM "tabSales Invoice" WHERE docstatus = 1 GROUP BY status ORDER BY cnt DESC """, as_dict=True) print("\n Status breakdown:") for s in statuses: print(" {}: {}".format(s['status'], s['cnt'])) # GL balance check gl = frappe.db.sql(""" SELECT ROUND(SUM(debit)::numeric, 2) as total_debit, ROUND(SUM(credit)::numeric, 2) as total_credit FROM "tabGL Entry" """, as_dict=True)[0] print("\n GL Balance: debit={} credit={} diff={}".format( gl['total_debit'], gl['total_credit'], round(float(gl['total_debit'] or 0) - float(gl['total_credit'] or 0), 2))) elapsed = time.time() - t0 print("\nDone in {:.0f}s".format(elapsed))