clinical_laboratory/lims_management/models/lims_result.py
Luis Ernesto Portillo Zaldivar 5bee8e79df feat(#51): Task 5 completada - Modificar modelo lims.result
- Cambio de parameter_name (Char) a parameter_id (Many2one a lims.analysis.parameter)
- Mantener parameter_name como campo related para compatibilidad
- Agregados campos: parameter_value_type, parameter_unit, value_boolean, value_display
- Implementado _compute_applicable_range() para determinar rango según paciente
- Actualizado _compute_is_out_of_range() para usar rangos flexibles y detectar valores críticos
- Validación mejorada según tipo de parámetro
- Actualizada vista de resultados en lims.test para nuevos campos

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 12:33:20 -06:00

261 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class LimsResult(models.Model):
_name = 'lims.result'
_description = 'Resultado de Prueba de Laboratorio'
_rec_name = 'display_name'
_order = 'test_id, sequence'
display_name = fields.Char(
string='Nombre',
compute='_compute_display_name',
store=True
)
test_id = fields.Many2one(
'lims.test',
string='Prueba',
required=True,
ondelete='cascade'
)
# Cambio de parameter_name a parameter_id
parameter_id = fields.Many2one(
'lims.analysis.parameter',
string='Parámetro',
required=True,
ondelete='restrict'
)
# Mantener parameter_name como campo related para compatibilidad
parameter_name = fields.Char(
string='Nombre del Parámetro',
related='parameter_id.name',
store=True,
readonly=True
)
sequence = fields.Integer(
string='Secuencia',
default=10
)
# Campos relacionados del parámetro
parameter_value_type = fields.Selection(
related='parameter_id.value_type',
string='Tipo de Valor',
store=True,
readonly=True
)
parameter_unit = fields.Char(
related='parameter_id.unit',
string='Unidad',
readonly=True
)
# Valores del resultado
value_numeric = fields.Float(
string='Valor Numérico'
)
value_text = fields.Char(
string='Valor de Texto'
)
value_selection = fields.Char(
string='Valor de Selección'
)
value_boolean = fields.Boolean(
string='Valor Sí/No'
)
# Campo unificado para mostrar el valor
value_display = fields.Char(
string='Valor',
compute='_compute_value_display',
store=True
)
# Campos computados para validación de rangos
applicable_range_id = fields.Many2one(
'lims.parameter.range',
compute='_compute_applicable_range',
string='Rango Aplicable',
store=False
)
is_out_of_range = fields.Boolean(
string='Fuera de Rango',
compute='_compute_is_out_of_range',
store=True
)
is_critical = fields.Boolean(
string='Valor Crítico',
compute='_compute_is_out_of_range',
store=True
)
notes = fields.Text(
string='Notas del Técnico'
)
# Información del paciente (para cálculo de rangos)
patient_id = fields.Many2one(
related='test_id.patient_id',
string='Paciente',
store=True
)
test_date = fields.Datetime(
related='test_id.create_date',
string='Fecha de la Prueba',
store=True
)
@api.depends('test_id', 'parameter_name')
def _compute_display_name(self):
"""Calcula el nombre a mostrar."""
for record in self:
if record.test_id and record.parameter_name:
record.display_name = f"{record.test_id.name} - {record.parameter_name}"
else:
record.display_name = record.parameter_name or _('Nuevo')
@api.depends('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
def _compute_value_display(self):
"""Calcula el valor a mostrar según el tipo de dato."""
for record in self:
if record.parameter_value_type == 'numeric':
if record.value_numeric is not False:
record.value_display = f"{record.value_numeric} {record.parameter_unit or ''}"
else:
record.value_display = ''
elif record.parameter_value_type == 'text':
record.value_display = record.value_text or ''
elif record.parameter_value_type == 'selection':
record.value_display = record.value_selection or ''
elif record.parameter_value_type == 'boolean':
record.value_display = '' if record.value_boolean else 'No'
else:
record.value_display = ''
@api.depends('parameter_id', 'patient_id', 'test_date')
def _compute_applicable_range(self):
"""Determina el rango de referencia aplicable según el paciente."""
for record in self:
if not record.parameter_id or not record.patient_id:
record.applicable_range_id = False
continue
# Calcular edad del paciente en la fecha del test
if record.test_date:
age = record.patient_id.get_age_at_date(record.test_date.date())
else:
age = record.patient_id.age
# Buscar rango más específico
domain = [
('parameter_id', '=', record.parameter_id.id),
('age_min', '<=', age),
('age_max', '>=', age),
'|',
('gender', '=', record.patient_id.gender),
('gender', '=', 'both')
]
# Considerar embarazo si aplica
if record.patient_id.gender == 'female' and record.patient_id.is_pregnant:
domain.append(('pregnant', '=', True))
# Ordenar para obtener el más específico primero
ranges = self.env['lims.parameter.range'].search(
domain,
order='gender desc, pregnant desc',
limit=1
)
record.applicable_range_id = ranges[0] if ranges else False
@api.depends('value_numeric', 'applicable_range_id', 'parameter_value_type')
def _compute_is_out_of_range(self):
"""Determina si el valor está fuera del rango normal y si es crítico."""
for record in self:
record.is_out_of_range = False
record.is_critical = False
# Solo aplica para valores numéricos
if record.parameter_value_type != 'numeric' or record.value_numeric is False:
continue
if not record.applicable_range_id:
continue
range_obj = record.applicable_range_id
status = range_obj.get_value_status(record.value_numeric)
record.is_out_of_range = (status != 'normal')
record.is_critical = (status == 'critical')
@api.constrains('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
def _check_value_type(self):
"""Asegura que el valor ingresado corresponda al tipo de parámetro."""
for record in self:
if not record.parameter_id:
continue
value_type = record.parameter_value_type
has_value = False
if value_type == 'numeric':
has_value = record.value_numeric is not False
if record.value_text or record.value_selection:
raise ValidationError(
_('Para parámetros numéricos solo se debe ingresar el valor numérico.')
)
elif value_type == 'text':
has_value = bool(record.value_text)
if record.value_numeric is not False or record.value_selection or record.value_boolean:
raise ValidationError(
_('Para parámetros de texto solo se debe ingresar el valor de texto.')
)
elif value_type == 'selection':
has_value = bool(record.value_selection)
if record.value_numeric is not False or record.value_text or record.value_boolean:
raise ValidationError(
_('Para parámetros de selección solo se debe elegir una opción.')
)
elif value_type == 'boolean':
has_value = True # Boolean siempre tiene valor (True o False)
if record.value_numeric is not False or record.value_text or record.value_selection:
raise ValidationError(
_('Para parámetros Sí/No solo se debe marcar el checkbox.')
)
if not has_value and record.parameter_id:
raise ValidationError(
_('Debe ingresar un valor para el resultado del parámetro %s.') % record.parameter_name
)
@api.onchange('parameter_id')
def _onchange_parameter_id(self):
"""Limpia los valores cuando se cambia el parámetro."""
if self.parameter_id:
# Limpiar todos los valores
self.value_numeric = False
self.value_text = False
self.value_selection = False
self.value_boolean = False
# Si es selección, obtener las opciones
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
# Esto se usará en las vistas para mostrar las opciones dinámicamente
pass