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>
223 lines
8.5 KiB
Python
223 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Import legacy payments (24 months) as Payment Entry + references to Sales Invoices."""
|
|
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": 600}
|
|
PG = {"host": "db", "port": 5432, "user": "postgres", "password": "123",
|
|
"dbname": "_eb65bdc0c4b1b2d6"}
|
|
|
|
ADMIN = "Administrator"
|
|
COMPANY = "TARGO"
|
|
|
|
# Legacy type → ERPNext mode_of_payment
|
|
MODE_MAP = {
|
|
"ppa": "Bank Draft",
|
|
"paiement direct": "Bank Draft",
|
|
"carte credit": "Credit Card",
|
|
"cheque": "Cheque",
|
|
"comptant": "Cash",
|
|
"reversement": "Bank Draft",
|
|
"credit": "Credit Note",
|
|
"credit targo": "Credit Note",
|
|
"credit facture": "Credit Note",
|
|
}
|
|
|
|
def uid(p=""):
|
|
return p + uuid.uuid4().hex[:10]
|
|
|
|
def ts_to_dt(t):
|
|
if not t or t <= 0: return None
|
|
try: return datetime.fromtimestamp(int(t), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
except: return None
|
|
|
|
def ts_to_date(t):
|
|
if not t or t <= 0: return None
|
|
try: return datetime.fromtimestamp(int(t), tz=timezone.utc).strftime("%Y-%m-%d")
|
|
except: return None
|
|
|
|
def log(msg):
|
|
print(msg, flush=True)
|
|
|
|
def main():
|
|
log("=== Import Payments (24 months) ===")
|
|
|
|
mc = pymysql.connect(**LEGACY)
|
|
cur = mc.cursor(pymysql.cursors.DictCursor)
|
|
cutoff = int(datetime.now(timezone.utc).timestamp()) - (24 * 30 * 86400)
|
|
|
|
cur.execute("SELECT * FROM payment WHERE date_orig >= %s AND type != 'credit' AND type != 'credit targo' AND type != 'credit facture' ORDER BY id", (cutoff,))
|
|
payments = cur.fetchall()
|
|
log(" {} payments (excl credits)".format(len(payments)))
|
|
|
|
# Payment items (links to invoices)
|
|
pay_ids = [p["id"] for p in payments]
|
|
items_by_pay = {}
|
|
chunk = 10000
|
|
for s in range(0, len(pay_ids), chunk):
|
|
batch = pay_ids[s:s+chunk]
|
|
cur.execute("SELECT * FROM payment_item WHERE payment_id IN ({})".format(",".join(["%s"]*len(batch))), batch)
|
|
for r in cur.fetchall():
|
|
items_by_pay.setdefault(r["payment_id"], []).append(r)
|
|
mc.close()
|
|
log(" {} payment-invoice links loaded".format(sum(len(v) for v in items_by_pay.values())))
|
|
|
|
pg = psycopg2.connect(**PG)
|
|
pgc = pg.cursor()
|
|
|
|
# Mappings
|
|
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()}
|
|
|
|
pgc.execute('SELECT legacy_invoice_id, name FROM "tabSales Invoice" WHERE legacy_invoice_id > 0')
|
|
inv_map = {r[0]: r[1] for r in pgc.fetchall()}
|
|
|
|
pgc.execute("SELECT name FROM \"tabAccount\" WHERE account_type = 'Receivable' AND company = %s AND is_group = 0 LIMIT 1", (COMPANY,))
|
|
receivable = pgc.fetchone()[0]
|
|
|
|
pgc.execute("SELECT name FROM \"tabAccount\" WHERE account_type = 'Bank' AND company = %s AND is_group = 0 LIMIT 1", (COMPANY,))
|
|
bank_row = pgc.fetchone()
|
|
bank_acct = bank_row[0] if bank_row else "Banque - T"
|
|
|
|
# Check existing
|
|
pgc.execute("SELECT name FROM \"tabPayment Entry\" WHERE name LIKE 'PE-%'")
|
|
existing = set(r[0] for r in pgc.fetchall())
|
|
|
|
# Add custom field for legacy payment id
|
|
try:
|
|
pgc.execute("""
|
|
INSERT INTO "tabCustom Field" (name, creation, modified, modified_by, owner, docstatus, idx,
|
|
dt, label, fieldname, fieldtype, insert_after)
|
|
VALUES (%s, %s, %s, %s, %s, 0, 0,
|
|
'Payment Entry', 'Legacy Payment ID', 'legacy_payment_id', 'Int', 'naming_series')
|
|
""", (uid("CF-"), ts_to_dt(int(datetime.now(timezone.utc).timestamp())),
|
|
ts_to_dt(int(datetime.now(timezone.utc).timestamp())), ADMIN, ADMIN))
|
|
pg.commit()
|
|
log(" Created legacy_payment_id custom field")
|
|
except:
|
|
pg.rollback()
|
|
log(" legacy_payment_id field already exists")
|
|
|
|
# Check if column exists
|
|
pgc.execute("""SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = 'tabPayment Entry' AND column_name = 'legacy_payment_id'""")
|
|
has_legacy_col = pgc.fetchone() is not None
|
|
|
|
pay_ok = pay_skip = pay_err = ref_ok = 0
|
|
now_ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
|
|
for i, p in enumerate(payments):
|
|
pe_name = "PE-{}".format(uid())
|
|
if pe_name in existing:
|
|
pay_skip += 1
|
|
continue
|
|
|
|
cust_name = cust_map.get(p["account_id"])
|
|
if not cust_name:
|
|
pay_err += 1
|
|
continue
|
|
|
|
posting_date = ts_to_date(p["date_orig"]) or "2025-01-01"
|
|
amount = round(abs(float(p["amount"] or 0)), 2)
|
|
if amount <= 0:
|
|
pay_skip += 1
|
|
continue
|
|
|
|
mode = MODE_MAP.get(p.get("type", ""), "Bank Draft")
|
|
ref = p.get("reference") or ""
|
|
memo = p.get("memo") or ""
|
|
|
|
try:
|
|
cols = """name, creation, modified, modified_by, owner, docstatus, idx,
|
|
naming_series, payment_type, posting_date, company,
|
|
mode_of_payment, party_type, party,
|
|
paid_from, paid_to, paid_amount, received_amount,
|
|
base_paid_amount, base_received_amount,
|
|
target_exchange_rate, source_exchange_rate,
|
|
reference_no, reference_date, remarks,
|
|
status"""
|
|
vals = [pe_name, now_ts, now_ts, ADMIN, ADMIN, 0, 0,
|
|
'ACC-PAY-.YYYY.-', 'Receive', posting_date, COMPANY,
|
|
mode, 'Customer', cust_name,
|
|
bank_acct, receivable, amount, amount,
|
|
amount, amount,
|
|
1, 1,
|
|
ref[:140] if ref else None, posting_date, memo[:140] if memo else None,
|
|
'Draft']
|
|
|
|
if has_legacy_col:
|
|
cols += ", legacy_payment_id"
|
|
vals.append(p["id"])
|
|
|
|
placeholders = ",".join(["%s"] * len(vals))
|
|
pgc.execute('INSERT INTO "tabPayment Entry" ({}) VALUES ({})'.format(cols, placeholders), vals)
|
|
|
|
# Payment Entry References (link to invoices)
|
|
for j, pi in enumerate(items_by_pay.get(p["id"], [])):
|
|
inv_name = inv_map.get(pi.get("invoice_id"))
|
|
if not inv_name:
|
|
continue
|
|
alloc = round(abs(float(pi.get("amount") or 0)), 2)
|
|
pgc.execute("""
|
|
INSERT INTO "tabPayment Entry Reference" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
reference_doctype, reference_name, allocated_amount,
|
|
total_amount, outstanding_amount, exchange_rate,
|
|
parent, parentfield, parenttype
|
|
) VALUES (%s, %s, %s, %s, %s, 0, %s,
|
|
'Sales Invoice', %s, %s,
|
|
%s, %s, 1,
|
|
%s, 'references', 'Payment Entry')
|
|
""", (uid("PER-"), now_ts, now_ts, ADMIN, ADMIN, j+1,
|
|
inv_name, alloc, alloc, alloc,
|
|
pe_name))
|
|
ref_ok += 1
|
|
|
|
pay_ok += 1
|
|
|
|
except Exception as e:
|
|
pay_err += 1
|
|
pg.rollback()
|
|
if pay_err <= 10:
|
|
log(" ERR pay#{} -> {}".format(p["id"], str(e)[:100]))
|
|
continue
|
|
|
|
if pay_ok % 2000 == 0:
|
|
pg.commit()
|
|
log(" [{}/{}] pay={} refs={} skip={} err={}".format(
|
|
i+1, len(payments), pay_ok, ref_ok, pay_skip, pay_err))
|
|
|
|
pg.commit()
|
|
|
|
# Fix creation dates
|
|
log("")
|
|
log("--- Fixing Payment Entry dates ---")
|
|
mc = pymysql.connect(**LEGACY)
|
|
cur = mc.cursor(pymysql.cursors.DictCursor)
|
|
cur.execute("SELECT id, date_orig FROM payment WHERE date_orig >= %s ORDER BY id", (cutoff,))
|
|
pay_dates = {r["id"]: r["date_orig"] for r in cur.fetchall()}
|
|
mc.close()
|
|
|
|
if has_legacy_col:
|
|
pgc.execute('SELECT name, legacy_payment_id FROM "tabPayment Entry" WHERE legacy_payment_id > 0')
|
|
for name, pid in pgc.fetchall():
|
|
dt = ts_to_dt(pay_dates.get(pid))
|
|
if dt:
|
|
pgc.execute('UPDATE "tabPayment Entry" SET creation = %s, modified = %s WHERE name = %s',
|
|
(dt, dt, name))
|
|
pg.commit()
|
|
log(" Dates fixed")
|
|
|
|
pg.close()
|
|
log("")
|
|
log("=" * 60)
|
|
log("Payments: {} created, {} skipped, {} errors".format(pay_ok, pay_skip, pay_err))
|
|
log("Invoice refs: {}".format(ref_ok))
|
|
log("=" * 60)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|