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

194 lines
6.2 KiB
Python

"""
Create Website Users for customer portal + store legacy MD5 password hashes.
Bridge auth: on first login, verify MD5 → convert to pbkdf2 → clear legacy hash.
Requires custom field 'legacy_password_md5' (Data) on User doctype.
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/create_portal_users.py
"""
import os, sys, time
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)
os.chdir("/home/frappe/frappe-bench/sites")
import frappe
frappe.init(site="erp.gigafibre.ca", sites_path=".")
frappe.connect()
# Disable rate limiting for bulk user creation
frappe.flags.in_migrate = True
frappe.flags.in_install = True
print("Connected:", frappe.local.site)
import pymysql
DRY_RUN = False # SET TO False WHEN READY
# ── Step 1: Ensure custom field exists ──
print("\n" + "=" * 60)
print("Step 1: Ensure legacy_password_md5 custom field on User")
print("=" * 60)
if not frappe.db.exists('Custom Field', {'dt': 'User', 'fieldname': 'legacy_password_md5'}):
if not DRY_RUN:
cf = frappe.get_doc({
'doctype': 'Custom Field',
'dt': 'User',
'fieldname': 'legacy_password_md5',
'label': 'Legacy Password (MD5)',
'fieldtype': 'Data',
'hidden': 1,
'no_copy': 1,
'print_hide': 1,
'insert_after': 'last_password_reset_date',
})
cf.insert(ignore_permissions=True)
frappe.db.commit()
print(" Created custom field")
else:
print(" [DRY RUN] Would create custom field")
else:
print(" Custom field already exists")
# ── Step 2: Fetch legacy accounts with email + password ──
print("\n" + "=" * 60)
print("Step 2: Fetch legacy accounts")
print("=" * 60)
legacy = pymysql.connect(
host="legacy-db", user="facturation", password="VD67owoj",
database="gestionclient", cursorclass=pymysql.cursors.DictCursor
)
with legacy.cursor() as cur:
cur.execute("""
SELECT id, email, password, first_name, last_name,
status, customer_id
FROM account
WHERE email IS NOT NULL AND email != '' AND TRIM(email) != ''
AND password IS NOT NULL AND password != ''
ORDER BY id
""")
legacy_accounts = cur.fetchall()
legacy.close()
print(f" Legacy accounts with email + password: {len(legacy_accounts)}")
# ── Step 3: Build legacy_account_id → Customer name map ──
acct_to_cust = {}
rows = frappe.db.sql('SELECT name, legacy_account_id FROM "tabCustomer" WHERE legacy_account_id IS NOT NULL', as_dict=True)
for r in rows:
acct_to_cust[int(r['legacy_account_id'])] = r['name']
print(f" Customers with legacy_account_id: {len(acct_to_cust)}")
# ── Step 4: Check existing users ──
existing_users = set()
rows = frappe.db.sql('SELECT LOWER(name) as name FROM "tabUser"', as_dict=True)
for r in rows:
existing_users.add(r['name'])
print(f" Existing ERPNext users: {len(existing_users)}")
# ── Step 5: Create Website Users ──
print("\n" + "=" * 60)
print("Step 3: Create Website Users")
print("=" * 60)
t0 = time.time()
created = 0
skipped_existing = 0
skipped_no_customer = 0
skipped_bad_email = 0
errors = 0
for acc in legacy_accounts:
email = acc['email'].strip().lower()
# Basic email validation
if '@' not in email or '.' not in email.split('@')[-1]:
skipped_bad_email += 1
continue
# Already exists?
if email in existing_users:
# Just update the legacy hash if not already set
if not DRY_RUN and acc['password']:
try:
frappe.db.sql(
"""UPDATE "tabUser" SET legacy_password_md5 = %s
WHERE LOWER(name) = %s AND (legacy_password_md5 IS NULL OR legacy_password_md5 = '')""",
(acc['password'], email)
)
except Exception:
pass # Field might not exist yet in dry run
skipped_existing += 1
continue
# Find the ERPNext customer via legacy_account_id
cust_name = acct_to_cust.get(int(acc['id']))
if not cust_name:
skipped_no_customer += 1
continue
first_name = (acc.get('first_name') or '').strip() or 'Client'
last_name = (acc.get('last_name') or '').strip() or ''
full_name = f"{first_name} {last_name}".strip()
if not DRY_RUN:
try:
user = frappe.get_doc({
'doctype': 'User',
'email': email,
'first_name': first_name,
'last_name': last_name,
'full_name': full_name,
'enabled': 1,
'user_type': 'Website User',
'legacy_password_md5': acc['password'] or '',
'roles': [{'role': 'Customer'}],
})
user.flags.no_welcome_email = True # Don't send email yet
user.flags.ignore_permissions = True
user.flags.in_import = True
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
created += 1
# Link user to customer as portal user
customer_doc = frappe.get_doc('Customer', cust_name)
customer_doc.append('portal_users', {'user': email})
customer_doc.save(ignore_permissions=True)
except frappe.DuplicateEntryError:
skipped_existing += 1
except Exception as e:
errors += 1
if errors <= 10:
print(f" ERR {email}: {e}")
else:
created += 1 # Count as would-create
if (created + skipped_existing) % 1000 == 0:
if not DRY_RUN:
frappe.db.commit()
elapsed = time.time() - t0
total = created + skipped_existing + skipped_no_customer + skipped_bad_email + errors
print(f" Progress: {total}/{len(legacy_accounts)} created={created} existing={skipped_existing} [{elapsed:.0f}s]")
if not DRY_RUN:
frappe.db.commit()
elapsed = time.time() - t0
print(f"\n Created: {created}")
print(f" Already existed: {skipped_existing}")
print(f" No matching customer: {skipped_no_customer}")
print(f" Bad email: {skipped_bad_email}")
print(f" Errors: {errors}")
print(f" Time: {elapsed:.0f}s")
if DRY_RUN:
print("\n ** DRY RUN — no changes made **")
print("\n" + "=" * 60)
print("DONE")
print("=" * 60)