Merge pull request 'fix: Corregir generación de secuencias en lims.test' (#76) from feature/71-laboratory-dashboards into dev
Reviewed-on: #76
This commit is contained in:
commit
a1219640f1
|
@ -28,7 +28,8 @@
|
|||
"Bash(git cherry-pick:*)",
|
||||
"Bash(del comment_issue_15.txt)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(powershell.exe:*)"
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(gh pr create:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
|
104
issue_critical_selection.txt
Normal file
104
issue_critical_selection.txt
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Determinar automáticamente valores críticos/anormales para parámetros de selección múltiple
|
||||
|
||||
## Descripción
|
||||
|
||||
Actualmente, el sistema puede determinar automáticamente si un valor numérico es crítico basándose en rangos mínimos y máximos. Sin embargo, para parámetros de tipo selección (como Positivo/Negativo, Reactivo/No Reactivo), no existe una forma dinámica de determinar cuándo un valor es crítico o anormal.
|
||||
|
||||
## Problema actual
|
||||
|
||||
Los parámetros de selección múltiple no tienen forma de indicar qué valores son:
|
||||
- Normales
|
||||
- Anormales
|
||||
- Críticos
|
||||
|
||||
Ejemplos de parámetros afectados:
|
||||
- Prueba de embarazo: Positivo/Negativo
|
||||
- HIV: Reactivo/No Reactivo/Indeterminado
|
||||
- Hepatitis: Reactivo/No Reactivo
|
||||
- Otros marcadores infecciosos
|
||||
|
||||
## Solución propuesta
|
||||
|
||||
### Opción 1: Agregar campos al modelo `lims.analysis.parameter`
|
||||
|
||||
Agregar campos que permitan definir qué valores de selección son críticos:
|
||||
```python
|
||||
critical_values = fields.Text(
|
||||
string="Valores Críticos",
|
||||
help="Lista de valores separados por coma que se consideran críticos"
|
||||
)
|
||||
abnormal_values = fields.Text(
|
||||
string="Valores Anormales",
|
||||
help="Lista de valores separados por coma que se consideran anormales"
|
||||
)
|
||||
```
|
||||
|
||||
### Opción 2: Crear modelo relacionado `lims.parameter.selection.value`
|
||||
|
||||
Crear un modelo que defina cada opción de selección con sus propiedades:
|
||||
```python
|
||||
class LimsParameterSelectionValue(models.Model):
|
||||
_name = 'lims.parameter.selection.value'
|
||||
|
||||
parameter_id = fields.Many2one('lims.analysis.parameter')
|
||||
value = fields.Char(string="Valor")
|
||||
is_normal = fields.Boolean(string="Es Normal", default=True)
|
||||
is_critical = fields.Boolean(string="Es Crítico", default=False)
|
||||
sequence = fields.Integer(string="Secuencia")
|
||||
notes_template = fields.Text(string="Plantilla de Notas")
|
||||
```
|
||||
|
||||
### Opción 3: Usar configuración JSON
|
||||
|
||||
Almacenar la configuración en un campo JSON:
|
||||
```python
|
||||
selection_config = fields.Json(
|
||||
string="Configuración de Valores",
|
||||
help="Configuración de valores normales, anormales y críticos"
|
||||
)
|
||||
```
|
||||
|
||||
## Beneficios esperados
|
||||
|
||||
1. **Automatización completa**: El sistema podrá determinar automáticamente si cualquier tipo de resultado es crítico
|
||||
2. **Flexibilidad**: Cada laboratorio podrá configurar qué valores considera críticos según sus protocolos
|
||||
3. **Consistencia**: Aplicación uniforme de criterios en todos los resultados
|
||||
4. **Alertas mejoradas**: Mejor identificación de resultados que requieren atención inmediata
|
||||
|
||||
## Casos de uso
|
||||
|
||||
1. **Prueba de embarazo**:
|
||||
- Normal: Negativo (para pacientes no embarazadas)
|
||||
- Anormal: Positivo (puede requerir seguimiento)
|
||||
- Crítico: Indeterminado (requiere repetición)
|
||||
|
||||
2. **HIV**:
|
||||
- Normal: No Reactivo
|
||||
- Crítico: Reactivo, Indeterminado
|
||||
|
||||
3. **Marcadores tumorales**:
|
||||
- Normal: Negativo, No Detectado
|
||||
- Anormal: Débilmente Positivo
|
||||
- Crítico: Positivo, Fuertemente Positivo
|
||||
|
||||
## Consideraciones técnicas
|
||||
|
||||
- Mantener compatibilidad con el sistema actual
|
||||
- Permitir migración de datos existentes
|
||||
- Interfaz de usuario intuitiva para configuración
|
||||
- Integración con el autocompletado de notas críticas existente
|
||||
|
||||
## Tareas propuestas
|
||||
|
||||
1. Análisis de la mejor opción de implementación
|
||||
2. Diseño del modelo de datos
|
||||
3. Implementación de campos/modelos necesarios
|
||||
4. Actualización de la lógica de `is_critical` en `lims.result`
|
||||
5. Creación de interfaz de configuración
|
||||
6. Migración de parámetros existentes
|
||||
7. Pruebas exhaustivas
|
||||
8. Documentación
|
||||
|
||||
## Prioridad
|
||||
|
||||
Media-Alta: Esta mejora completaría la funcionalidad de detección automática de valores críticos para todos los tipos de parámetros.
|
|
@ -258,6 +258,10 @@ class LimsResult(models.Model):
|
|||
@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."""
|
||||
# Skip validation if we're in initialization context
|
||||
if self.env.context.get('skip_value_validation'):
|
||||
return
|
||||
|
||||
for record in self:
|
||||
if not record.parameter_id:
|
||||
continue
|
||||
|
@ -301,8 +305,8 @@ class LimsResult(models.Model):
|
|||
_('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':
|
||||
# Solo requerir valor si la prueba existe y no está en borrador
|
||||
if not has_value and record.parameter_id and record.test_id and record.test_id.state != 'draft':
|
||||
raise ValidationError(
|
||||
_('Debe ingresar un valor para el resultado del parámetro %s.') % record.parameter_name
|
||||
)
|
||||
|
@ -365,6 +369,101 @@ class LimsResult(models.Model):
|
|||
if len(matches) == 1:
|
||||
self.value_selection = matches[0]
|
||||
|
||||
@api.onchange('value_numeric', 'is_critical')
|
||||
def _onchange_critical_value(self):
|
||||
"""Autocompleta las notas cuando el valor es crítico."""
|
||||
if self.is_critical and self.parameter_value_type == 'numeric' and self.value_numeric:
|
||||
# Diccionario de notas médicas para parámetros críticos
|
||||
CRITICAL_NOTES = {
|
||||
'glucosa': {
|
||||
'high': 'Valor elevado de glucosa. Posible prediabetes o diabetes. Se recomienda repetir la prueba en ayunas y consultar con endocrinología.',
|
||||
'low': 'Hipoglucemia detectada. Riesgo de síntomas neuroglucogénicos. Evaluar causas: medicamentos, insuficiencia hepática o endocrinopatías.'
|
||||
},
|
||||
'hemoglobina': {
|
||||
'high': 'Policitemia. Evaluar posibles causas: deshidratación, tabaquismo, cardiopatía o policitemia vera.',
|
||||
'low': 'Anemia severa. Investigar origen: deficiencia de hierro, pérdida sanguínea, hemólisis o enfermedad crónica.'
|
||||
},
|
||||
'hematocrito': {
|
||||
'high': 'Hemoconcentración. Correlacionar con hemoglobina. Descartar deshidratación o policitemia.',
|
||||
'low': 'Valor compatible con anemia. Evaluar junto con hemoglobina e índices eritrocitarios.'
|
||||
},
|
||||
'leucocitos': {
|
||||
'high': 'Leucocitosis marcada. Descartar proceso infeccioso, inflamatorio o hematológico.',
|
||||
'low': 'Leucopenia severa. Riesgo de infecciones. Evaluar causas: viral, medicamentosa o hematológica.'
|
||||
},
|
||||
'plaquetas': {
|
||||
'high': 'Trombocitosis. Riesgo trombótico. Descartar causa primaria vs reactiva.',
|
||||
'low': 'Trombocitopenia severa. Riesgo de sangrado. Evaluar PTI, hiperesplenismo o supresión medular.'
|
||||
},
|
||||
'neutrofilos': {
|
||||
'high': 'Neutrofilia. Sugiere infección bacteriana o proceso inflamatorio agudo.',
|
||||
'low': 'Neutropenia. Alto riesgo de infección bacteriana. Evaluar urgentemente.'
|
||||
},
|
||||
'linfocitos': {
|
||||
'high': 'Linfocitosis. Considerar infección viral o proceso linfoproliferativo.',
|
||||
'low': 'Linfopenia. Evaluar inmunodeficiencia o efecto de corticoides.'
|
||||
},
|
||||
'colesterol total': {
|
||||
'high': 'Hipercolesterolemia. Riesgo cardiovascular elevado. Iniciar medidas dietéticas y evaluar tratamiento con estatinas.',
|
||||
'low': 'Hipocolesterolemia. Evaluar malnutrición, hipertiroidismo o enfermedad hepática.'
|
||||
},
|
||||
'trigliceridos': {
|
||||
'high': 'Hipertrigliceridemia severa. Riesgo de pancreatitis aguda. Considerar tratamiento farmacológico urgente.',
|
||||
'low': 'Valor bajo, generalmente sin significado patológico.'
|
||||
},
|
||||
'hdl': {
|
||||
'high': 'HDL elevado, factor protector cardiovascular.',
|
||||
'low': 'HDL bajo. Factor de riesgo cardiovascular. Recomendar ejercicio y cambios en estilo de vida.'
|
||||
},
|
||||
'ldl': {
|
||||
'high': 'LDL elevado. Alto riesgo aterogénico. Evaluar inicio de estatinas según riesgo global.',
|
||||
'low': 'LDL bajo, generalmente favorable.'
|
||||
},
|
||||
'glucosa en sangre': {
|
||||
'high': 'Hiperglucemia. Si en ayunas >126 mg/dL sugiere diabetes. Confirmar con segunda muestra.',
|
||||
'low': 'Hipoglucemia. Evaluar síntomas y causas. Riesgo neurológico si <50 mg/dL.'
|
||||
}
|
||||
}
|
||||
|
||||
# Solo autocompletar si no hay notas previas o están vacías
|
||||
if not self.notes or self.notes.strip() == '':
|
||||
note = self._get_critical_note(CRITICAL_NOTES)
|
||||
if note:
|
||||
self.notes = note
|
||||
|
||||
def _get_critical_note(self, critical_notes_dict):
|
||||
"""Obtiene la nota apropiada para un resultado crítico."""
|
||||
if not self.parameter_id or not self.parameter_name:
|
||||
return False
|
||||
|
||||
param_lower = self.parameter_name.lower()
|
||||
|
||||
# Buscar el parámetro en el diccionario
|
||||
for key in critical_notes_dict:
|
||||
if key in param_lower:
|
||||
# Obtener rangos del rango aplicable si existe
|
||||
normal_min = normal_max = None
|
||||
if self.applicable_range_id:
|
||||
normal_min = self.applicable_range_id.normal_min
|
||||
normal_max = self.applicable_range_id.normal_max
|
||||
|
||||
if normal_max and self.value_numeric > normal_max:
|
||||
return critical_notes_dict[key].get('high', f'Valor crítico alto para {self.parameter_name}. Requiere evaluación médica inmediata.')
|
||||
elif normal_min and self.value_numeric < normal_min:
|
||||
return critical_notes_dict[key].get('low', f'Valor crítico bajo para {self.parameter_name}. Requiere evaluación médica inmediata.')
|
||||
|
||||
# Nota genérica si no se encuentra el parámetro
|
||||
if self.applicable_range_id:
|
||||
normal_min = self.applicable_range_id.normal_min
|
||||
normal_max = self.applicable_range_id.normal_max
|
||||
|
||||
if normal_max and self.value_numeric > normal_max:
|
||||
return f'Valor significativamente elevado. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
|
||||
elif normal_min and self.value_numeric < normal_min:
|
||||
return f'Valor significativamente bajo. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
|
||||
|
||||
return 'Valor fuera de rango normal. Requiere interpretación clínica.'
|
||||
|
||||
def _validate_and_autocomplete_selection(self, value):
|
||||
"""Valida y autocompleta el valor de selección.
|
||||
|
||||
|
|
|
@ -169,17 +169,6 @@ class LimsTest(models.Model):
|
|||
}
|
||||
}
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Genera código único al crear."""
|
||||
for vals in vals_list:
|
||||
if vals.get('name', 'Nuevo') == 'Nuevo':
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
|
||||
|
||||
tests = super().create(vals_list)
|
||||
# Generar resultados automáticamente
|
||||
tests._generate_test_results()
|
||||
return tests
|
||||
|
||||
def _generate_test_results(self):
|
||||
"""Genera automáticamente las líneas de resultado basadas en los parámetros configurados del análisis."""
|
||||
|
@ -505,13 +494,20 @@ class LimsTest(models.Model):
|
|||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""Override create para validaciones adicionales"""
|
||||
"""Override create para validaciones adicionales y generación de secuencia"""
|
||||
# Generar código único si no se proporciona
|
||||
if vals.get('name', 'Nuevo') == 'Nuevo':
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
|
||||
|
||||
# Si se está creando con un estado diferente a draft, verificar permisos
|
||||
if vals.get('state') and vals['state'] != 'draft':
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('Solo administradores pueden crear pruebas en estado diferente a borrador'))
|
||||
|
||||
return super().create(vals)
|
||||
test = super().create(vals)
|
||||
# Generar resultados automáticamente
|
||||
test._generate_test_results()
|
||||
return test
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para auditoría adicional"""
|
||||
|
|
|
@ -190,8 +190,8 @@ def process_order_tests(env, order):
|
|||
|
||||
# Evaluar resultados críticos y agregar notas
|
||||
for result in test.result_ids:
|
||||
# Leer el registro para actualizar campos computados
|
||||
result.read(['is_critical'])
|
||||
# Leer el registro para actualizar campos computados con contexto especial
|
||||
result.with_context(skip_value_validation=True).read(['is_critical'])
|
||||
|
||||
# Si el resultado es crítico, agregar nota
|
||||
if result.is_critical and result.parameter_id.value_type == 'numeric':
|
||||
|
|
85
test/test_critical_notes_autocomplete.py
Normal file
85
test/test_critical_notes_autocomplete.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
import odoo
|
||||
import json
|
||||
|
||||
def test_critical_notes_autocomplete(cr):
|
||||
"""Prueba el autocompletado de notas críticas en resultados de laboratorio"""
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
print("\n=== PRUEBA DE AUTOCOMPLETADO DE NOTAS CRÍTICAS ===\n")
|
||||
|
||||
# Buscar algunas pruebas con resultados
|
||||
tests = env['lims.test'].search([('state', 'in', ['result_entered', 'validated'])], limit=5)
|
||||
|
||||
if not tests:
|
||||
print("No se encontraron pruebas con resultados para probar.")
|
||||
return
|
||||
|
||||
for test in tests:
|
||||
print(f"\nPrueba: {test.name} - {test.product_id.name}")
|
||||
print(f"Paciente: {test.patient_id.name}")
|
||||
|
||||
for result in test.result_ids:
|
||||
if result.parameter_value_type == 'numeric':
|
||||
print(f"\n Parámetro: {result.parameter_name}")
|
||||
print(f" Valor: {result.value_numeric} {result.parameter_unit or ''}")
|
||||
print(f" ¿Es crítico?: {'SÍ' if result.is_critical else 'NO'}")
|
||||
|
||||
if result.is_critical:
|
||||
# Limpiar las notas para probar el autocompletado
|
||||
result.notes = ''
|
||||
|
||||
# Simular cambio en el valor para activar el onchange
|
||||
with env.cr.savepoint():
|
||||
# Trigger the onchange by updating the value
|
||||
result.with_context(force_onchange=True)._onchange_critical_value()
|
||||
|
||||
print(f" Nota autocompletada: {result.notes}")
|
||||
|
||||
# No guardar los cambios, solo mostrar
|
||||
env.cr.rollback()
|
||||
|
||||
# Probar con valores específicos
|
||||
print("\n\n=== PRUEBA CON VALORES ESPECÍFICOS ===\n")
|
||||
|
||||
# Buscar parámetros específicos
|
||||
test_params = [
|
||||
('Glucosa', 200.0, 'high'),
|
||||
('Glucosa', 50.0, 'low'),
|
||||
('Hemoglobina', 20.0, 'high'),
|
||||
('Hemoglobina', 7.0, 'low'),
|
||||
('Plaquetas', 600000, 'high'),
|
||||
('Plaquetas', 50000, 'low')
|
||||
]
|
||||
|
||||
for param_name, test_value, expected_type in test_params:
|
||||
# Buscar un resultado con este parámetro
|
||||
result = env['lims.result'].search([
|
||||
('parameter_name', 'ilike', param_name),
|
||||
('parameter_value_type', '=', 'numeric')
|
||||
], limit=1)
|
||||
|
||||
if result:
|
||||
print(f"\nProbando {param_name} con valor {test_value} (esperado: {expected_type})")
|
||||
|
||||
with env.cr.savepoint():
|
||||
# Establecer el valor de prueba
|
||||
result.value_numeric = test_value
|
||||
result.notes = ''
|
||||
|
||||
# Forzar recálculo de is_critical
|
||||
result._compute_is_out_of_range()
|
||||
|
||||
# Trigger el onchange
|
||||
result._onchange_critical_value()
|
||||
|
||||
print(f" ¿Es crítico?: {'SÍ' if result.is_critical else 'NO'}")
|
||||
print(f" Nota generada: {result.notes[:100]}...")
|
||||
|
||||
# No guardar
|
||||
env.cr.rollback()
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
test_critical_notes_autocomplete(cr)
|
64
test/verify_test_sequence.py
Normal file
64
test/verify_test_sequence.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import odoo
|
||||
|
||||
def verify_test_sequence(cr):
|
||||
"""Verificar que los tests están usando la secuencia correcta"""
|
||||
print("\n=== VERIFICACIÓN DE SECUENCIAS EN LIMS.TEST ===\n")
|
||||
|
||||
# Buscar todos los tests
|
||||
cr.execute("""
|
||||
SELECT id, name, create_date
|
||||
FROM lims_test
|
||||
ORDER BY create_date
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
tests = cr.fetchall()
|
||||
|
||||
print(f"Total de tests encontrados (mostrando primeros 10): {len(tests)}")
|
||||
print("-" * 50)
|
||||
print("ID | Código | Fecha de Creación")
|
||||
print("-" * 50)
|
||||
|
||||
for test in tests:
|
||||
print(f"{test[0]:<4} | {test[1]:<15} | {test[2]}")
|
||||
|
||||
# Verificar si hay algún test con nombre "Nuevo"
|
||||
cr.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM lims_test
|
||||
WHERE name = 'Nuevo'
|
||||
""")
|
||||
|
||||
nuevo_count = cr.fetchone()[0]
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"\nTests con nombre 'Nuevo': {nuevo_count}")
|
||||
|
||||
if nuevo_count == 0:
|
||||
print("✅ ÉXITO: Todos los tests están usando la secuencia correcta")
|
||||
else:
|
||||
print("❌ ERROR: Hay tests con nombre 'Nuevo'")
|
||||
|
||||
# Verificar el patrón de la secuencia
|
||||
cr.execute("""
|
||||
SELECT name
|
||||
FROM lims_test
|
||||
WHERE name LIKE 'LAB-%'
|
||||
ORDER BY create_date DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
recent_tests = cr.fetchall()
|
||||
|
||||
print("\nÚltimos 5 tests con secuencia LAB-:")
|
||||
for test in recent_tests:
|
||||
print(f" - {test[0]}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
verify_test_sequence(cr)
|
Loading…
Reference in New Issue
Block a user