Files
SKEEN-Proyecto/odoo-addons/skeen_whatsapp/roles.py

242 lines
9.6 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'],
'/cortes': ['admin', 'recepcion', 'gerencia'],
'/reportes': ['admin', 'recepcion', 'lectura', 'gerencia'],
'/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=exportar': ['admin', 'recepcion', 'lectura', 'gerencia'],
'/configuracion': ['admin', 'gerencia'],
'/cumpleanos': ['admin', 'recepcion', 'gerencia', 'marketing'],
'/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',
)
# 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()