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

289 lines
11 KiB
Python

"""
Map legacy group_ad to ERPNext Role Profiles and assign roles to users.
Legacy groups:
admin → Full admin access (System Manager + all modules)
sysadmin → Technical admin (System Manager, HR, all operations)
tech → Field technicians (Dispatch, limited Accounts read)
support → Customer support (Support Team, Sales read, Dispatch)
comptabilite → Accounting (Accounts Manager, HR User)
facturation → Billing (Accounts User, Sales User)
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/setup_user_roles.py
"""
import frappe
import pymysql
import os
import time
import hashlib
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)
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
# ═══════════════════════════════════════════════════════════════
# ROLE PROFILE DEFINITIONS
# ═══════════════════════════════════════════════════════════════
# Roles each group should have
ROLE_MAP = {
"admin": [
"System Manager", "Accounts Manager", "Accounts User",
"Sales Manager", "Sales User", "HR Manager", "HR User",
"Support Team", "Dispatch Technician", "Employee",
"Projects Manager", "Stock Manager", "Stock User",
"Purchase Manager", "Purchase User", "Website Manager",
"Report Manager", "Dashboard Manager",
],
"sysadmin": [
"System Manager", "Accounts User",
"Sales Manager", "Sales User", "HR User",
"Support Team", "Dispatch Technician", "Employee",
"Projects Manager", "Stock Manager", "Stock User",
"Purchase User", "Website Manager",
"Report Manager", "Dashboard Manager",
],
"tech": [
"Dispatch Technician", "Employee",
"Support Team", "Stock User",
],
"support": [
"Support Team", "Employee",
"Sales User", "Dispatch Technician",
"Accounts User",
],
"comptabilite": [
"Accounts Manager", "Accounts User", "Employee",
"HR User", "Sales User", "Report Manager",
],
"facturation": [
"Accounts User", "Employee",
"Sales User", "Report Manager",
],
}
# Profile display names
PROFILE_NAMES = {
"admin": "Admin - Full Access",
"sysadmin": "SysAdmin - Technical",
"tech": "Technician - Field",
"support": "Support - Customer Service",
"comptabilite": "Comptabilité - Accounting",
"facturation": "Facturation - Billing",
}
# ═══════════════════════════════════════════════════════════════
# PHASE 1: Create/update Role Profiles
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 1: CREATE ROLE PROFILES")
print("="*60)
for group_key, roles in ROLE_MAP.items():
profile_name = PROFILE_NAMES[group_key]
# Delete existing profile and its roles
frappe.db.sql('DELETE FROM "tabHas Role" WHERE parent = %s AND parenttype = %s',
(profile_name, "Role Profile"))
if frappe.db.exists("Role Profile", profile_name):
frappe.db.sql('DELETE FROM "tabRole Profile" WHERE name = %s', (profile_name,))
# Create profile
frappe.db.sql("""
INSERT INTO "tabRole Profile" (name, creation, modified, modified_by, owner, docstatus, idx, role_profile)
VALUES (%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0, %(name)s)
""", {"name": profile_name, "now": now_str})
# Add roles
for i, role in enumerate(roles):
rname = "rp-{}-{}-{}".format(group_key, i, int(time.time()))
frappe.db.sql("""
INSERT INTO "tabHas Role" (
name, creation, modified, modified_by, owner, docstatus, idx,
parent, parentfield, parenttype, role
) VALUES (
%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, %(idx)s,
%(parent)s, 'roles', 'Role Profile', %(role)s
)
""", {"name": rname, "now": now_str, "idx": i + 1, "parent": profile_name, "role": role})
frappe.db.commit()
print(" Created profile '{}' with {} roles: {}".format(
profile_name, len(roles), ", ".join(roles[:5]) + ("..." if len(roles) > 5 else "")))
# ═══════════════════════════════════════════════════════════════
# PHASE 2: Get employee → user → group mapping
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 2: MAP EMPLOYEES TO ROLES")
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, username, first_name, last_name, email, group_ad, status
FROM staff WHERE status = 1 AND email IS NOT NULL AND email != ''
""")
active_staff = cur.fetchall()
conn.close()
# Build email → group_ad map
email_to_group = {}
for s in active_staff:
email = s["email"].strip().lower()
if email:
email_to_group[email] = (s["group_ad"] or "").strip().lower()
print("Active staff with email: {}".format(len(email_to_group)))
# Get all ERPNext users
erp_users = frappe.db.sql("""
SELECT name FROM "tabUser"
WHERE name NOT IN ('Administrator', 'Guest', 'admin@example.com')
AND enabled = 1
""", as_dict=True)
print("ERPNext users: {}".format(len(erp_users)))
# ═══════════════════════════════════════════════════════════════
# PHASE 3: Assign roles to users
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 3: ASSIGN ROLES")
print("="*60)
assigned = 0
skipped = 0
for user in erp_users:
email = user["name"].lower()
group = email_to_group.get(email)
if not group or group not in ROLE_MAP:
skipped += 1
continue
profile_name = PROFILE_NAMES[group]
target_roles = set(ROLE_MAP[group])
# Always add "All" and "Desk User" which are standard
target_roles.add("All")
target_roles.add("Desk User")
# Get current roles
current_roles = set()
current = frappe.db.sql("""
SELECT role FROM "tabHas Role"
WHERE parent = %s AND parenttype = 'User'
""", (user["name"],))
for r in current:
current_roles.add(r[0])
# Remove roles not in target (except a few we should keep)
keep_always = {"All", "Desk User"}
to_remove = current_roles - target_roles - keep_always
to_add = target_roles - current_roles
if to_remove:
for role in to_remove:
frappe.db.sql("""
DELETE FROM "tabHas Role"
WHERE parent = %s AND parenttype = 'User' AND role = %s
""", (user["name"], role))
if to_add:
for role in to_add:
rname = "ur-{}-{}".format(hashlib.md5("{}{}".format(user["name"], role).encode()).hexdigest()[:10], int(time.time()))
frappe.db.sql("""
INSERT INTO "tabHas Role" (
name, creation, modified, modified_by, owner, docstatus, idx,
parent, parentfield, parenttype, role
) VALUES (
%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0,
%(parent)s, 'roles', 'User', %(role)s
)
""", {"name": rname, "now": now_str, "parent": user["name"], "role": role})
# Set role profile on user
frappe.db.sql("""
UPDATE "tabUser" SET role_profile_name = %s WHERE name = %s
""", (profile_name, user["name"]))
assigned += 1
if to_add or to_remove:
print(" {}{} (+{} -{})".format(
user["name"], profile_name, len(to_add), len(to_remove)))
frappe.db.commit()
print("\nAssigned roles to {} users ({} skipped - no legacy group match)".format(assigned, skipped))
# Also link Employee → User
print("\n--- Linking Employee → User ---")
linked = 0
employees = frappe.db.sql("""
SELECT name, company_email FROM "tabEmployee"
WHERE status = 'Active' AND company_email IS NOT NULL
""", as_dict=True)
for emp in employees:
email = emp["company_email"]
# Check if user exists
user_exists = frappe.db.exists("User", email)
if user_exists:
frappe.db.sql("""
UPDATE "tabEmployee" SET user_id = %s WHERE name = %s
""", (email, emp["name"]))
linked += 1
frappe.db.commit()
print("Linked {} employees to their User accounts".format(linked))
# ═══════════════════════════════════════════════════════════════
# PHASE 4: VERIFY
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 4: VERIFY")
print("="*60)
# Count users per role profile
by_profile = frappe.db.sql("""
SELECT role_profile_name, COUNT(*) as cnt FROM "tabUser"
WHERE role_profile_name IS NOT NULL AND role_profile_name != ''
GROUP BY role_profile_name ORDER BY cnt DESC
""", as_dict=True)
print("Users by Role Profile:")
for p in by_profile:
print(" {}: {}".format(p["role_profile_name"], p["cnt"]))
# Sample users with their roles
sample_users = frappe.db.sql("""
SELECT u.name, u.full_name, u.role_profile_name,
STRING_AGG(hr.role, ', ' ORDER BY hr.role) as roles
FROM "tabUser" u
LEFT JOIN "tabHas Role" hr ON hr.parent = u.name AND hr.parenttype = 'User'
WHERE u.role_profile_name IS NOT NULL AND u.role_profile_name != ''
GROUP BY u.name, u.full_name, u.role_profile_name
ORDER BY u.role_profile_name, u.name
""", as_dict=True)
print("\nUsers and their roles:")
for u in sample_users:
print(" {} ({}) → {} roles: {}".format(
u["name"], u["full_name"] or "", u["role_profile_name"],
u["roles"][:80] if u["roles"] else "(none)"))
frappe.clear_cache()
print("\nDone — cache cleared")