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>
212 lines
7.4 KiB
Python
212 lines
7.4 KiB
Python
"""
|
|
Fix reversal invoices: link credit invoices to their original via return_against + PLE.
|
|
These are cancellation invoices created in legacy with no credit payment — just billed_amt = total_amt.
|
|
"""
|
|
import frappe, os, pymysql, 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()
|
|
|
|
legacy = pymysql.connect(
|
|
host="legacy-db", user="facturation", password="*******",
|
|
database="gestionclient", cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
# Get all unlinked settled return invoices
|
|
unlinked = frappe.db.sql("""
|
|
SELECT name, legacy_invoice_id, grand_total, customer, posting_date
|
|
FROM "tabSales Invoice"
|
|
WHERE docstatus = 1 AND is_return = 1
|
|
AND (return_against IS NULL OR return_against = '')
|
|
AND outstanding_amount < -0.005
|
|
ORDER BY outstanding_amount ASC
|
|
""", as_dict=True)
|
|
print("Unlinked settled returns: {}".format(len(unlinked)))
|
|
|
|
# Build legacy_invoice_id → SINV name map
|
|
inv_map = {}
|
|
map_rows = frappe.db.sql("""
|
|
SELECT name, legacy_invoice_id FROM "tabSales Invoice"
|
|
WHERE legacy_invoice_id IS NOT NULL AND docstatus = 1 AND is_return != 1
|
|
""", as_dict=True)
|
|
for r in map_rows:
|
|
inv_map[str(r['legacy_invoice_id'])] = r['name']
|
|
print("Invoice map: {} entries".format(len(inv_map)))
|
|
|
|
# Match each reversal to its original
|
|
matches = [] # (credit_sinv, target_sinv, amount)
|
|
unmatched = 0
|
|
|
|
with legacy.cursor() as cur:
|
|
for ret in unlinked:
|
|
leg_id = ret['legacy_invoice_id']
|
|
amount = float(ret['grand_total'])
|
|
target_amount = -amount
|
|
|
|
cur.execute("SELECT account_id, date_orig FROM invoice WHERE id = %s", (leg_id,))
|
|
credit_inv = cur.fetchone()
|
|
if not credit_inv:
|
|
unmatched += 1
|
|
continue
|
|
|
|
cur.execute("""
|
|
SELECT id FROM invoice
|
|
WHERE account_id = %s
|
|
AND ROUND(total_amt, 2) = ROUND(%s, 2)
|
|
AND total_amt > 0
|
|
AND date_orig <= %s
|
|
AND date_orig >= UNIX_TIMESTAMP('2024-04-08')
|
|
ORDER BY date_orig DESC
|
|
LIMIT 1
|
|
""", (credit_inv['account_id'], target_amount, credit_inv['date_orig']))
|
|
match = cur.fetchone()
|
|
|
|
if match:
|
|
target_sinv = inv_map.get(str(match['id']))
|
|
if target_sinv:
|
|
matches.append((ret['name'], target_sinv, amount))
|
|
else:
|
|
unmatched += 1
|
|
else:
|
|
unmatched += 1
|
|
|
|
legacy.close()
|
|
print("Matched: {} | Unmatched: {}".format(len(matches), unmatched))
|
|
|
|
# Load into temp table
|
|
frappe.db.sql("DROP TABLE IF EXISTS _tmp_reversal_match")
|
|
frappe.db.commit()
|
|
frappe.db.sql("""
|
|
CREATE TABLE _tmp_reversal_match (
|
|
id SERIAL PRIMARY KEY,
|
|
credit_sinv VARCHAR(140),
|
|
target_sinv VARCHAR(140),
|
|
amount DOUBLE PRECISION
|
|
)
|
|
""")
|
|
frappe.db.commit()
|
|
|
|
for i in range(0, len(matches), 5000):
|
|
batch = matches[i:i+5000]
|
|
values = ",".join(["('{}', '{}', {})".format(c, t, a) for c, t, a in batch])
|
|
frappe.db.sql("INSERT INTO _tmp_reversal_match (credit_sinv, target_sinv, amount) VALUES {}".format(values))
|
|
frappe.db.commit()
|
|
print("Loaded {} matches into temp table".format(len(matches)))
|
|
|
|
# Set return_against
|
|
frappe.db.sql("""
|
|
UPDATE "tabSales Invoice" si
|
|
SET return_against = rm.target_sinv
|
|
FROM _tmp_reversal_match rm
|
|
WHERE si.name = rm.credit_sinv
|
|
""")
|
|
frappe.db.commit()
|
|
print("Set return_against on {} credit invoices".format(len(matches)))
|
|
|
|
# Delete self-referencing PLE for these credit invoices
|
|
frappe.db.sql("""
|
|
DELETE FROM "tabPayment Ledger Entry"
|
|
WHERE name IN (
|
|
SELECT 'ple-' || rm.credit_sinv FROM _tmp_reversal_match rm
|
|
)
|
|
""")
|
|
frappe.db.commit()
|
|
|
|
# Insert PLE: credit allocation against target invoice
|
|
frappe.db.sql("""
|
|
INSERT INTO "tabPayment Ledger Entry" (
|
|
name, owner, creation, modified, modified_by, docstatus,
|
|
posting_date, company,
|
|
account_type, account, account_currency,
|
|
party_type, party,
|
|
due_date,
|
|
voucher_type, voucher_no,
|
|
against_voucher_type, against_voucher_no,
|
|
amount, amount_in_account_currency,
|
|
delinked, remarks
|
|
)
|
|
SELECT
|
|
'plr-' || rm.id, 'Administrator', NOW(), NOW(), 'Administrator', 1,
|
|
si.posting_date, 'TARGO',
|
|
'Receivable', 'Comptes clients - T', 'CAD',
|
|
'Customer', si.customer,
|
|
COALESCE(si.due_date, si.posting_date),
|
|
'Sales Invoice', rm.credit_sinv,
|
|
'Sales Invoice', rm.target_sinv,
|
|
rm.amount, rm.amount,
|
|
0, 'Reversal allocation'
|
|
FROM _tmp_reversal_match rm
|
|
JOIN "tabSales Invoice" si ON si.name = rm.credit_sinv
|
|
""")
|
|
frappe.db.commit()
|
|
new_ple = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE name LIKE 'plr-%%'")[0][0]
|
|
print("Created {} reversal PLE entries".format(new_ple))
|
|
|
|
# Set outstanding = 0 on linked credit invoices
|
|
frappe.db.sql("""
|
|
UPDATE "tabSales Invoice"
|
|
SET outstanding_amount = 0, status = 'Return'
|
|
WHERE name IN (SELECT credit_sinv FROM _tmp_reversal_match)
|
|
""")
|
|
frappe.db.commit()
|
|
|
|
# Recalculate outstanding on target invoices (original invoices being cancelled)
|
|
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.name IN (SELECT target_sinv FROM _tmp_reversal_match)
|
|
""")
|
|
frappe.db.commit()
|
|
|
|
# Update statuses for affected target invoices
|
|
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Paid'
|
|
WHERE name IN (SELECT target_sinv FROM _tmp_reversal_match)
|
|
AND is_return != 1 AND ROUND(outstanding_amount::numeric, 2) = 0""")
|
|
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Overdue'
|
|
WHERE name IN (SELECT target_sinv FROM _tmp_reversal_match)
|
|
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 name IN (SELECT target_sinv FROM _tmp_reversal_match)
|
|
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 name IN (SELECT target_sinv FROM _tmp_reversal_match)
|
|
AND is_return != 1 AND outstanding_amount < -0.005""")
|
|
frappe.db.commit()
|
|
|
|
# Cleanup
|
|
frappe.db.sql("DROP TABLE IF EXISTS _tmp_reversal_match")
|
|
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']))
|
|
|
|
elapsed = time.time() - t0
|
|
print("\nDone in {:.0f}s".format(elapsed))
|