gigafibre-fsm/scripts/migration/setup_user_roles.py
louispaulb 101faa21f1 feat: inline editing, search, notifications + full repo cleanup
- InlineField component + useInlineEdit composable for Odoo-style dblclick editing
- Client search by name, account ID, and legacy_customer_id (or_filters)
- SMS/Email notification panel on ContactCard via n8n webhooks
- Ticket reply thread via Communication docs
- All migration scripts (51 files) now tracked
- Client portal and field tech app added to monorepo
- README rewritten with full feature list, migration summary, architecture
- CHANGELOG updated with all recent work
- ROADMAP updated with current completion status
- Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN)
- .gitignore updated (docker/, .claude/, exports/, .quasar/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 07:34:41 -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="10.100.80.100",
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")