# -*- 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' ) # Campo relacionado para acceder a la muestra sin duplicar datos test_sample_id = fields.Many2one( 'stock.lot', string='Muestra', related='test_id.sample_id', readonly=True, store=True # Para poder buscar y filtrar ) # Campo relacionado para mostrar el estado sin duplicar test_sample_state = fields.Selection( string='Estado de Muestra', related='test_sample_id.state', readonly=True ) # 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 ) parameter_code = fields.Char( string='Código', related='parameter_id.code', 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 ) result_status = fields.Selection([ ('normal', 'Normal'), ('abnormal', 'Anormal'), ('critical', 'Crítico') ], string='Estado', compute='_compute_result_status', 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 = 'Sí' 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.depends('parameter_id', 'value_numeric', 'is_out_of_range', 'is_critical', 'parameter_value_type') def _compute_result_status(self): """Calcula el estado visual del resultado.""" for record in self: if record.parameter_value_type != 'numeric': record.result_status = 'normal' elif record.is_critical: record.result_status = 'critical' elif record.is_out_of_range: record.result_status = 'abnormal' else: record.result_status = 'normal' @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 not in [False, 0.0] 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 not in [False, 0.0]) 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 not in [False, 0.0]) 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 not in [False, 0.0]) or record.value_text or record.value_selection: raise ValidationError( _('Para parámetros Sí/No solo se debe marcar el checkbox.') ) # Solo requerir valor si la prueba no está en borrador if not has_value and record.parameter_id and record.test_id.state != 'draft': 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