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:
@@ -4,7 +4,7 @@ from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..clinic_settings import agenda_bounds
|
||||
from ..clinic_settings import agenda_bounds, get_recursos
|
||||
|
||||
|
||||
# Estados que liberan el horario: no ocupan agenda ni bloquean solapes.
|
||||
@@ -134,6 +134,11 @@ class SkeenCita(models.Model):
|
||||
string='Sucursal',
|
||||
default=lambda self: self.env['skeen.sucursal'].default_code())
|
||||
doctor_id = fields.Many2one('hr.employee', string='Médico / Especialista')
|
||||
# Recursos físicos asignados (cabina, sala, equipo). Vacío mientras la
|
||||
# configuración de la clínica no active el módulo de recursos.
|
||||
recurso_ids = fields.Many2many(
|
||||
'skeen.recurso', 'skeen_cita_recurso_rel', 'cita_id', 'recurso_id',
|
||||
string='Recursos')
|
||||
medium = fields.Selection([
|
||||
('videocall', 'Videollamada'),
|
||||
('onsite', 'Presencial'),
|
||||
@@ -432,6 +437,52 @@ class SkeenCita(models.Model):
|
||||
if b.all_day or (ini < b.time_to and fin > b.time_from):
|
||||
raise ValidationError(_('El médico tiene un bloqueo (%s)') % b.nombre_motivo())
|
||||
|
||||
@api.constrains('date', 'time', 'servicio_id', 'duration', 'recurso_ids', 'state')
|
||||
def _check_recursos_solape(self):
|
||||
"""Rechaza citas que pisen un recurso ya ocupado.
|
||||
|
||||
Solo corre cuando la clínica tiene `recursos.activo` y `validar_solape`;
|
||||
con el módulo apagado (default) no cambia nada. Un recurso está ocupado
|
||||
por otra cita activa (excluye cancelled/no_show/waitlist) o por un
|
||||
bloqueo de agenda del recurso. El mensaje indica el recurso y con quién
|
||||
choca, para que la UI lo muestre legible.
|
||||
"""
|
||||
if self.env.context.get('skeen_skip_recursos_solape'):
|
||||
return
|
||||
cfg = get_recursos(self.env)
|
||||
if not (cfg['activo'] and cfg['validar_solape']):
|
||||
return
|
||||
Bloqueo = self.env['skeen.cita.bloqueo']
|
||||
for cita in self:
|
||||
if cita.state in INACTIVE_STATES or not cita.date or not cita.recurso_ids:
|
||||
continue
|
||||
ini = cita.time
|
||||
fin = ini + cita._duracion() / 60.0
|
||||
for recurso in cita.recurso_ids:
|
||||
otra = self.search([
|
||||
('id', '!=', cita.id),
|
||||
('date', '=', cita.date),
|
||||
('state', 'not in', INACTIVE_STATES),
|
||||
('recurso_ids', 'in', recurso.id),
|
||||
('datetime_start', '<', cita.datetime_end),
|
||||
('datetime_end', '>', cita.datetime_start),
|
||||
], limit=1)
|
||||
if otra:
|
||||
raise ValidationError(_(
|
||||
'%(rec)s está ocupado por %(pac)s a las %(hora)s. '
|
||||
'Elige otro horario o desasigna el recurso.'
|
||||
) % {'rec': recurso.name, 'pac': otra.partner_id.name or 'otra cita',
|
||||
'hora': otra.time_str or ''})
|
||||
bloqueos = Bloqueo.search([
|
||||
('date', '=', cita.date),
|
||||
('recurso_ids', 'in', recurso.id),
|
||||
])
|
||||
for b in bloqueos:
|
||||
if b.all_day or (ini < b.time_to and fin > b.time_from):
|
||||
raise ValidationError(_(
|
||||
'%(rec)s tiene la agenda bloqueada (%(motivo)s).'
|
||||
) % {'rec': recurso.name, 'motivo': b.nombre_motivo()})
|
||||
|
||||
@api.constrains('date', 'time', 'doctor_id', 'servicio_id', 'duration')
|
||||
def _check_horario_medico(self):
|
||||
"""Rechaza citas fuera del horario semanal del médico.
|
||||
|
||||
Reference in New Issue
Block a user