feat: Phase 7 — 45 ERPNext Users from legacy staff
- 45 users created with Authentik SSO (no password) - Roles assigned: System Manager, Support Team, Sales/Accounts - Service accounts skipped (admin, tech, dev, inventaire, agent) - Email = Authentik identity link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a15bfd600
commit
ac9b367334
148
scripts/migration/migrate_users.py
Normal file
148
scripts/migration/migrate_users.py
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create ERPNext Users from legacy staff + link to Authentik via email.
|
||||||
|
Authentik forwardAuth sends X-authentik-email header → ERPNext matches by email.
|
||||||
|
Direct PG. Detached.
|
||||||
|
"""
|
||||||
|
import pymysql
|
||||||
|
import psycopg2
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from html import unescape
|
||||||
|
|
||||||
|
LEGACY = {"host": "10.100.80.100", "user": "facturation", "password": "VD67owoj",
|
||||||
|
"database": "gestionclient", "connect_timeout": 30, "read_timeout": 120}
|
||||||
|
PG = {"host": "db", "port": 5432, "user": "postgres", "password": "123",
|
||||||
|
"dbname": "_eb65bdc0c4b1b2d6"}
|
||||||
|
|
||||||
|
ADMIN = "Administrator"
|
||||||
|
|
||||||
|
# Roles for ISP staff
|
||||||
|
ROLES_ALL = ["System Manager", "Support Team", "Sales User", "Accounts User"]
|
||||||
|
ROLES_TECH = ["Support Team"]
|
||||||
|
ROLES_ADMIN = ["System Manager", "Support Team", "Sales User", "Accounts User", "Sales Manager", "Accounts Manager"]
|
||||||
|
|
||||||
|
# Staff IDs that are admin/sysadmin
|
||||||
|
ADMIN_IDS = {2, 4, 11, 12, 3293, 4661, 4664, 4671, 4749}
|
||||||
|
# Staff IDs that are service accounts (skip)
|
||||||
|
SKIP_IDS = {1, 3301, 3393, 4663, 4682}
|
||||||
|
|
||||||
|
def uid(prefix=""):
|
||||||
|
return prefix + uuid.uuid4().hex[:10]
|
||||||
|
|
||||||
|
def now():
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
|
|
||||||
|
def clean(val):
|
||||||
|
if not val: return ""
|
||||||
|
return unescape(str(val)).strip()
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ts = now()
|
||||||
|
log("=== User Migration ===")
|
||||||
|
|
||||||
|
mc = pymysql.connect(**LEGACY)
|
||||||
|
cur = mc.cursor(pymysql.cursors.DictCursor)
|
||||||
|
cur.execute("SELECT * FROM staff WHERE status = 1 ORDER BY id")
|
||||||
|
staff = cur.fetchall()
|
||||||
|
mc.close()
|
||||||
|
log("{} active staff loaded".format(len(staff)))
|
||||||
|
|
||||||
|
pg = psycopg2.connect(**PG)
|
||||||
|
pgc = pg.cursor()
|
||||||
|
|
||||||
|
# Existing users
|
||||||
|
pgc.execute('SELECT name FROM "tabUser"')
|
||||||
|
existing = set(r[0] for r in pgc.fetchall())
|
||||||
|
|
||||||
|
created = skipped = errors = 0
|
||||||
|
|
||||||
|
for s in staff:
|
||||||
|
sid = s["id"]
|
||||||
|
if sid in SKIP_IDS:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
email = clean(s.get("email"))
|
||||||
|
if not email or "@" not in email:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if email in existing or email.lower() in {e.lower() for e in existing}:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
first = clean(s["first_name"])
|
||||||
|
last = clean(s["last_name"])
|
||||||
|
full = "{} {}".format(first, last).strip()
|
||||||
|
username = clean(s.get("username"))
|
||||||
|
cell = clean(s.get("cell"))
|
||||||
|
|
||||||
|
# Determine roles
|
||||||
|
if sid in ADMIN_IDS:
|
||||||
|
roles = ROLES_ADMIN
|
||||||
|
else:
|
||||||
|
roles = ROLES_ALL
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create User
|
||||||
|
pgc.execute("""
|
||||||
|
INSERT INTO "tabUser" (
|
||||||
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
||||||
|
email, first_name, last_name, full_name, username,
|
||||||
|
mobile_no, language, time_zone, enabled,
|
||||||
|
user_type, role_profile_name,
|
||||||
|
new_password
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, 0, 0,
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, 'fr', 'America/Toronto', 1,
|
||||||
|
'System User', NULL,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
""", (email, ts, ts, ADMIN, ADMIN,
|
||||||
|
email, first, last or None, full, username,
|
||||||
|
cell or None))
|
||||||
|
|
||||||
|
# Create Role assignments
|
||||||
|
for i, role in enumerate(roles):
|
||||||
|
pgc.execute("""
|
||||||
|
INSERT INTO "tabHas Role" (
|
||||||
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
||||||
|
role, parent, parentfield, parenttype
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, 0, %s, %s, %s, 'roles', 'User')
|
||||||
|
""", (uid("HR-"), ts, ts, ADMIN, ADMIN, i+1, role, email))
|
||||||
|
|
||||||
|
# Create UserRole for "All"
|
||||||
|
pgc.execute("""
|
||||||
|
INSERT INTO "tabHas Role" (
|
||||||
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
||||||
|
role, parent, parentfield, parenttype
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, 0, %s, 'All', %s, 'roles', 'User')
|
||||||
|
""", (uid("HR-"), ts, ts, ADMIN, ADMIN, len(roles)+1, email))
|
||||||
|
|
||||||
|
created += 1
|
||||||
|
log(" OK {} ({})".format(full, email))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
pg.rollback()
|
||||||
|
log(" ERR {} -> {}".format(email, str(e)[:80]))
|
||||||
|
continue
|
||||||
|
|
||||||
|
pg.commit()
|
||||||
|
pg.close()
|
||||||
|
|
||||||
|
log("")
|
||||||
|
log("=" * 50)
|
||||||
|
log("Users: {} created, {} skipped, {} errors".format(created, skipped, errors))
|
||||||
|
log("=" * 50)
|
||||||
|
log("")
|
||||||
|
log("Users are created with NO password (Authentik SSO handles login).")
|
||||||
|
log("Next: bench --site erp.gigafibre.ca clear-cache")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user