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>
289 lines
11 KiB
Python
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")
|