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>
This commit is contained in:
parent
99990bdfcc
commit
5bee8e79df
|
@ -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.')
|
||||
)
|
||||
# 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
|
|
@ -58,18 +58,21 @@
|
|||
context="{'default_test_id': id}">
|
||||
<list string="Resultados" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="parameter_id" options="{'no_create': True}"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
<field name="value_numeric"
|
||||
invisible="value_text or value_selection"
|
||||
decoration-danger="is_out_of_range"/>
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
decoration-danger="is_out_of_range"
|
||||
decoration-warning="is_critical"/>
|
||||
<field name="value_text"
|
||||
invisible="value_numeric or value_selection"/>
|
||||
invisible="parameter_value_type != 'text'"/>
|
||||
<field name="value_selection"
|
||||
invisible="value_numeric or value_text"/>
|
||||
<field name="unit" optional="show"/>
|
||||
<field name="normal_min" optional="hide"/>
|
||||
<field name="normal_max" optional="hide"/>
|
||||
invisible="parameter_value_type != 'selection'"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"/>
|
||||
<field name="parameter_unit" optional="show"/>
|
||||
<field name="is_out_of_range" invisible="1"/>
|
||||
<field name="is_critical" invisible="1"/>
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
|
|
Loading…
Reference in New Issue
Block a user