""" 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="10.100.80.100", 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)