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>
222 lines
8.0 KiB
Python
222 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Phase 5: Opening Balance — Create Journal Entry with customer balances.
|
|
Direct PostgreSQL. Detached.
|
|
Log: /tmp/migrate_phase5.log
|
|
"""
|
|
import pymysql
|
|
import psycopg2
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
LEGACY = {"host": "legacy-db", "user": "facturation", "password": "VD67owoj",
|
|
"database": "gestionclient", "connect_timeout": 30, "read_timeout": 300}
|
|
PG = {"host": "db", "port": 5432, "user": "postgres", "password": "123",
|
|
"dbname": "_eb65bdc0c4b1b2d6"}
|
|
|
|
ADMIN = "Administrator"
|
|
COMPANY = "TARGO"
|
|
OPENING_DATE = "2026-03-28" # Date de l'opening balance
|
|
|
|
def uid(prefix=""):
|
|
return prefix + uuid.uuid4().hex[:10]
|
|
|
|
def now():
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
|
|
def log(msg):
|
|
print(msg, flush=True)
|
|
|
|
def main():
|
|
ts = now()
|
|
log("=== Phase 5: Opening Balance ===")
|
|
|
|
# 1. Read legacy balances (excluding credit-type payments)
|
|
log("Reading legacy balances...")
|
|
mc = pymysql.connect(**LEGACY)
|
|
cur = mc.cursor(pymysql.cursors.DictCursor)
|
|
cur.execute("""
|
|
SELECT a.id as account_id,
|
|
ROUND(
|
|
COALESCE((SELECT SUM(total_amt) FROM invoice WHERE account_id = a.id AND billing_status = 1), 0) -
|
|
COALESCE((SELECT SUM(amount) FROM payment WHERE account_id = a.id
|
|
AND type NOT IN ('credit', 'credit targo', 'credit facture')), 0)
|
|
, 2) as balance
|
|
FROM account a
|
|
WHERE a.status IN (1, 2)
|
|
HAVING balance > 0.50 OR balance < -0.50
|
|
ORDER BY a.id
|
|
""")
|
|
balances = cur.fetchall()
|
|
mc.close()
|
|
|
|
total_debit = sum(b["balance"] for b in balances if b["balance"] > 0)
|
|
total_credit = sum(abs(b["balance"]) for b in balances if b["balance"] < 0)
|
|
log(" {} clients with balance".format(len(balances)))
|
|
log(" Total AR (debit): ${:,.2f}".format(total_debit))
|
|
log(" Total credit: ${:,.2f}".format(total_credit))
|
|
|
|
# 2. Connect ERPNext PG
|
|
log("Connecting to ERPNext PostgreSQL...")
|
|
pg = psycopg2.connect(**PG)
|
|
pgc = pg.cursor()
|
|
|
|
# Customer mapping
|
|
pgc.execute('SELECT legacy_account_id, name FROM "tabCustomer" WHERE legacy_account_id > 0')
|
|
cust_map = {r[0]: r[1] for r in pgc.fetchall()}
|
|
|
|
# Get receivable account
|
|
pgc.execute("""SELECT name FROM "tabAccount" WHERE account_type = 'Receivable'
|
|
AND company = 'TARGO' AND is_group = 0 LIMIT 1""")
|
|
row = pgc.fetchone()
|
|
if not row:
|
|
log("ERROR: No receivable account found!")
|
|
return
|
|
receivable_acct = row[0]
|
|
log(" Receivable account: {}".format(receivable_acct))
|
|
|
|
# Get temporary opening account
|
|
pgc.execute("""SELECT name FROM "tabAccount" WHERE account_name LIKE '%Temporary%Opening%'
|
|
AND company = 'TARGO' LIMIT 1""")
|
|
row = pgc.fetchone()
|
|
if not row:
|
|
# Use equity opening account
|
|
pgc.execute("""SELECT name FROM "tabAccount"
|
|
WHERE account_name LIKE '%Opening%' AND company = 'TARGO'
|
|
AND root_type = 'Equity' LIMIT 1""")
|
|
row = pgc.fetchone()
|
|
if not row:
|
|
log("ERROR: No opening balance account found! Creating one...")
|
|
ob_name = uid("ACCT-")
|
|
pgc.execute("""SELECT name FROM "tabAccount" WHERE account_name = 'Equity'
|
|
AND company = 'TARGO' AND is_group = 1 LIMIT 1""")
|
|
equity_parent = pgc.fetchone()
|
|
if not equity_parent:
|
|
pgc.execute("""SELECT name FROM "tabAccount" WHERE root_type = 'Equity'
|
|
AND company = 'TARGO' AND is_group = 1 LIMIT 1""")
|
|
equity_parent = pgc.fetchone()
|
|
parent = equity_parent[0] if equity_parent else "Equity - T"
|
|
|
|
pgc.execute("""
|
|
INSERT INTO "tabAccount" (name, creation, modified, modified_by, owner, docstatus, idx,
|
|
account_name, parent_account, root_type, account_type, company, is_group, lft, rgt)
|
|
VALUES (%s, %s, %s, %s, %s, 0, 0,
|
|
'Temporary Opening', %s, 'Equity', 'Temporary', %s, 0, 0, 0)
|
|
""", (ob_name, ts, ts, ADMIN, ADMIN, parent, COMPANY))
|
|
pg.commit()
|
|
opening_acct = ob_name
|
|
log(" Created Temporary Opening account: {}".format(ob_name))
|
|
else:
|
|
opening_acct = row[0]
|
|
log(" Opening account: {}".format(opening_acct))
|
|
|
|
# 3. Create Journal Entry
|
|
je_name = uid("JE-OB-")
|
|
log("")
|
|
log("Creating Journal Entry: {}".format(je_name))
|
|
log(" Date: {}".format(OPENING_DATE))
|
|
|
|
pgc.execute("""
|
|
INSERT INTO "tabJournal Entry" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
title, voucher_type, naming_series, posting_date, company,
|
|
is_opening, remark, user_remark, total_debit, total_credit,
|
|
multi_currency
|
|
) VALUES (
|
|
%s, %s, %s, %s, %s, 0, 0,
|
|
'Opening Balance - Legacy Migration', 'Opening Entry', 'ACC-JV-.YYYY.-',
|
|
%s, %s, 'Yes',
|
|
'Opening balance from legacy CRM/Billing migration',
|
|
'Soldes clients migrés du système legacy Facturation PHP/MariaDB',
|
|
%s, %s, 0
|
|
)
|
|
""", (je_name, ts, ts, ADMIN, ADMIN, OPENING_DATE, COMPANY,
|
|
round(total_debit + total_credit, 2), round(total_debit + total_credit, 2)))
|
|
|
|
# 4. Create Journal Entry Account lines
|
|
line_idx = 0
|
|
ok = 0
|
|
skip = 0
|
|
|
|
for b in balances:
|
|
cust_name = cust_map.get(b["account_id"])
|
|
if not cust_name:
|
|
skip += 1
|
|
continue
|
|
|
|
line_idx += 1
|
|
amount = round(abs(b["balance"]), 2)
|
|
|
|
if b["balance"] > 0:
|
|
# Customer owes money → debit receivable
|
|
debit = amount
|
|
credit = 0
|
|
else:
|
|
# Customer has credit → credit receivable
|
|
debit = 0
|
|
credit = amount
|
|
|
|
pgc.execute("""
|
|
INSERT INTO "tabJournal Entry Account" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
account, account_type, party_type, party,
|
|
debit_in_account_currency, debit, credit_in_account_currency, credit,
|
|
account_currency, cost_center, is_advance,
|
|
parent, parentfield, parenttype
|
|
) VALUES (
|
|
%s, %s, %s, %s, %s, 0, %s,
|
|
%s, 'Receivable', 'Customer', %s,
|
|
%s, %s, %s, %s,
|
|
'CAD', 'Main - T', 'No',
|
|
%s, 'accounts', 'Journal Entry'
|
|
)
|
|
""", (uid("JEA-"), ts, ts, ADMIN, ADMIN, line_idx,
|
|
receivable_acct, cust_name,
|
|
debit, debit, credit, credit,
|
|
je_name))
|
|
ok += 1
|
|
|
|
# Balancing entry → Opening account
|
|
line_idx += 1
|
|
net = round(total_debit - total_credit, 2)
|
|
if net > 0:
|
|
bal_debit = 0
|
|
bal_credit = net
|
|
else:
|
|
bal_debit = abs(net)
|
|
bal_credit = 0
|
|
|
|
pgc.execute("""
|
|
INSERT INTO "tabJournal Entry Account" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
account, debit_in_account_currency, debit, credit_in_account_currency, credit,
|
|
account_currency, cost_center, is_advance,
|
|
parent, parentfield, parenttype
|
|
) VALUES (
|
|
%s, %s, %s, %s, %s, 0, %s,
|
|
%s, %s, %s, %s, %s,
|
|
'CAD', 'Main - T', 'No',
|
|
%s, 'accounts', 'Journal Entry'
|
|
)
|
|
""", (uid("JEA-"), ts, ts, ADMIN, ADMIN, line_idx,
|
|
opening_acct, bal_debit, bal_debit, bal_credit, bal_credit,
|
|
je_name))
|
|
|
|
pg.commit()
|
|
pg.close()
|
|
|
|
log("")
|
|
log("=" * 60)
|
|
log("Journal Entry: {} (DRAFT — review before submitting)".format(je_name))
|
|
log(" {} customer lines, {} skipped (no matching customer)".format(ok, skip))
|
|
log(" Total debit: ${:,.2f}".format(total_debit))
|
|
log(" Total credit: ${:,.2f}".format(total_credit))
|
|
log(" Net AR: ${:,.2f}".format(net))
|
|
log("=" * 60)
|
|
log("")
|
|
log("Next: Review in ERPNext, then Submit when ready.")
|
|
log(" bench --site erp.gigafibre.ca clear-cache")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|