gigafibre-fsm/scripts/migration/fix_reversement.py
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
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>
2026-04-13 08:39:58 -04:00

169 lines
5.8 KiB
Python

"""
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))