From 5bee8e79dfad8af593b07a5831d7f3a42dbf3f3e Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 12:33:20 -0600 Subject: [PATCH] feat(#51): Task 5 completada - Modificar modelo lims.result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lims_management/models/lims_result.py | 227 +++++++++++++++++----- lims_management/views/lims_test_views.xml | 19 +- 2 files changed, 193 insertions(+), 53 deletions(-) diff --git a/lims_management/models/lims_result.py b/lims_management/models/lims_result.py index a123c25..d6b3e8b 100644 --- a/lims_management/models/lims_result.py +++ b/lims_management/models/lims_result.py @@ -25,10 +25,20 @@ class LimsResult(models.Model): ondelete='cascade' ) - # Por ahora, estos campos básicos - parameter_name = fields.Char( + # Cambio de parameter_name a parameter_id + parameter_id = fields.Many2one( + 'lims.analysis.parameter', string='Parámetro', - required=True + 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( @@ -36,12 +46,21 @@ class LimsResult(models.Model): default=10 ) - # TODO: Implementar parameter_id cuando exista lims.test.parameter - # parameter_id = fields.Many2one( - # 'lims.test.parameter', - # string='Parámetro' - # ) + # 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' ) @@ -50,32 +69,56 @@ class LimsResult(models.Model): string='Valor de Texto' ) - value_selection = fields.Selection( - [], # Por ahora vacío + 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' ) - # Campos para rangos normales (temporal) - normal_min = fields.Float( - string='Valor Normal Mínimo' + # Información del paciente (para cálculo de rangos) + patient_id = fields.Many2one( + related='test_id.patient_id', + string='Paciente', + store=True ) - normal_max = fields.Float( - string='Valor Normal Máximo' - ) - - unit = fields.Char( - string='Unidad' + test_date = fields.Datetime( + related='test_id.create_date', + string='Fecha de la Prueba', + store=True ) @api.depends('test_id', 'parameter_name') @@ -87,38 +130,132 @@ class LimsResult(models.Model): else: record.display_name = record.parameter_name or _('Nuevo') - @api.depends('value_numeric', 'normal_min', 'normal_max') - def _compute_is_out_of_range(self): - """Determina si el valor está fuera del rango normal.""" + @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.value_numeric and (record.normal_min or record.normal_max): - if record.normal_min and record.value_numeric < record.normal_min: - record.is_out_of_range = True - elif record.normal_max and record.value_numeric > record.normal_max: - record.is_out_of_range = True + 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.is_out_of_range = False + 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 = 'Sí' if record.value_boolean else 'No' else: - record.is_out_of_range = False + record.value_display = '' - @api.constrains('value_numeric', 'value_text', 'value_selection') - def _check_single_value_type(self): - """Asegura que solo un tipo de valor esté lleno.""" + @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: - filled_values = 0 - if record.value_numeric: - filled_values += 1 - if record.value_text: - filled_values += 1 - if record.value_selection: - filled_values += 1 + if not record.parameter_id or not record.patient_id: + record.applicable_range_id = False + continue - if filled_values > 1: + # 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( - _('Solo se puede ingresar un tipo de valor (numérico, texto o selección) por resultado.') + _('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 - if filled_values == 0: - raise ValidationError( - _('Debe ingresar al menos un valor para el resultado.') - ) \ No newline at end of file + # 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 \ No newline at end of file diff --git a/lims_management/views/lims_test_views.xml b/lims_management/views/lims_test_views.xml index 4588e15..2b7c5f8 100644 --- a/lims_management/views/lims_test_views.xml +++ b/lims_management/views/lims_test_views.xml @@ -58,18 +58,21 @@ context="{'default_test_id': id}"> - + + + invisible="parameter_value_type != 'numeric'" + decoration-danger="is_out_of_range" + decoration-warning="is_critical"/> + invisible="parameter_value_type != 'text'"/> - - - + invisible="parameter_value_type != 'selection'"/> + + +