- {favs.map((k) => (
+ {favsVisibles.map((k) => (
{
{/* Familias */}
- {FAMILIAS.map((f) => {
+ {familiasVisibles.map((f) => {
const Icono = f.icon;
const activa = f.key === familiaKey;
return (
diff --git a/odoo-addons/skeen_chat/controllers/chat.py b/odoo-addons/skeen_chat/controllers/chat.py
index fed1a1a..b74ef10 100644
--- a/odoo-addons/skeen_chat/controllers/chat.py
+++ b/odoo-addons/skeen_chat/controllers/chat.py
@@ -27,17 +27,20 @@ class SkeenChatController(http.Controller):
def _touch(self, me):
"""Marca actividad del usuario (presencia). Nunca debe romper la petición.
- Se hace con SQL directo dentro de un savepoint: el frontend consulta
- varios endpoints de chat en paralelo (bandeja, badge, heartbeat) y dos
- UPDATE concurrentes sobre la misma fila abortan la transacción en
- REPEATABLE READ ("could not serialize access due to concurrent update").
- Con el savepoint ese fallo se descarta y la petición sigue; además sólo
- se escribe si el último registro tiene más de TOUCH_INTERVAL segundos,
- que es lo que evita la mayor parte de los choques.
+ El frontend consulta varios endpoints de chat en paralelo (bandeja,
+ badge, heartbeat) y todos llaman aquí. Por eso se corta antes de tocar
+ la fila cuando el registro es reciente (TOUCH_INTERVAL): el UPDATE se
+ ejecutaba aunque no aplicara ningún cambio y abortaba la transacción de
+ otra petición concurrente ("could not serialize access due to concurrent
+ update", 211 veces en un barrido de 260 visitas). El savepoint sigue
+ ahí por si dos peticiones pasan el corte en el mismo instante; su fallo
+ se descarta y la petición continúa.
"""
try:
ahora = fields.Datetime.now()
limite = fields.Datetime.subtract(ahora, seconds=TOUCH_INTERVAL)
+ if me.last_seen and me.last_seen >= limite:
+ return
cr = request.env.cr
with cr.savepoint():
cr.execute(
diff --git a/odoo-addons/skeen_whatsapp/controllers/auth.py b/odoo-addons/skeen_whatsapp/controllers/auth.py
index d67f5d6..578a02b 100644
--- a/odoo-addons/skeen_whatsapp/controllers/auth.py
+++ b/odoo-addons/skeen_whatsapp/controllers/auth.py
@@ -15,7 +15,7 @@ import time
from odoo.http import request, Response
-from ..roles import ROLE_RANKS
+from ..roles import ROLE_RANKS, get_menu_roles
TTL_SECONDS = 60 * 60 * 12 # 12 horas
SECRET_KEY = 'skeen.frontend.secret'
@@ -36,6 +36,26 @@ MEDICO_BLOCKED_PREFIXES = (
)
+def _medico_reporte_permitido(env, path):
+ """True si la matriz de menus concede al medico ESA pestana de reportes.
+
+ El veto de 'medico' sobre /reports/ es por diseno (no ve dinero), pero la
+ matriz permite habilitar pestañas puntuales (/reportes?tab=horas-agenda).
+ Sin este puente el menu mostraria la opcion y la API la rechazaria con 403.
+ Las demas rutas vetadas (sales, payments, wallets...) siguen cerradas.
+ """
+ prefijo = '/skeen/frontend/v1/reports/'
+ if not path.startswith(prefijo):
+ return False
+ tab = path[len(prefijo):].strip('/')
+ if not tab or '/' in tab:
+ return False
+ matriz = get_menu_roles(env)
+ if 'medico' in (matriz.get('/reportes') or []):
+ return True
+ return 'medico' in (matriz.get('/reportes?tab=%s' % tab) or [])
+
+
def _cors_headers():
return {
'Access-Control-Allow-Origin': '*',
@@ -156,7 +176,7 @@ def require_role(min_role='lectura'):
request.httprequest.path == p or request.httprequest.path.startswith(
p if p.endswith('/') else p + '/')
for p in MEDICO_BLOCKED_PREFIXES
- ):
+ ) and not _medico_reporte_permitido(request.env, request.httprequest.path):
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
request.frontend_user = user
return func(self, *args, **kw)