gigafibre-fsm/scripts/migration/import_employees.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

292 lines
11 KiB
Python

"""
Import employees from legacy staff table into ERPNext Employee doctype.
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_employees.py
Maps legacy group_ad → Department, status → Active/Inactive.
Idempotent: deletes existing employees before reimporting.
"""
import frappe
import pymysql
import os
import time
from datetime import datetime
os.chdir("/home/frappe/frappe-bench/sites")
frappe.init(site="erp.gigafibre.ca", sites_path=".")
frappe.connect()
frappe.local.flags.ignore_permissions = True
print("Connected:", frappe.local.site)
T_TOTAL = time.time()
# ═══════════════════════════════════════════════════════════════
# CONFIG
# ═══════════════════════════════════════════════════════════════
COMPANY = "TARGO"
# Legacy group_ad → ERPNext Department
DEPT_MAP = {
"admin": "Management - T",
"sysadmin": "Operations - T",
"tech": "Operations - T",
"support": "Customer Service - T",
"comptabilite": "Accounts - T",
"facturation": "Accounts - T",
"": None,
"none": None,
}
# Legacy group_ad → ERPNext Designation
DESIG_MAP = {
"admin": "Manager",
"sysadmin": "Engineer",
"tech": "Technician",
"support": "Customer Service Representative",
"comptabilite": "Accountant",
"facturation": "Accountant",
}
# ═══════════════════════════════════════════════════════════════
# PHASE 0: Ensure prerequisite records exist
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 0: PREREQUISITES")
print("="*60)
# Gender "Prefer not to say"
if not frappe.db.exists("Gender", "Prefer not to say"):
frappe.get_doc({"doctype": "Gender", "gender": "Prefer not to say"}).insert()
frappe.db.commit()
print("Created Gender: Prefer not to say")
# Designation "Technician" — insert via SQL
for desig_name in ["Technician"]:
if not frappe.db.exists("Designation", desig_name):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
frappe.db.sql("""
INSERT INTO "tabDesignation" (name, creation, modified, modified_by, owner, docstatus, idx)
VALUES (%(n)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0)
""", {"n": desig_name, "now": now})
frappe.db.commit()
print("Created Designation:", desig_name)
# ═══════════════════════════════════════════════════════════════
# PHASE 1: CLEANUP existing employees
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 1: CLEANUP")
print("="*60)
existing = frappe.db.sql('SELECT COUNT(*) FROM "tabEmployee"')[0][0]
if existing > 0:
frappe.db.sql('DELETE FROM "tabEmployee"')
frappe.db.commit()
print("Deleted {} existing employees".format(existing))
else:
print("No existing employees to delete")
# Reset naming series counter
frappe.db.sql("""
DELETE FROM "tabSeries" WHERE name = 'HR-EMP-'
""")
frappe.db.commit()
# ═══════════════════════════════════════════════════════════════
# PHASE 2: FETCH legacy staff
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 2: FETCH LEGACY STAFF")
print("="*60)
conn = pymysql.connect(
host="legacy-db",
user="facturation",
password="*******",
database="gestionclient",
cursorclass=pymysql.cursors.DictCursor
)
with conn.cursor() as cur:
cur.execute("""
SELECT id, status, username, first_name, last_name, email, ext, cell,
group_ad, date_embauche, fete, matricule_desjardins, ldap_id
FROM staff ORDER BY id
""")
staff = cur.fetchall()
conn.close()
print("Fetched {} staff records".format(len(staff)))
# ═══════════════════════════════════════════════════════════════
# PHASE 3: INSERT employees via bulk SQL
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 3: INSERT EMPLOYEES")
print("="*60)
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
counter = 0
skipped = 0
for s in staff:
# Skip system/bot accounts with no real name
if not s["first_name"] or s["first_name"].strip() == "":
skipped += 1
continue
counter += 1
emp_name = "HR-EMP-{}".format(counter)
first_name = (s["first_name"] or "").replace("&#039;", "'").strip()
last_name = (s["last_name"] or "").replace("&#039;", "'").strip()
full_name = "{} {}".format(first_name, last_name).strip()
# Status: legacy 1 = Active, -1 = Inactive/Left
status = "Active" if s["status"] == 1 else "Left"
# Department
group = (s["group_ad"] or "").strip().lower()
dept = DEPT_MAP.get(group)
# Designation
desig = DESIG_MAP.get(group)
# Date of joining from unix timestamp
doj = None
if s["date_embauche"]:
try:
ts = int(s["date_embauche"])
if ts > 0:
doj = datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
except (ValueError, OSError):
pass
if not doj:
doj = "2020-01-01" # placeholder
# Date of birth from fete (DD|MM or MM|DD format)
dob = None
if s["fete"]:
parts = s["fete"].split("|")
if len(parts) == 2:
try:
day = int(parts[0])
month = int(parts[1])
# Format is DD|MM based on sample data (e.g. "06|05" = June 5th)
# But "30|12" = 30th of December — day|month
if day > 12:
# day is definitely the day
dob = "1990-{:02d}-{:02d}".format(month, day)
elif month > 12:
# month field is actually the day
dob = "1990-{:02d}-{:02d}".format(day, month)
else:
# Ambiguous — use DD|MM interpretation
dob = "1990-{:02d}-{:02d}".format(month, day)
except (ValueError, IndexError):
pass
if not dob:
dob = "1990-01-01" # placeholder
# Email
email = (s["email"] or "").strip()
company_email = email if email.endswith("@targointernet.com") else None
# Cell phone
cell = (s["cell"] or "").strip()
# Employee number = legacy staff ID
emp_number = str(s["id"])
frappe.db.sql("""
INSERT INTO "tabEmployee" (
name, creation, modified, modified_by, owner, docstatus, idx,
naming_series, first_name, last_name, employee_name,
gender, date_of_birth, date_of_joining,
status, company, department, designation,
employee_number, cell_number, company_email, personal_email,
prefered_contact_email,
lft, rgt
) VALUES (
%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0,
'HR-EMP-', %(first_name)s, %(last_name)s, %(full_name)s,
'Prefer not to say', %(dob)s, %(doj)s,
%(status)s, %(company)s, %(dept)s, %(desig)s,
%(emp_number)s, %(cell)s, %(company_email)s, %(personal_email)s,
%(pref_email)s,
0, 0
)
""", {
"name": emp_name,
"now": now_str,
"first_name": first_name,
"last_name": last_name,
"full_name": full_name,
"dob": dob,
"doj": doj,
"status": status,
"company": COMPANY,
"dept": dept,
"desig": desig,
"emp_number": emp_number,
"cell": cell if cell else None,
"company_email": company_email,
"personal_email": email if email and not email.endswith("@targointernet.com") else None,
"pref_email": "Company Email" if company_email else None,
})
frappe.db.commit()
print("Inserted {} employees ({} skipped - no name)".format(counter, skipped))
# Set the naming series counter
frappe.db.sql("""
INSERT INTO "tabSeries" (name, current) VALUES ('HR-EMP-', %(counter)s)
ON CONFLICT (name) DO UPDATE SET current = %(counter)s
""", {"counter": counter})
frappe.db.commit()
# Rebuild tree (Employee is a tree doctype with lft/rgt)
try:
frappe.rebuild_tree("Employee", "reports_to")
print("Rebuilt employee tree")
except Exception as e:
print("Tree rebuild skipped:", e)
# ═══════════════════════════════════════════════════════════════
# PHASE 4: VERIFY
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 4: VERIFY")
print("="*60)
total = frappe.db.sql('SELECT COUNT(*) FROM "tabEmployee"')[0][0]
active = frappe.db.sql('SELECT COUNT(*) FROM "tabEmployee" WHERE status = %s', ("Active",))[0][0]
left = frappe.db.sql('SELECT COUNT(*) FROM "tabEmployee" WHERE status = %s', ("Left",))[0][0]
print("Total employees: {}".format(total))
print(" Active: {}".format(active))
print(" Left: {}".format(left))
by_dept = frappe.db.sql("""
SELECT department, COUNT(*) as cnt FROM "tabEmployee"
GROUP BY department ORDER BY cnt DESC
""", as_dict=True)
print("\nBy department:")
for d in by_dept:
print(" {}: {}".format(d["department"] or "(none)", d["cnt"]))
# Sample
sample = frappe.db.sql("""
SELECT name, employee_name, status, department, designation, employee_number, date_of_joining
FROM "tabEmployee" ORDER BY name LIMIT 10
""", as_dict=True)
print("\nSample:")
for e in sample:
print(" {} {} [{}] dept={} desig={} legacy_id={} joined={}".format(
e["name"], e["employee_name"], e["status"],
e["department"], e["designation"], e["employee_number"], e["date_of_joining"]))
elapsed = time.time() - T_TOTAL
print("\n" + "="*60)
print("DONE in {:.1f}s".format(elapsed))
print("="*60)