fase 3.1: recursos fisicos en agenda (modelo skeen.recurso con semilla cabina/consultoria, asignacion multiple a citas con validacion de solape, bloqueos por recurso, lista de espera desde recursos, vista/filtro por recurso, reporte de ocupacion); apagado por defecto

This commit is contained in:
2026-10-11 11:33:59 +00:00
parent 8ff13b9c2a
commit ee8a844d96
29 changed files with 1986 additions and 78 deletions

View File

@@ -606,6 +606,66 @@ def get_membresias(env):
"""Bloque de membresías vigente (copia). Fuente única para modelos y API."""
return normalizar_membresias(get_settings(env).get('membresias'))
# ---------------------------------------------------------------------------
# Recursos físicos de la agenda (cabinas, salas, consultorios, equipos).
#
# Bloque anidado del mismo JSON de ajustes, igual que `impuestos`. La regla de
# oro: con `activo=False` (default) NADA cambia en la agenda — no aparece la
# sección en Configuración ni el selector en el modal de cita, la agenda se ve
# igual que hoy y no corre ninguna validación de solape de recursos. Por eso el
# default es apagado.
#
# activo: enciende todo el módulo de recursos
# validar_solape: impide dos citas/bloqueos en el mismo recurso a la
# misma hora (solo aplica si activo)
# obligatorio_al_agendar: exige elegir al menos un recurso al crear la cita
# permitir_multiples: permite asignar más de un recurso a una cita
# mostrar_en_agenda: muestra el recurso (chips/color) en las tarjetas
# filtrar_como_doctores: ofrece el filtro por recurso junto al de doctores
# ---------------------------------------------------------------------------
RECURSOS_DEFAULT = {
'activo': False,
'validar_solape': True,
'obligatorio_al_agendar': False,
'permitir_multiples': True,
'mostrar_en_agenda': True,
'filtrar_como_doctores': True,
}
RECURSOS_BOOLEANOS = tuple(RECURSOS_DEFAULT)
def normalizar_recursos(raw):
"""Bloque `recursos` completo, con defaults. Tolerante: nunca lanza."""
out = dict(RECURSOS_DEFAULT)
if not isinstance(raw, dict):
return out
for k in RECURSOS_BOOLEANOS:
out[k] = _bool(raw.get(k), RECURSOS_DEFAULT[k])
return out
def validar_recursos(raw):
"""Valida el bloque `recursos` de un PUT. Lanza ValueError si algo no
cuadra (se traduce a 400). Devuelve SOLO las claves presentes y normalizadas
para que el merge anidado no reinicie lo que el PUT no tocó."""
if raw is None:
return None
if not isinstance(raw, dict):
raise ValueError('El bloque de recursos debe ser un objeto')
out = {}
for k in RECURSOS_BOOLEANOS:
if k in raw:
if not isinstance(raw[k], bool):
raise ValueError('El campo %s debe ser verdadero o falso' % k)
out[k] = raw[k]
return out
def get_recursos(env):
"""Bloque de recursos vigente (copia). Fuente única para modelos y API."""
return normalizar_recursos(get_settings(env).get('recursos'))
# Horas de agenda de respaldo: el comportamiento de siempre (8:00-19:00).
AGENDA_START_DEFAULT = 8.0
AGENDA_END_DEFAULT = 19.0
@@ -649,6 +709,7 @@ def get_settings(env):
'reservas_online': dict(RESERVAS_ONLINE_DEFAULT),
'notificaciones': normalizar_notificaciones(None),
'membresias': normalizar_membresias(None),
'recursos': dict(RECURSOS_DEFAULT),
}
raw = env['ir.config_parameter'].sudo().get_param(SETTINGS_KEY)
if raw:
@@ -664,6 +725,7 @@ def get_settings(env):
out['reservas_online'] = normalizar_reservas_online(guardado.get('reservas_online'))
out['notificaciones'] = normalizar_notificaciones(guardado.get('notificaciones'))
out['membresias'] = normalizar_membresias(guardado.get('membresias'))
out['recursos'] = normalizar_recursos(guardado.get('recursos'))
return out
@@ -692,6 +754,10 @@ def set_settings(env, vals):
bloque = validar_membresias(vals['membresias'])
if bloque is not None:
actual['membresias'] = {**actual['membresias'], **bloque}
if 'recursos' in vals:
bloque = validar_recursos(vals['recursos'])
if bloque is not None:
actual['recursos'] = {**actual['recursos'], **bloque}
env['ir.config_parameter'].sudo().set_param(
SETTINGS_KEY, json.dumps(actual, ensure_ascii=False))
return get_settings(env)