From 302d54eb0a07faada410adba284b64dfcbc3fb09 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Thu, 2 Jul 2026 19:37:19 +0000 Subject: [PATCH] fix(config): re-sync employee permissions on role change; add sync script --- pos/blueprints/config_bp.py | 9 +++ scripts/sync_employee_permissions.py | 111 +++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 scripts/sync_employee_permissions.py diff --git a/pos/blueprints/config_bp.py b/pos/blueprints/config_bp.py index 828f93f..ad4140f 100644 --- a/pos/blueprints/config_bp.py +++ b/pos/blueprints/config_bp.py @@ -486,6 +486,15 @@ def update_employee(emp_id): params.append(emp_id) cur.execute(f"UPDATE employees SET {', '.join(updates)} WHERE id = %s", params) + # If the role changed, re-sync permissions to match the new role defaults/config. + if 'role' in data: + cur.execute("DELETE FROM employee_permissions WHERE employee_id = %s", (emp_id,)) + for perm in _get_role_permissions(conn, data['role']): + cur.execute( + "INSERT INTO employee_permissions (employee_id, permission) VALUES (%s, %s) ON CONFLICT DO NOTHING", + (emp_id, perm) + ) + from services.audit import log_action log_action(conn, 'EMPLOYEE_UPDATE', 'employee', emp_id, new_value={k: v for k, v in data.items() if k != 'pin'}) diff --git a/scripts/sync_employee_permissions.py b/scripts/sync_employee_permissions.py new file mode 100644 index 0000000..0337d9b --- /dev/null +++ b/scripts/sync_employee_permissions.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Re-sync employee_permissions with role defaults / configured role permissions. + +Useful when an employee's permissions get out of sync with the role they belong to +(e.g. after a role change or a buggy save). + +Example: + python3 scripts/sync_employee_permissions.py --db tenant_refaccionaria_rached +""" +import argparse +import json +import os +import sys + +import psycopg2 + +# Keep in sync with pos/blueprints/config_bp.py +_DEFAULT_ROLE_PERMISSIONS = { + 'owner': [], + 'admin': [ + 'config.view', 'config.edit', 'config.edit_prices', 'config.delete', + 'pos.view', 'pos.sell', 'pos.cancel', 'pos.remission', + 'inventory.view', 'inventory.create', 'inventory.edit', 'inventory.adjust', 'inventory.transfer', + 'customers.view', 'customers.create', 'customers.edit', 'customers.delete', + 'catalog.view', 'catalog.edit', + 'workshop.view', 'workshop.edit', 'workshop.delete', + 'invoicing.view', 'invoicing.create', + 'reports.view', + ], + 'manager': [ + 'pos.view', 'pos.sell', 'pos.cancel', 'pos.remission', + 'inventory.view', 'inventory.create', 'inventory.edit', 'inventory.adjust', 'inventory.transfer', + 'customers.view', 'customers.create', 'customers.edit', + 'catalog.view', 'catalog.edit', + 'workshop.view', 'workshop.edit', + 'invoicing.view', 'invoicing.create', + 'reports.view', + ], + 'warehouse': [ + 'inventory.view', 'inventory.create', 'inventory.edit', 'inventory.adjust', 'inventory.transfer', + 'pos.view', + ], + 'counter': [ + 'pos.sell', 'pos.view', 'pos.remission', 'inventory.view', 'customers.view', 'catalog.view', + ], + 'cashier': [ + 'pos.sell', 'pos.view', 'pos.remission', 'inventory.view', 'customers.view', 'catalog.view', 'invoicing.view', + ], + 'workshop': [ + 'workshop.view', 'customers.view', 'customers.create', 'inventory.view', + ], + 'mechanic': [ + 'workshop.view', 'customers.view', 'customers.create', 'inventory.view', + ], +} + + +def _get_role_permissions(cur, role): + cur.execute("SELECT value FROM tenant_config WHERE key = 'role_permissions'") + row = cur.fetchone() + if row and row[0]: + try: + configured = json.loads(row[0]) + if isinstance(configured, dict) and role in configured: + return list(configured.get(role, [])) + except (ValueError, TypeError): + pass + return list(_DEFAULT_ROLE_PERMISSIONS.get(role, [])) + + +def sync(db_url): + conn = psycopg2.connect(db_url) + cur = conn.cursor() + + cur.execute("SELECT id, name, role FROM employees ORDER BY id") + employees = cur.fetchall() + + total = 0 + for emp_id, name, role in employees: + perms = _get_role_permissions(cur, role) + cur.execute("DELETE FROM employee_permissions WHERE employee_id = %s", (emp_id,)) + for perm in perms: + cur.execute( + "INSERT INTO employee_permissions (employee_id, permission) VALUES (%s, %s) ON CONFLICT DO NOTHING", + (emp_id, perm) + ) + total += 1 + print(f" [{emp_id:>3}] {name:<30} role={role:<10} perms={len(perms)}") + + conn.commit() + cur.close() + conn.close() + print(f"\nSynced {total} employees.") + + +def main(): + parser = argparse.ArgumentParser(description="Sync employee_permissions with role defaults/config") + parser.add_argument('--db', required=True, help='PostgreSQL database name or full connection URL') + args = parser.parse_args() + + db = args.db + if db.startswith('postgresql://'): + db_url = db + else: + db_url = f"postgresql://postgres@/{db}" + + sync(db_url) + + +if __name__ == '__main__': + main()