diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ee793ed..8142a24 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] } diff --git a/issue_critical_selection.txt b/issue_critical_selection.txt new file mode 100644 index 0000000..632c3bd --- /dev/null +++ b/issue_critical_selection.txt @@ -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. \ No newline at end of file diff --git a/lims_management/models/lims_result.py b/lims_management/models/lims_result.py index dfaca77..c231295 100644 --- a/lims_management/models/lims_result.py +++ b/lims_management/models/lims_result.py @@ -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. diff --git a/lims_management/models/lims_test.py b/lims_management/models/lims_test.py index 98f9699..5cfba53 100644 --- a/lims_management/models/lims_test.py +++ b/lims_management/models/lims_test.py @@ -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""" diff --git a/test/create_lab_requests.py b/test/create_lab_requests.py index 707ef61..b956471 100644 --- a/test/create_lab_requests.py +++ b/test/create_lab_requests.py @@ -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': diff --git a/test/test_critical_notes_autocomplete.py b/test/test_critical_notes_autocomplete.py new file mode 100644 index 0000000..7c4891d --- /dev/null +++ b/test/test_critical_notes_autocomplete.py @@ -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) \ No newline at end of file diff --git a/test/verify_test_sequence.py b/test/verify_test_sequence.py new file mode 100644 index 0000000..6f3769e --- /dev/null +++ b/test/verify_test_sequence.py @@ -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) \ No newline at end of file