# -*- coding: utf-8 -*- """Catálogo de permisos por ACCIÓN y defaults por rol. A diferencia de la matriz de menús (qué secciones ve un rol) y de los permisos de agenda por usuario, esto responde a "qué puede HACER" cada rol dentro de las pantallas: cancelar un ticket, aplicar descuentos, cerrar caja, exportar, administrar usuarios, etc. Sin dependencias de Odoo para poder importarse desde modelos y controladores. Persistencia: ir.config_parameter 'skeen.role_perms' = JSON {role_code: {perm_key: true/false}}. Lo guardado manda sobre los defaults; no hay que persistir los defaults (se calculan). REGLA DE ORO: con la configuración por defecto TODO se comporta como antes. Los defaults solo son restrictivos donde ya lo era el guard viejo (@require_role) o donde el rol es claramente de solo lectura. Todo lo demás queda permitido para los roles de escritura y son los guards existentes (veto de 'medico' a dinero, forbid_roles('marketing'), _puede_gestionar_*) los que siguen mandando. Así ningún usuario pierde acceso al desplegar. """ import json ROLE_PERMS_KEY = 'skeen.role_perms' # --------------------------------------------------------------------------- # Catálogo: grupos -> [(clave, etiqueta)]. El orden es el de la UI. # --------------------------------------------------------------------------- PERMISSION_GROUPS = [ ('agenda', 'Agenda', [ ('agenda.ver', 'Consultar la agenda'), ('agenda.agendar', 'Agendar citas'), ('agenda.editar', 'Editar / reprogramar citas'), ('agenda.eliminar', 'Eliminar o cancelar citas'), ('agenda.mover', 'Mover citas de horario'), ('agenda.visitas', 'Generar visitas (check-in)'), ('agenda.todas', 'Ver la agenda de todos (no solo la propia)'), ('agenda.bloquear', 'Bloquear / cerrar agenda'), ]), ('recursos', 'Recursos de la agenda', [ ('recursos.ver', 'Ver recursos (cabinas, salas, equipos)'), ('recursos.editar', 'Administrar recursos y su configuración'), ]), ('pacientes', 'Pacientes', [ ('pacientes.ver', 'Ver pacientes'), ('pacientes.expediente', 'Ver expediente completo'), ('pacientes.crear', 'Crear pacientes'), ('pacientes.editar', 'Editar pacientes'), ('pacientes.eliminar', 'Eliminar pacientes'), ('pacientes.exportar', 'Exportar pacientes'), ('pacientes.saldos', 'Ver saldos y adeudos'), ]), ('visitas', 'Visitas y expedientes', [ ('visitas.ver', 'Ver visitas'), ('visitas.crear', 'Crear visitas / consultas'), ('visitas.editar', 'Editar visitas'), ('visitas.eliminar', 'Eliminar visitas'), ('visitas.archivos', 'Subir archivos al expediente'), ]), ('ventas', 'Ventas y cobros', [ ('ventas.vender', 'Vender (punto de venta)'), ('ventas.cobrar', 'Cobrar y registrar pagos'), ('ventas.descuentos', 'Aplicar descuentos'), ('ventas.impuestos', 'Ajustar el impuesto por línea en la venta'), ('ventas.cancelar', 'Cancelar tickets / ventas'), ('ventas.devolver', 'Hacer devoluciones'), ('ventas.reimprimir', 'Reimprimir tickets'), ]), ('inventario', 'Inventario', [ ('inventario.ver', 'Ver inventario'), ('inventario.ajustar', 'Ajustar existencias'), ('inventario.mover', 'Registrar movimientos / compras / bajas'), ('inventario.costos', 'Editar costos'), ]), ('servicios', 'Servicios y catálogo', [ ('servicios.ver', 'Ver servicios y catálogo'), ('servicios.editar', 'Editar servicios y precios'), ]), ('reportes', 'Reportes y caja', [ ('reportes.ver', 'Ver reportes'), ('reportes.exportar', 'Exportar reportes'), ('caja.ver', 'Ver cortes de caja'), ('caja.cerrar', 'Cerrar caja'), ]), ('whatsapp', 'WhatsApp y leads', [ ('whatsapp.ver', 'Ver conversaciones y leads'), ('whatsapp.responder', 'Responder conversaciones'), ('whatsapp.asignar', 'Asignar conversaciones y leads'), ]), ('notificaciones', 'Notificaciones', [ ('notificaciones.ver', 'Ver el historial de notificaciones'), ('notificaciones.enviar', 'Enviar y reintentar notificaciones'), ('notificaciones.editar', 'Configurar recordatorios y plantillas'), ]), ('formularios', 'Formularios y consentimientos', [ ('formularios.ver', 'Ver formularios y respuestas'), ('formularios.crear', 'Llenar formularios (recepción)'), ('formularios.editar', 'Administrar plantillas de formularios'), ('formularios.enviar', 'Enviar formularios a pacientes'), ]), ('membresias', 'Membresías y suscripciones', [ ('membresias.ver', 'Ver membresías, planes y adeudos'), ('membresias.crear', 'Dar de alta membresías'), ('membresias.cobrar', 'Cobrar y renovar membresías'), ('membresias.editar', 'Editar planes y membresías'), ('membresias.cancelar', 'Congelar y cancelar membresías'), ]), ('compras', 'Compras y proveedores', [ ('compras.ver', 'Ver compras, cuentas por pagar y margen'), ('compras.crear', 'Crear órdenes de compra'), ('compras.recibir', 'Recibir mercancía (afecta stock y costo)'), ('compras.pagar', 'Registrar pagos a proveedores'), ('compras.editar', 'Editar órdenes de compra'), ('compras.cancelar', 'Cancelar órdenes de compra'), ('proveedores.ver', 'Ver proveedores'), ('proveedores.editar', 'Administrar proveedores'), ]), ('comisiones', 'Comisiones y liquidaciones', [ ('comisiones.ver', 'Ver comisiones y liquidaciones'), ('comisiones.editar', 'Administrar reglas y metas de comisiones'), ('comisiones.liquidar', 'Generar, ajustar y pagar liquidaciones'), ]), ('config', 'Usuarios y configuración', [ ('config.usuarios', 'Administrar usuarios'), ('config.roles', 'Administrar roles y menús'), ('config.editar', 'Editar la configuración de la clínica'), ('config.auditoria', 'Ver la auditoría'), ]), ('plataforma', 'Licencia, API y Webhooks', [ ('api.claves', 'Administrar claves de API'), ('webhooks.editar', 'Administrar webhooks'), ]), ] PERM_KEYS = tuple( key for _g, _label, perms in PERMISSION_GROUPS for key, _l in perms ) PERM_LABELS = { key: label for _g, _label, perms in PERMISSION_GROUPS for key, label in perms } # Permisos que sólo son lectura: son los que conserva el rol 'lectura'. READ_PERM_KEYS = ( 'agenda.ver', 'agenda.todas', 'recursos.ver', 'pacientes.ver', 'pacientes.expediente', 'pacientes.exportar', 'pacientes.saldos', 'visitas.ver', 'inventario.ver', 'servicios.ver', 'reportes.ver', 'reportes.exportar', 'caja.ver', 'ventas.reimprimir', 'whatsapp.ver', 'notificaciones.ver', 'formularios.ver', 'membresias.ver', 'compras.ver', 'proveedores.ver', 'comisiones.ver', ) # --------------------------------------------------------------------------- # Conjuntos de roles para los defaults. # W = roles con rango de escritura (todo menos 'lectura'). # WN = W sin marketing (a quien forbid_roles cierra los módulos de dinero). # MONEY = W sin medico ni marketing (veto de medico a las pantallas de dinero). # AG = admin + gerencia (los que hoy administran usuarios/roles/catálogos). # AD = admin (lo que hoy exige @require_role('admin')). # ALL = W + lectura. # --------------------------------------------------------------------------- W = ('admin', 'medico', 'recepcion', 'enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'gerencia', 'marketing') WN = tuple(r for r in W if r != 'marketing') MONEY = tuple(r for r in W if r not in ('medico', 'marketing')) AG = ('admin', 'gerencia') AD = ('admin',) ALL = W + ('lectura',) # --------------------------------------------------------------------------- # Defaults por permiso. Tabla explicable: # - lectura -> solo los READ_PERM_KEYS. # - W -> escritura general (agenda, pacientes, visitas, visitas...). # - WN / MONEY -> bloques de dinero: coinciden con require_role('recepcion') # + forbid_roles('marketing') y con el veto de 'medico'. # - AG / AD -> administración (hoy restringida a admin/gerencia o admin). # --------------------------------------------------------------------------- DEFAULT_PERM_ROLES = { # Agenda (hoy: require_role('recepcion') para escribir) 'agenda.ver': ALL, 'agenda.agendar': W, 'agenda.editar': W, 'agenda.eliminar': W, 'agenda.mover': W, 'agenda.visitas': W, 'agenda.todas': W, 'agenda.bloquear': W, # Recursos de la agenda: ver es de todos; administrarlos es de admin/gerencia. 'recursos.ver': ALL, 'recursos.editar': AG, # Pacientes 'pacientes.ver': ALL, 'pacientes.expediente': ALL, 'pacientes.crear': W, 'pacientes.editar': W, 'pacientes.eliminar': W, 'pacientes.exportar': ALL, 'pacientes.saldos': ALL, # Visitas 'visitas.ver': ALL, 'visitas.crear': W, 'visitas.editar': W, 'visitas.eliminar': W, 'visitas.archivos': W, # Ventas y cobros 'ventas.vender': MONEY, 'ventas.cobrar': WN, # medico también cobra depósitos de paciente hoy 'ventas.descuentos': MONEY, 'ventas.impuestos': MONEY, 'ventas.cancelar': AG, # hoy: admin/gerencia (chequeo explícito en el endpoint) 'ventas.devolver': MONEY, 'ventas.reimprimir': ALL, # Inventario (hoy: require_role('recepcion') + forbid_roles('marketing')) 'inventario.ver': ALL, 'inventario.ajustar': WN, 'inventario.mover': WN, 'inventario.costos': WN, # Servicios y catálogo (hoy services POST/PUT/DELETE exigen admin) 'servicios.ver': ALL, 'servicios.editar': AD, # Reportes y caja 'reportes.ver': ALL, 'reportes.exportar': ALL, 'caja.ver': ALL, 'caja.cerrar': MONEY, # WhatsApp y leads (menú WACRM: admin, recepcion, gerencia, marketing) 'whatsapp.ver': W, 'whatsapp.responder': W, 'whatsapp.asignar': W, # Notificaciones de citas: ver es de todos; enviar lo hace recepción y # quien agenda; configurar (plantillas y conexión) es de admin/gerencia. 'notificaciones.ver': ALL, 'notificaciones.enviar': W, 'notificaciones.editar': AG, # Formularios digitales y consentimientos: ver es de todos; llenar/enviar lo # hace quien atiende al paciente; administrar plantillas es de admin/gerencia. 'formularios.ver': ALL, 'formularios.crear': ('admin', 'medico', 'recepcion', 'enfermeria', 'gerencia'), 'formularios.editar': AG, 'formularios.enviar': ('admin', 'medico', 'recepcion', 'enfermeria', 'gerencia'), # Membresías y suscripciones. Ver es de todos; dar de alta y cobrar es de # recepción/gerencia/admin; editar planes y cancelar/congelar es de # gerencia/admin (contra el dinero recurrente de la clínica). 'membresias.ver': ALL, 'membresias.crear': ('admin', 'recepcion', 'gerencia'), 'membresias.cobrar': ('admin', 'recepcion', 'gerencia'), 'membresias.editar': AG, 'membresias.cancelar': AG, # Compras y proveedores. Ver es de todos; crear/recibir/pagar es de # recepción/gerencia/admin (igual que inventario y membresías); editar, # cancelar y administrar proveedores es de admin/gerencia (tocan dinero). 'compras.ver': ALL, 'compras.crear': ('admin', 'recepcion', 'gerencia'), 'compras.recibir': ('admin', 'recepcion', 'gerencia'), 'compras.pagar': ('admin', 'recepcion', 'gerencia'), 'compras.editar': AG, 'compras.cancelar': AG, 'proveedores.ver': ALL, 'proveedores.editar': AG, # Comisiones y liquidaciones. Ver es de todos (el rol médico ve SOLO las # suyas: el backend filtra por su empleado); administrar el motor de reglas y # metas, y liquidar/pagar, es de admin/gerencia (tocan dinero del personal). 'comisiones.ver': ALL, 'comisiones.editar': AG, 'comisiones.liquidar': AG, # Usuarios y configuración 'config.usuarios': AG, 'config.roles': AG, 'config.editar': AG, 'config.auditoria': AD, # Plataforma: claves de API solo admin; webhooks admin/gerencia. 'api.claves': AD, 'webhooks.editar': AG, } # Los roles nuevos (creados desde la UI) arrancan como 'lectura': el admin # decide después qué permisos les da. Si no, cualquier rol nuevo tendría acceso # de escritura sin que nadie lo pidiera. NEW_ROLE_BASE = 'lectura' def default_perms_for(role_code): """Dict {perm_key: bool} por defecto para un rol (base por rango).""" base_role = role_code if role_code in W or role_code == 'lectura' else NEW_ROLE_BASE perms = {} for key in PERM_KEYS: if base_role == 'lectura': perms[key] = key in READ_PERM_KEYS else: perms[key] = base_role in (DEFAULT_PERM_ROLES.get(key) or ()) return perms def default_role_perms(): """Defaults completos {role_code: {perm_key: bool}} para los 10 roles base. No incluye roles personalizados (no se conocen sin consultar Odoo); el frontend usa su propio espejo para eso. """ return {code: default_perms_for(code) for code in ('admin', 'medico', 'recepcion', 'enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'gerencia', 'marketing', 'lectura')} def _stored(env): """Overrides guardados {role_code: {perm_key: bool}} (o {}).""" raw = env['ir.config_parameter'].sudo().get_param(ROLE_PERMS_KEY) if not raw: return {} try: val = json.loads(raw) except Exception: return {} if not isinstance(val, dict): return {} clean = {} for role, perms in val.items(): if isinstance(perms, dict): clean[role] = {k: bool(v) for k, v in perms.items() if k in PERM_KEYS} return clean def resolve_role_perms(env, role_code): """Permisos vigentes de un rol: defaults + overrides guardados.""" perms = default_perms_for(role_code) perms.update(_stored(env).get(role_code) or {}) return perms def resolve_role_perms_for_user(user): """Permisos vigentes del usuario (por su rol). None -> todo False.""" if user is None: return {k: False for k in PERM_KEYS} return resolve_role_perms(user.env, user.role) def has_perm(user, perm_key): """True si el usuario puede ejecutar la acción. Admin siempre puede.""" if user is None: return False if user.role == 'admin': return True return bool(resolve_role_perms_for_user(user).get(perm_key)) def all_roles_perms(env): """{role_code: {perm_key: bool}} para todos los roles en skeen.role.""" roles = env['skeen.role'].sudo().with_context(active_test=False).search([]) codes = [r.code for r in roles if r.code] if not codes: return default_role_perms() return {code: resolve_role_perms(env, code) for code in codes} def set_role_perms(env, role_code, perms): """Guarda permisos de un rol (merge con lo ya guardado). False si no aplica.""" rol = env['skeen.role'].sudo().with_context(active_test=False).search( [('code', '=', role_code)], limit=1) if not rol: return False clean = {k: bool(v) for k, v in (perms or {}).items() if k in PERM_KEYS} if not clean: return False stored = _stored(env) current = dict(stored.get(role_code) or {}) current.update(clean) stored[role_code] = current env['ir.config_parameter'].sudo().set_param(ROLE_PERMS_KEY, json.dumps(stored)) return True def reset_role_perms(env, role_code): """Quita los overrides de un rol (vuelve a los defaults).""" stored = _stored(env) if role_code in stored: stored.pop(role_code) env['ir.config_parameter'].sudo().set_param(ROLE_PERMS_KEY, json.dumps(stored)) return True