Oleada UX 5: Servicios (duplicar, favoritos y orden para POS, mas vendidos) y Medicos (semana tipo con validacion en agenda, meta mensual con progreso)

This commit is contained in:
2026-09-17 21:28:43 +00:00
parent 9ee3343e22
commit cd945df6a5
10 changed files with 885 additions and 41 deletions

View File

@@ -1,6 +1,59 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
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):
@@ -18,3 +71,90 @@ class HrEmployee(models.Model):
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, ''

View File

@@ -4,3 +4,4 @@ access_skeen_patient_adjunto_user,Acceso a Adjuntos de Paciente SKEEN,model_skee
access_skeen_receta_user,Acceso a Recetas SKEEN,model_skeen_receta,base.group_user,1,1,1,1
access_skeen_diagnostico_user,Acceso a Diagnósticos SKEEN,model_skeen_diagnostico,base.group_user,1,1,1,1
access_skeen_procedimiento_user,Acceso a Procedimientos SKEEN,model_skeen_procedimiento,base.group_user,1,1,1,1
access_hr_employee_horario_user,Acceso a Horario semanal del médico,model_hr_employee_horario,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_skeen_receta_user Acceso a Recetas SKEEN model_skeen_receta base.group_user 1 1 1 1
5 access_skeen_diagnostico_user Acceso a Diagnósticos SKEEN model_skeen_diagnostico base.group_user 1 1 1 1
6 access_skeen_procedimiento_user Acceso a Procedimientos SKEEN model_skeen_procedimiento base.group_user 1 1 1 1
7 access_hr_employee_horario_user Acceso a Horario semanal del médico model_hr_employee_horario base.group_user 1 1 1 1