# -*- coding: utf-8 -*- from odoo import models, fields, api from odoo.exceptions import ValidationError DIAS = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'] DIAS_ABREV = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'] def _hora_str(h): """9.5 -> '9:30'""" h = float(h or 0.0) return '%d:%02d' % (int(h), int(round((h - int(h)) * 60))) class HrEmployeeHorario(models.Model): """Línea de horario semanal del médico. Un registro por día que el médico atiende. Un empleado SIN registros se considera sin horario configurado (la agenda se comporta como siempre). """ _name = 'hr.employee.horario' _description = 'Horario semanal del médico' _order = 'day_of_week' employee_id = fields.Many2one('hr.employee', string='Médico', required=True, ondelete='cascade', index=True) day_of_week = fields.Selection([ ('0', 'Lunes'), ('1', 'Martes'), ('2', 'Miércoles'), ('3', 'Jueves'), ('4', 'Viernes'), ('5', 'Sábado'), ('6', 'Domingo'), ], string='Día', required=True) hour_from = fields.Float(string='Hora inicio', required=True, default=9.0) hour_to = fields.Float(string='Hora fin', required=True, default=14.0) day_label = fields.Char(string='Día (texto)', compute='_compute_day_label') _sql_constraints = [ ('unique_employee_day', 'unique(employee_id, day_of_week)', 'Ya hay un horario registrado para ese día.'), ] @api.depends('day_of_week') def _compute_day_label(self): for rec in self: rec.day_label = DIAS[int(rec.day_of_week or 0)] @api.constrains('hour_from', 'hour_to') def _check_horas(self): for rec in self: if rec.hour_to <= rec.hour_from: raise ValidationError('La hora fin debe ser mayor a la hora inicio.') class HrEmployee(models.Model): _inherit = 'hr.employee' commission_pct = fields.Float( string='% Comisión', default=0.0, help='Porcentaje de comisión sobre la venta de artículos recetados por este médico.', ) staff_type = fields.Selection( [('doctor', 'Doctor'), ('cosmetologa', 'Cosmetóloga')], string='Tipo de Staff', default='doctor', help='Tipo de personal de la agenda: Doctor o Cosmetóloga.', ) monthly_goal = fields.Float( string='Meta mensual', default=0.0, help='Objetivo de venta del mes (m.n.) para el tablero del médico.', ) horario_ids = fields.One2many( 'hr.employee.horario', 'employee_id', string='Horario semanal', help='Días y horas que atiende. Si está vacío, la agenda no restringe horarios.', ) tiene_horario = fields.Boolean( string='Con horario configurado', compute='_compute_tiene_horario', ) horario_resumen = fields.Char( string='Horario', compute='_compute_horario_resumen', help='Resumen legible (p. ej. "Lun-Vie 9:00-14:00").', ) @api.depends('horario_ids') def _compute_tiene_horario(self): for emp in self: emp.tiene_horario = bool(emp.horario_ids) @api.depends('horario_ids.day_of_week', 'horario_ids.hour_from', 'horario_ids.hour_to') def _compute_horario_resumen(self): for emp in self: emp.horario_resumen = emp._horario_resumen() # ------------------------------------------------------------------ # Consultas de horario # ------------------------------------------------------------------ def _horario_del_dia(self, fecha): """Línea de horario del día de la semana de `fecha` (vacío si no aplica).""" self.ensure_one() if not fecha: return self.env['hr.employee.horario'] return self.horario_ids.filtered(lambda h: int(h.day_of_week) == fecha.weekday())[:1] def _horario_resumen(self): """Resumen legible agrupando días consecutivos con el mismo horario. Ej.: "Lun-Vie 9:00-14:00" o "Lun, Mié 9:00-14:00; Sáb 9:00-13:00". """ self.ensure_one() if not self.horario_ids: return '' por_hora = {} for h in self.horario_ids: clave = (_hora_str(h.hour_from), _hora_str(h.hour_to)) por_hora.setdefault(clave, []).append(int(h.day_of_week)) partes = [] for (desde, hasta), dias in por_hora.items(): dias = sorted(dias) tramos = [] inicio = previo = dias[0] for d in dias[1:]: if d == previo + 1: previo = d continue tramos.append((inicio, previo)) inicio = previo = d tramos.append((inicio, previo)) etiquetas = [ DIAS_ABREV[a] if a == b else '%s-%s' % (DIAS_ABREV[a], DIAS_ABREV[b]) for a, b in tramos ] partes.append('%s %s-%s' % (', '.join(etiquetas), desde, hasta)) return '; '.join(partes) def _valida_cita_en_horario(self, fecha, hora_ini, duracion_min): """(ok, mensaje) para una cita en `fecha` a las `hora_ini` (float de horas). Si el médico no tiene horario configurado devuelve (True, ''). """ self.ensure_one() if not self.horario_ids or not fecha: return True, '' linea = self._horario_del_dia(fecha) resumen = self._horario_resumen() or 'sin definir' if not linea: return False, 'El doctor no atiende el %s (su horario: %s)' % ( DIAS[fecha.weekday()].lower(), resumen) dur = (duracion_min or 30) / 60.0 if hora_ini < linea.hour_from - 1e-6 or hora_ini + dur > linea.hour_to + 1e-6: return False, 'El doctor no atiende a esa hora (su horario: %s)' % resumen return True, ''