255 lines
10 KiB
Python
255 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Roles del frontend React y matriz de permisos de menú por rol.
|
|
|
|
Módulo sin dependencias de Odoo para poder importarse tanto desde modelos
|
|
como desde controladores sin problemas de orden de carga.
|
|
|
|
La matriz se persiste en ir.config_parameter 'skeen.menu_roles' como JSON
|
|
{href: [roles...]}. Si no existe, se usan los defaults de DEFAULT_MENU_ROLES
|
|
(mismos que MENU_ROLES hardcodeado del frontend) y los 5 roles nuevos heredan
|
|
el mismo set de secciones que 'lectura'.
|
|
"""
|
|
|
|
import json
|
|
|
|
ROLE_SELECTION = [
|
|
('admin', 'Administrador'),
|
|
('medico', 'Médico'),
|
|
('recepcion', 'Recepción'),
|
|
('enfermeria', 'Enfermería'),
|
|
('cosmetologia', 'Cosmetología'),
|
|
('psicologa', 'Psicóloga'),
|
|
('farmacia', 'Farmacia'),
|
|
('gerencia', 'Gerencia'),
|
|
('marketing', 'Marketing'),
|
|
('lectura', 'Lectura'),
|
|
]
|
|
|
|
ROLE_KEYS = tuple(k for k, _ in ROLE_SELECTION)
|
|
|
|
ROLE_RANKS = {
|
|
'admin': 100,
|
|
'medico': 50,
|
|
'recepcion': 50,
|
|
'enfermeria': 50,
|
|
'cosmetologia': 50,
|
|
'psicologa': 50,
|
|
'farmacia': 50,
|
|
'gerencia': 50,
|
|
# marketing pasó a 50: puede agendar citas y escribir en el WACRM, igual
|
|
# que recepción (los permisos de ESCRITURA dependen del rango).
|
|
'marketing': 50,
|
|
'lectura': 10,
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Roles dinámicos (modelo skeen.role). ROLE_SELECTION/ROLE_RANKS quedan como
|
|
# respaldo para el arranque (antes de que la semilla XML esté disponible) y para
|
|
# no romper instalaciones viejas.
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def role_records(env):
|
|
"""Registros de skeen.role (incluye inactivos) o lista vacía si no existe."""
|
|
try:
|
|
return env['skeen.role'].sudo().with_context(active_test=False).search([])
|
|
except KeyError:
|
|
return []
|
|
|
|
|
|
def role_selection(env):
|
|
"""[(code, name)] de los roles configurados, con respaldo estático."""
|
|
seleccion = [(r.code, r.name) for r in role_records(env) if r.code]
|
|
return seleccion or ROLE_SELECTION
|
|
|
|
|
|
def role_keys(env):
|
|
"""Códigos de rol válidos (incluye inactivos, para no invalidar históricos)."""
|
|
return tuple(k for k, _ in role_selection(env))
|
|
|
|
|
|
def role_rank_map(env):
|
|
"""{code: rank} de los roles configurados, con respaldo estático."""
|
|
mapa = {r.code: r.rank for r in role_records(env) if r.code}
|
|
return mapa or dict(ROLE_RANKS)
|
|
|
|
|
|
def min_rank(env, code):
|
|
"""Rango mínimo exigido por un código ('recepcion' -> 50 por defecto)."""
|
|
return role_rank_map(env).get(code, ROLE_RANKS.get(code, 10))
|
|
|
|
MENU_ROLES_KEY = 'skeen.menu_roles'
|
|
|
|
# ------------------------------------------------------------------
|
|
# Permisos de ACCIÓN dentro de la Agenda (selector por usuario).
|
|
#
|
|
# No son visibilidad de menú: deciden qué se puede HACER una vez dentro.
|
|
# Claves estables (se guardan como JSON en skeen.frontend.user.agenda_perms).
|
|
# ------------------------------------------------------------------
|
|
AGENDA_PERMS = [
|
|
('ver', 'Consultar agenda'),
|
|
('agendar', 'Agendar cita'),
|
|
('editar', 'Editar citas'),
|
|
('eliminar', 'Eliminar citas'),
|
|
('visitas', 'Generar visitas'),
|
|
]
|
|
AGENDA_PERM_KEYS = tuple(k for k, _ in AGENDA_PERMS)
|
|
|
|
# Acciones que requieren rango de escritura (recepción para arriba): quien no
|
|
# tiene lista propia configurada hereda esto, que es exactamente lo que ya
|
|
# permitía @require_role('recepcion'). Así el selector puede QUITAR sin
|
|
# configurar nada y no cambia el comportamiento actual de nadie.
|
|
WRITE_RANK_MIN = 'recepcion'
|
|
|
|
# Roles sin rango propio en ROLE_RANKS no existen; el default se resuelve por
|
|
# rango, no por nombre, para no duplicar la lista de roles.
|
|
|
|
|
|
# Mapa de cada acción de agenda al permiso por rol equivalente. El permiso de
|
|
# rol es el default; la lista propia del usuario (agenda_perms) lo pisa.
|
|
AGENDA_ROLE_PERM = {
|
|
'ver': 'agenda.ver',
|
|
'agendar': 'agenda.agendar',
|
|
'editar': 'agenda.editar',
|
|
'eliminar': 'agenda.eliminar',
|
|
'visitas': 'agenda.visitas',
|
|
}
|
|
|
|
|
|
def agenda_defaults_for(user):
|
|
"""Permisos de agenda por defecto de un usuario (sin lista propia).
|
|
|
|
Se derivan de los permisos por acción del rol; 'ver' siempre se conserva.
|
|
"""
|
|
if user is None:
|
|
return ['ver']
|
|
from .permissions import resolve_role_perms_for_user
|
|
role_perms = resolve_role_perms_for_user(user)
|
|
acciones = [a for a, key in AGENDA_ROLE_PERM.items() if role_perms.get(key)]
|
|
if 'ver' not in acciones:
|
|
acciones.insert(0, 'ver')
|
|
return acciones
|
|
|
|
|
|
def resolve_agenda_perms(user):
|
|
"""Permisos de agenda vigentes: los del usuario si tiene lista, si no el default.
|
|
|
|
'ver' nunca se quita: sin él no hay pantalla que mostrar y el resto de las
|
|
acciones carece de sentido. Se fuerza al subir.
|
|
"""
|
|
custom = user._agenda_perms_list() if user is not None else None
|
|
if custom is None:
|
|
return agenda_defaults_for(user)
|
|
valid = [k for k in AGENDA_PERM_KEYS if k in custom]
|
|
if 'ver' not in valid:
|
|
valid.insert(0, 'ver')
|
|
return valid
|
|
|
|
# Defaults = MENU_ROLES de src/lib/auth.tsx (4 roles originales).
|
|
# 'gerencia' (gerencia de la clínica) ve toda la
|
|
# operación EXCEPTO /configuracion (solo admin del sistema), /mis-ventas,
|
|
# /cumpleanos y /wacrm/*.
|
|
DEFAULT_MENU_ROLES = {
|
|
'/' : ['admin', 'medico', 'recepcion', 'enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'gerencia', 'marketing', 'lectura'],
|
|
'/agenda': ['admin', 'recepcion', 'medico', 'gerencia', 'cosmetologia', 'marketing'],
|
|
'/mis-ventas': ['medico'],
|
|
'/pacientes': ['admin', 'recepcion', 'medico', 'lectura', 'gerencia', 'marketing'],
|
|
'/pos': ['admin', 'recepcion', 'gerencia'],
|
|
'/visitas': ['admin', 'recepcion', 'medico', 'gerencia', 'marketing'],
|
|
'/consultas': ['admin', 'recepcion', 'medico', 'gerencia'],
|
|
'/medicos': ['admin', 'recepcion', 'gerencia'],
|
|
'/servicios': ['admin', 'recepcion', 'medico', 'lectura', 'gerencia'],
|
|
'/paquetes': ['admin', 'recepcion', 'medico', 'lectura', 'gerencia'],
|
|
# Membresías: apagado por defecto (membresias.activo=false). La sección solo
|
|
# se pinta cuando la instancia la enciende; aquí queda su visibilidad por rol.
|
|
'/membresias': ['admin', 'recepcion', 'medico', 'lectura', 'gerencia'],
|
|
'/productos': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/ventas': ['admin', 'recepcion', 'gerencia'],
|
|
'/tickets': ['admin', 'gerencia'],
|
|
'/pagos': ['admin', 'recepcion', 'gerencia'],
|
|
'/monedero': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/inventario': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
# Compras: apagado por defecto (compras.activo=false). La sección solo se
|
|
# pinta cuando la instancia la enciende; aquí queda su visibilidad por rol.
|
|
'/compras': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/cortes': ['admin', 'recepcion', 'gerencia'],
|
|
'/reportes': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
# Tablero de dirección: solo dueño/administración y marketing. El rol médico
|
|
# no lo ve (tampoco 'lectura'). Su acceso a la API lo cierra `direccion.ver`.
|
|
'/direccion': ['admin', 'gerencia', 'marketing'],
|
|
'/reportes?tab=adeudos': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=comisiones': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=horas-agenda': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=paquetes': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=membresias': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=vendedores': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=concentrado': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=recomendaciones': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=compras': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=margen': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/reportes?tab=exportar': ['admin', 'recepcion', 'lectura', 'gerencia'],
|
|
'/configuracion': ['admin', 'gerencia'],
|
|
'/cumpleanos': ['admin', 'recepcion', 'gerencia', 'marketing'],
|
|
# Campañas y satisfacción: apagadas por defecto (campanas.activo /
|
|
# encuestas.activo). Se listan aquí para que canSee las reconozca cuando la
|
|
# instancia las encienda.
|
|
'/campanas': ['admin', 'recepcion', 'gerencia', 'marketing'],
|
|
'/satisfaccion': ['admin', 'recepcion', 'gerencia', 'medico'],
|
|
'/wacrm/messages': ['admin', 'recepcion', 'gerencia', 'marketing'],
|
|
'/wacrm/leads': ['admin', 'recepcion', 'gerencia', 'marketing'],
|
|
# El chat interno se lista explícitamente para dejar fuera a marketing
|
|
# (antes, al no estar listado, lo veía todo el mundo).
|
|
'/chat': ['admin', 'medico', 'recepcion', 'enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'gerencia', 'lectura'],
|
|
}
|
|
|
|
# Marketing NO hereda los menús de 'lectura': sólo estos (mínimo privilegio).
|
|
MARKETING_MENUS = (
|
|
'/', '/agenda', '/pacientes', '/cumpleanos', '/visitas',
|
|
'/wacrm/messages', '/wacrm/leads', '/campanas', '/direccion',
|
|
)
|
|
|
|
# Roles nuevos que por default ven lo mismo que 'lectura'.
|
|
READ_ONLY_NEW_ROLES = ('enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'marketing')
|
|
|
|
|
|
def default_menu_roles():
|
|
"""Matriz por default: MENU_ROLES + roles nuevos donde esté 'lectura'.
|
|
|
|
Marketing es la excepción: no hereda lo de 'lectura', sólo entra en sus
|
|
menús explícitos (agenda, pacientes, cumpleaños, visitas y WACRM).
|
|
"""
|
|
matrix = {}
|
|
for href, roles in DEFAULT_MENU_ROLES.items():
|
|
r = list(roles)
|
|
if 'lectura' in roles:
|
|
r.extend(nr for nr in READ_ONLY_NEW_ROLES if nr not in r)
|
|
matrix[href] = r
|
|
for href in matrix:
|
|
matrix[href] = [r for r in matrix[href] if r != 'marketing']
|
|
for href in MARKETING_MENUS:
|
|
matrix.setdefault(href, [])
|
|
if 'marketing' not in matrix[href]:
|
|
matrix[href].append('marketing')
|
|
return matrix
|
|
|
|
|
|
def get_menu_roles(env):
|
|
"""Matriz vigente: la guardada en ir.config_parameter o los defaults.
|
|
|
|
La matriz guardada manda sobre los defaults, PERO se completan en memoria
|
|
las rutas que no existan en ella. Sin eso, una ruta nueva (p. ej.
|
|
'/paquetes') no aparecería en la matriz guardada y el frontend la trataría
|
|
como "visible para todos" por no matchear ninguna llave.
|
|
"""
|
|
raw = env['ir.config_parameter'].sudo().get_param(MENU_ROLES_KEY)
|
|
if raw:
|
|
try:
|
|
val = json.loads(raw)
|
|
if isinstance(val, dict):
|
|
for href, roles in default_menu_roles().items():
|
|
val.setdefault(href, roles)
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return default_menu_roles()
|