From aaa12044909dbbb8c749ef46a606e9c25a974dfe Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 14:08:33 -0600 Subject: [PATCH] =?UTF-8?q?feat(#51):=20Task=2012=20completada=20-=20Tests?= =?UTF-8?q?=20automatizados=20para=20cat=C3=A1logo=20de=20par=C3=A1metros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Creados 4 archivos de test completos con cobertura total - test_analysis_parameter.py: Tests del modelo de parámetros - test_parameter_range.py: Tests de rangos de referencia - test_result_parameter_integration.py: Tests de integración - test_auto_result_generation.py: Tests de generación automática - Creado script simplificado test_parameters_simple.py para ejecución rápida - Corregido valor por defecto de age_max a 150 en parameter_range.py - Documentación completa en README.md --- lims_management/models/parameter_range.py | 4 +- lims_management/tests/README.md | 80 +++++ lims_management/tests/__init__.py | 5 + .../tests/test_analysis_parameter.py | 175 +++++++++++ .../tests/test_auto_result_generation.py | 283 +++++++++++++++++ lims_management/tests/test_parameter_range.py | 249 +++++++++++++++ .../test_result_parameter_integration.py | 291 ++++++++++++++++++ test/run_parameter_tests.py | 186 +++++++++++ test/test_parameters_simple.py | 221 +++++++++++++ 9 files changed, 1492 insertions(+), 2 deletions(-) create mode 100644 lims_management/tests/README.md create mode 100644 lims_management/tests/__init__.py create mode 100644 lims_management/tests/test_analysis_parameter.py create mode 100644 lims_management/tests/test_auto_result_generation.py create mode 100644 lims_management/tests/test_parameter_range.py create mode 100644 lims_management/tests/test_result_parameter_integration.py create mode 100644 test/run_parameter_tests.py create mode 100644 test/test_parameters_simple.py diff --git a/lims_management/models/parameter_range.py b/lims_management/models/parameter_range.py index 9b6803f..097a449 100644 --- a/lims_management/models/parameter_range.py +++ b/lims_management/models/parameter_range.py @@ -44,7 +44,7 @@ class LimsParameterRange(models.Model): age_max = fields.Integer( string='Edad Máxima', - default=999, + default=150, help='Edad máxima en años (inclusive)' ) @@ -117,7 +117,7 @@ class LimsParameterRange(models.Model): parts.append(gender_name) # Agregar rango de edad - if record.age_min == 0 and record.age_max == 999: + if record.age_min == 0 and record.age_max == 150: parts.append('Todas las edades') else: parts.append(f"{record.age_min}-{record.age_max} años") diff --git a/lims_management/tests/README.md b/lims_management/tests/README.md new file mode 100644 index 0000000..c4d4048 --- /dev/null +++ b/lims_management/tests/README.md @@ -0,0 +1,80 @@ +# Tests del Módulo LIMS + +Este directorio contiene los tests automatizados para el módulo `lims_management`, específicamente para el sistema de catálogo de parámetros. + +## Estructura de Tests + +### 1. test_analysis_parameter.py +Tests para el modelo `lims.analysis.parameter`: +- Creación de parámetros con diferentes tipos de valores +- Validaciones de campos requeridos +- Prevención de códigos duplicados +- Relaciones con rangos y análisis + +### 2. test_parameter_range.py +Tests para el modelo `lims.parameter.range`: +- Creación de rangos de referencia +- Validaciones de valores mínimos y máximos +- Rangos específicos por género y edad +- Búsqueda de rangos aplicables según características del paciente + +### 3. test_result_parameter_integration.py +Tests de integración entre resultados y parámetros: +- Asignación de parámetros a resultados +- Selección automática de rangos aplicables +- Detección de valores fuera de rango y críticos +- Formato de visualización de resultados + +### 4. test_auto_result_generation.py +Tests para la generación automática de resultados: +- Creación automática al generar pruebas +- Herencia de secuencia desde la configuración +- Rendimiento en creación masiva + +## Ejecución de Tests + +### Usando Odoo Test Framework +```bash +# Desde el servidor Odoo +python3 -m odoo.cli.server -d lims_demo --test-enable --test-tags lims_management +``` + +### Usando el Script Simplificado +```bash +# Copiar script al contenedor +docker cp test/test_parameters_simple.py lims_odoo:/tmp/ + +# Ejecutar tests +docker-compose exec odoo python3 /tmp/test_parameters_simple.py +``` + +## Cobertura de Tests + +Los tests cubren: + +1. **Validaciones del Modelo** + - Campos requeridos según tipo de parámetro + - Restricciones de unicidad + - Validaciones de rangos + +2. **Lógica de Negocio** + - Generación automática de resultados + - Búsqueda de rangos aplicables + - Cálculo de estados (fuera de rango, crítico) + +3. **Integración** + - Flujo completo desde orden hasta resultados + - Compatibilidad con el sistema existente + +## Datos de Prueba + +Los tests utilizan: +- Parámetros de demostración del archivo `parameter_demo.xml` +- Rangos de referencia de `parameter_range_demo.xml` +- Análisis configurados en `analysis_parameter_config_demo.xml` + +## Notas Importantes + +- Los tests se ejecutan en transacciones que se revierten automáticamente +- No afectan los datos de producción o demostración +- Requieren que el módulo esté instalado con datos demo \ No newline at end of file diff --git a/lims_management/tests/__init__.py b/lims_management/tests/__init__.py new file mode 100644 index 0000000..0a66197 --- /dev/null +++ b/lims_management/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import test_analysis_parameter +from . import test_parameter_range +from . import test_result_parameter_integration +from . import test_auto_result_generation \ No newline at end of file diff --git a/lims_management/tests/test_analysis_parameter.py b/lims_management/tests/test_analysis_parameter.py new file mode 100644 index 0000000..18bd63f --- /dev/null +++ b/lims_management/tests/test_analysis_parameter.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +""" +Tests para el modelo lims.analysis.parameter +""" +from odoo.tests import TransactionCase +from odoo.exceptions import ValidationError + + +class TestAnalysisParameter(TransactionCase): + """Tests para el catálogo de parámetros de análisis""" + + def setUp(self): + super().setUp() + self.Parameter = self.env['lims.analysis.parameter'] + + def test_create_numeric_parameter(self): + """Test crear parámetro numérico con validaciones""" + # Crear parámetro numérico válido + param = self.Parameter.create({ + 'code': 'TEST001', + 'name': 'Test Parameter', + 'value_type': 'numeric', + 'unit': 'mg/dL', + 'description': 'Test numeric parameter' + }) + + self.assertEqual(param.code, 'TEST001') + self.assertEqual(param.value_type, 'numeric') + self.assertEqual(param.unit, 'mg/dL') + + def test_numeric_parameter_requires_unit(self): + """Test que parámetros numéricos requieren unidad""" + with self.assertRaises(ValidationError) as e: + self.Parameter.create({ + 'code': 'TEST002', + 'name': 'Test Parameter No Unit', + 'value_type': 'numeric', + # Sin unit - debe fallar + }) + self.assertIn('unidad de medida', str(e.exception)) + + def test_create_selection_parameter(self): + """Test crear parámetro de selección con opciones""" + param = self.Parameter.create({ + 'code': 'TEST003', + 'name': 'Test Selection', + 'value_type': 'selection', + 'selection_values': 'Positivo,Negativo,Indeterminado' + }) + + self.assertEqual(param.value_type, 'selection') + self.assertEqual(param.selection_values, 'Positivo,Negativo,Indeterminado') + + def test_selection_parameter_requires_values(self): + """Test que parámetros de selección requieren valores""" + with self.assertRaises(ValidationError) as e: + self.Parameter.create({ + 'code': 'TEST004', + 'name': 'Test Selection No Values', + 'value_type': 'selection', + # Sin selection_values - debe fallar + }) + self.assertIn('valores de selección', str(e.exception)) + + def test_duplicate_code_not_allowed(self): + """Test que no se permiten códigos duplicados""" + # Crear primer parámetro + self.Parameter.create({ + 'code': 'DUP001', + 'name': 'Original Parameter', + 'value_type': 'text' + }) + + # Intentar crear duplicado + with self.assertRaises(ValidationError) as e: + self.Parameter.create({ + 'code': 'DUP001', + 'name': 'Duplicate Parameter', + 'value_type': 'text' + }) + self.assertIn('ya existe', str(e.exception)) + + def test_boolean_parameter(self): + """Test crear parámetro booleano""" + param = self.Parameter.create({ + 'code': 'BOOL001', + 'name': 'Test Boolean', + 'value_type': 'boolean', + 'description': 'Boolean parameter' + }) + + self.assertEqual(param.value_type, 'boolean') + self.assertFalse(param.unit) # Boolean no debe tener unidad + + def test_text_parameter(self): + """Test crear parámetro de texto""" + param = self.Parameter.create({ + 'code': 'TEXT001', + 'name': 'Test Text', + 'value_type': 'text', + 'description': 'Text parameter' + }) + + self.assertEqual(param.value_type, 'text') + self.assertFalse(param.unit) # Text no debe tener unidad + self.assertFalse(param.selection_values) # Text no debe tener valores de selección + + def test_parameter_name_display(self): + """Test nombre mostrado del parámetro""" + # Con unidad + param1 = self.Parameter.create({ + 'code': 'DISP001', + 'name': 'Glucosa', + 'value_type': 'numeric', + 'unit': 'mg/dL' + }) + self.assertEqual(param1.display_name, 'Glucosa (mg/dL)') + + # Sin unidad + param2 = self.Parameter.create({ + 'code': 'DISP002', + 'name': 'Cultivo', + 'value_type': 'text' + }) + self.assertEqual(param2.display_name, 'Cultivo') + + def test_parameter_ranges_relationship(self): + """Test relación con rangos de referencia""" + param = self.Parameter.create({ + 'code': 'RANGE001', + 'name': 'Test with Ranges', + 'value_type': 'numeric', + 'unit': 'U/L' + }) + + # Crear rango para este parámetro + range1 = self.env['lims.parameter.range'].create({ + 'parameter_id': param.id, + 'name': 'Adult Male', + 'gender': 'male', + 'age_min': 18, + 'age_max': 65, + 'normal_min': 10.0, + 'normal_max': 50.0 + }) + + self.assertEqual(len(param.range_ids), 1) + self.assertEqual(param.range_ids[0], range1) + + def test_parameter_analysis_relationship(self): + """Test relación con análisis a través de product.template.parameter""" + param = self.Parameter.create({ + 'code': 'ANAL001', + 'name': 'Test Analysis Link', + 'value_type': 'numeric', + 'unit': 'mmol/L' + }) + + # Crear producto análisis + analysis = self.env['product.template'].create({ + 'name': 'Test Analysis', + 'type': 'service', + 'is_analysis': True, + 'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id, + }) + + # Crear configuración parámetro-análisis + config = self.env['product.template.parameter'].create({ + 'product_tmpl_id': analysis.id, + 'parameter_id': param.id, + 'sequence': 10 + }) + + self.assertEqual(len(param.analysis_config_ids), 1) + self.assertEqual(param.analysis_config_ids[0], config) \ No newline at end of file diff --git a/lims_management/tests/test_auto_result_generation.py b/lims_management/tests/test_auto_result_generation.py new file mode 100644 index 0000000..d0ae8ed --- /dev/null +++ b/lims_management/tests/test_auto_result_generation.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +""" +Tests para la generación automática de resultados basada en parámetros +""" +from odoo.tests import TransactionCase +from datetime import date + + +class TestAutoResultGeneration(TransactionCase): + """Tests para la generación automática de resultados al crear pruebas""" + + def setUp(self): + super().setUp() + + # Modelos + self.Test = self.env['lims.test'] + self.Sample = self.env['stock.lot'] + self.Order = self.env['sale.order'] + self.Parameter = self.env['lims.analysis.parameter'] + self.TemplateParam = self.env['product.template.parameter'] + self.Product = self.env['product.template'] + self.Partner = self.env['res.partner'] + + # Crear paciente + self.patient = self.Partner.create({ + 'name': 'Patient for Auto Generation', + 'is_patient': True, + 'gender': 'male', + 'birth_date': date(1985, 3, 15) + }) + + # Crear doctor + self.doctor = self.Partner.create({ + 'name': 'Dr. Test', + 'is_doctor': True + }) + + # Crear parámetros + self.param1 = self.Parameter.create({ + 'code': 'AUTO1', + 'name': 'Parameter Auto 1', + 'value_type': 'numeric', + 'unit': 'mg/dL' + }) + + self.param2 = self.Parameter.create({ + 'code': 'AUTO2', + 'name': 'Parameter Auto 2', + 'value_type': 'selection', + 'selection_values': 'Normal,Anormal' + }) + + self.param3 = self.Parameter.create({ + 'code': 'AUTO3', + 'name': 'Parameter Auto 3', + 'value_type': 'text' + }) + + # Crear análisis con parámetros configurados + self.analysis_multi = self.Product.create({ + 'name': 'Multi-Parameter Analysis', + 'type': 'service', + 'is_analysis': True, + 'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id, + 'sample_type_id': self.env.ref('lims_management.sample_type_blood').id, + }) + + # Configurar parámetros en el análisis + self.TemplateParam.create({ + 'product_tmpl_id': self.analysis_multi.id, + 'parameter_id': self.param1.id, + 'sequence': 10 + }) + + self.TemplateParam.create({ + 'product_tmpl_id': self.analysis_multi.id, + 'parameter_id': self.param2.id, + 'sequence': 20 + }) + + self.TemplateParam.create({ + 'product_tmpl_id': self.analysis_multi.id, + 'parameter_id': self.param3.id, + 'sequence': 30 + }) + + # Crear análisis sin parámetros + self.analysis_empty = self.Product.create({ + 'name': 'Empty Analysis', + 'type': 'service', + 'is_analysis': True, + 'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id, + }) + + def test_auto_generate_results_on_test_creation(self): + """Test generación automática de resultados al crear una prueba""" + # Crear orden y muestra + order = self.Order.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis_multi.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + order.action_confirm() + + # Generar muestra + order.action_generate_samples() + sample = order.lab_sample_ids[0] + + # La prueba debe haberse creado automáticamente con los resultados + self.assertEqual(len(sample.test_ids), 1) + test = sample.test_ids[0] + + # Verificar que se generaron todos los resultados + self.assertEqual(len(test.result_ids), 3) + + # Verificar que cada resultado tiene el parámetro correcto + param_ids = test.result_ids.mapped('parameter_id') + self.assertIn(self.param1, param_ids) + self.assertIn(self.param2, param_ids) + self.assertIn(self.param3, param_ids) + + # Verificar orden de secuencia + results_sorted = test.result_ids.sorted('sequence') + self.assertEqual(results_sorted[0].parameter_id, self.param1) + self.assertEqual(results_sorted[1].parameter_id, self.param2) + self.assertEqual(results_sorted[2].parameter_id, self.param3) + + def test_no_results_for_analysis_without_parameters(self): + """Test que no se generan resultados para análisis sin parámetros""" + # Crear orden con análisis sin parámetros + order = self.Order.create({ + 'partner_id': self.patient.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis_empty.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + order.action_confirm() + order.action_generate_samples() + + sample = order.lab_sample_ids[0] + test = sample.test_ids[0] + + # No debe haber resultados + self.assertEqual(len(test.result_ids), 0) + + def test_manual_test_creation_generates_results(self): + """Test generación de resultados al crear prueba manualmente""" + # Crear muestra manual + sample = self.Sample.create({ + 'name': 'SAMPLE-MANUAL-001', + 'is_lab_sample': True, + 'patient_id': self.patient.id, + 'sample_state': 'collected' + }) + + # Crear prueba manualmente + test = self.Test.create({ + 'sample_id': sample.id, + 'patient_id': self.patient.id, + 'product_id': self.analysis_multi.product_variant_id.id, + 'state': 'draft' + }) + + # Verificar generación automática + self.assertEqual(len(test.result_ids), 3) + + def test_results_inherit_correct_sequence(self): + """Test que los resultados heredan la secuencia correcta""" + # Crear análisis con secuencias específicas + analysis = self.Product.create({ + 'name': 'Sequence Test Analysis', + 'type': 'service', + 'is_analysis': True, + 'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id, + }) + + # Configurar con secuencias no consecutivas + self.TemplateParam.create({ + 'product_tmpl_id': analysis.id, + 'parameter_id': self.param1.id, + 'sequence': 100 + }) + + self.TemplateParam.create({ + 'product_tmpl_id': analysis.id, + 'parameter_id': self.param2.id, + 'sequence': 50 + }) + + self.TemplateParam.create({ + 'product_tmpl_id': analysis.id, + 'parameter_id': self.param3.id, + 'sequence': 75 + }) + + # Crear prueba + test = self.Test.create({ + 'patient_id': self.patient.id, + 'product_id': analysis.product_variant_id.id, + 'state': 'draft' + }) + + # Verificar orden: param2 (50), param3 (75), param1 (100) + results_sorted = test.result_ids.sorted('sequence') + self.assertEqual(results_sorted[0].parameter_id, self.param2) + self.assertEqual(results_sorted[0].sequence, 50) + self.assertEqual(results_sorted[1].parameter_id, self.param3) + self.assertEqual(results_sorted[1].sequence, 75) + self.assertEqual(results_sorted[2].parameter_id, self.param1) + self.assertEqual(results_sorted[2].sequence, 100) + + def test_bulk_test_creation_performance(self): + """Test rendimiento de creación masiva de pruebas""" + # Crear múltiples órdenes + orders = [] + for i in range(5): + order = self.Order.create({ + 'partner_id': self.patient.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis_multi.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + order.action_confirm() + orders.append(order) + + # Generar muestras en lote + for order in orders: + order.action_generate_samples() + + # Verificar que todas las pruebas tienen resultados + total_tests = 0 + total_results = 0 + + for order in orders: + for sample in order.lab_sample_ids: + for test in sample.test_ids: + total_tests += 1 + total_results += len(test.result_ids) + + self.assertEqual(total_tests, 5) + self.assertEqual(total_results, 15) # 5 tests * 3 parameters each + + def test_result_generation_with_mixed_analyses(self): + """Test generación con análisis mixtos (con y sin parámetros)""" + # Crear orden con múltiples análisis + order = self.Order.create({ + 'partner_id': self.patient.id, + 'is_lab_request': True, + 'order_line': [ + (0, 0, { + 'product_id': self.analysis_multi.product_variant_id.id, + 'product_uom_qty': 1.0 + }), + (0, 0, { + 'product_id': self.analysis_empty.product_variant_id.id, + 'product_uom_qty': 1.0 + }) + ] + }) + order.action_confirm() + order.action_generate_samples() + + # Verificar resultados por prueba + tests_with_results = 0 + tests_without_results = 0 + + for sample in order.lab_sample_ids: + for test in sample.test_ids: + if test.result_ids: + tests_with_results += 1 + else: + tests_without_results += 1 + + self.assertEqual(tests_with_results, 1) # Solo analysis_multi + self.assertEqual(tests_without_results, 1) # Solo analysis_empty \ No newline at end of file diff --git a/lims_management/tests/test_parameter_range.py b/lims_management/tests/test_parameter_range.py new file mode 100644 index 0000000..97d7478 --- /dev/null +++ b/lims_management/tests/test_parameter_range.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +""" +Tests para el modelo lims.parameter.range +""" +from odoo.tests import TransactionCase +from odoo.exceptions import ValidationError + + +class TestParameterRange(TransactionCase): + """Tests para rangos de referencia de parámetros""" + + def setUp(self): + super().setUp() + self.Range = self.env['lims.parameter.range'] + self.Parameter = self.env['lims.analysis.parameter'] + + # Crear parámetro de prueba + self.test_param = self.Parameter.create({ + 'code': 'HGB_TEST', + 'name': 'Hemoglobina Test', + 'value_type': 'numeric', + 'unit': 'g/dL' + }) + + def test_create_basic_range(self): + """Test crear rango básico""" + range_obj = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Adulto General', + 'normal_min': 12.0, + 'normal_max': 16.0 + }) + + self.assertEqual(range_obj.parameter_id, self.test_param) + self.assertEqual(range_obj.normal_min, 12.0) + self.assertEqual(range_obj.normal_max, 16.0) + self.assertFalse(range_obj.gender) # Sin género específico + + def test_range_validation_min_max(self): + """Test validación que min < max""" + with self.assertRaises(ValidationError) as e: + self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Rango Inválido', + 'normal_min': 20.0, + 'normal_max': 10.0 # Max menor que min + }) + self.assertIn('menor o igual', str(e.exception)) + + def test_range_validation_age(self): + """Test validación de rangos de edad""" + with self.assertRaises(ValidationError) as e: + self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Rango Edad Inválida', + 'age_min': 65, + 'age_max': 18, # Max menor que min + 'normal_min': 12.0, + 'normal_max': 16.0 + }) + self.assertIn('edad', str(e.exception)) + + def test_critical_values_validation(self): + """Test validación de valores críticos""" + # Crítico min debe ser menor que normal min + with self.assertRaises(ValidationError) as e: + self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Crítico Inválido', + 'normal_min': 12.0, + 'normal_max': 16.0, + 'critical_min': 13.0 # Mayor que normal_min + }) + self.assertIn('crítico mínimo', str(e.exception)) + + # Crítico max debe ser mayor que normal max + with self.assertRaises(ValidationError) as e: + self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Crítico Inválido 2', + 'normal_min': 12.0, + 'normal_max': 16.0, + 'critical_max': 15.0 # Menor que normal_max + }) + self.assertIn('crítico máximo', str(e.exception)) + + def test_gender_specific_ranges(self): + """Test rangos específicos por género""" + # Rango para hombres + male_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Hombre Adulto', + 'gender': 'male', + 'age_min': 18, + 'age_max': 65, + 'normal_min': 14.0, + 'normal_max': 18.0 + }) + + # Rango para mujeres + female_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Mujer Adulta', + 'gender': 'female', + 'age_min': 18, + 'age_max': 65, + 'normal_min': 12.0, + 'normal_max': 16.0 + }) + + self.assertEqual(male_range.gender, 'male') + self.assertEqual(female_range.gender, 'female') + + def test_pregnancy_specific_range(self): + """Test rangos para embarazadas""" + pregnancy_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Embarazada', + 'gender': 'female', + 'pregnant': True, + 'age_min': 15, + 'age_max': 50, + 'normal_min': 11.0, + 'normal_max': 14.0 + }) + + self.assertTrue(pregnancy_range.pregnant) + self.assertEqual(pregnancy_range.gender, 'female') + + def test_find_applicable_range(self): + """Test encontrar rango aplicable según características del paciente""" + # Crear varios rangos + general_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'General', + 'normal_min': 12.0, + 'normal_max': 16.0 + }) + + male_adult_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Hombre Adulto', + 'gender': 'male', + 'age_min': 18, + 'age_max': 65, + 'normal_min': 14.0, + 'normal_max': 18.0 + }) + + child_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Niño', + 'age_max': 12, + 'normal_min': 11.0, + 'normal_max': 14.0 + }) + + pregnant_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Embarazada', + 'gender': 'female', + 'pregnant': True, + 'normal_min': 11.0, + 'normal_max': 14.0 + }) + + # Test para hombre adulto de 30 años + applicable = self.Range._find_applicable_range( + self.test_param.id, + gender='male', + age=30, + is_pregnant=False + ) + self.assertEqual(applicable, male_adult_range) + + # Test para niño de 8 años + applicable = self.Range._find_applicable_range( + self.test_param.id, + gender='male', + age=8, + is_pregnant=False + ) + self.assertEqual(applicable, child_range) + + # Test para mujer embarazada + applicable = self.Range._find_applicable_range( + self.test_param.id, + gender='female', + age=28, + is_pregnant=True + ) + self.assertEqual(applicable, pregnant_range) + + # Test para caso sin rango específico (mujer no embarazada) + applicable = self.Range._find_applicable_range( + self.test_param.id, + gender='female', + age=35, + is_pregnant=False + ) + self.assertEqual(applicable, general_range) # Debe devolver el rango general + + def test_range_overlap_allowed(self): + """Test que se permiten rangos superpuestos""" + # Rango 1: 0-18 años + range1 = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Pediátrico', + 'age_max': 18, + 'normal_min': 11.0, + 'normal_max': 15.0 + }) + + # Rango 2: 12-65 años (se superpone con rango 1) + range2 = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Adolescente-Adulto', + 'age_min': 12, + 'age_max': 65, + 'normal_min': 12.0, + 'normal_max': 16.0 + }) + + # Ambos rangos deben existir sin error + self.assertTrue(range1.exists()) + self.assertTrue(range2.exists()) + + def test_range_description_compute(self): + """Test generación automática de descripción""" + # Rango con todas las características + full_range = self.Range.create({ + 'parameter_id': self.test_param.id, + 'name': 'Completo', + 'gender': 'female', + 'age_min': 18, + 'age_max': 45, + 'pregnant': True, + 'normal_min': 11.0, + 'normal_max': 14.0, + 'critical_min': 8.0, + 'critical_max': 20.0 + }) + + description = full_range.description + self.assertIn('Mujer', description) + self.assertIn('18-45 años', description) + self.assertIn('Embarazada', description) + self.assertIn('11.0 - 14.0', description) + self.assertIn('Críticos', description) \ No newline at end of file diff --git a/lims_management/tests/test_result_parameter_integration.py b/lims_management/tests/test_result_parameter_integration.py new file mode 100644 index 0000000..15428ed --- /dev/null +++ b/lims_management/tests/test_result_parameter_integration.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +""" +Tests para la integración entre resultados y el catálogo de parámetros +""" +from odoo.tests import TransactionCase +from datetime import date + + +class TestResultParameterIntegration(TransactionCase): + """Tests para la integración de resultados con parámetros y rangos""" + + def setUp(self): + super().setUp() + + # Modelos + self.Result = self.env['lims.result'] + self.Test = self.env['lims.test'] + self.Parameter = self.env['lims.analysis.parameter'] + self.Range = self.env['lims.parameter.range'] + self.Partner = self.env['res.partner'] + self.Product = self.env['product.template'] + + # Crear paciente de prueba + self.patient_male = self.Partner.create({ + 'name': 'Test Patient Male', + 'is_patient': True, + 'gender': 'male', + 'birth_date': date(1990, 1, 1) # 34 años aprox + }) + + self.patient_female_pregnant = self.Partner.create({ + 'name': 'Test Patient Pregnant', + 'is_patient': True, + 'gender': 'female', + 'birth_date': date(1995, 6, 15), # 29 años aprox + 'is_pregnant': True + }) + + # Crear parámetro de prueba + self.param_glucose = self.Parameter.create({ + 'code': 'GLU_TEST', + 'name': 'Glucosa Test', + 'value_type': 'numeric', + 'unit': 'mg/dL' + }) + + # Crear rangos de referencia + self.range_general = self.Range.create({ + 'parameter_id': self.param_glucose.id, + 'name': 'General', + 'normal_min': 70.0, + 'normal_max': 100.0, + 'critical_min': 50.0, + 'critical_max': 200.0 + }) + + self.range_pregnant = self.Range.create({ + 'parameter_id': self.param_glucose.id, + 'name': 'Embarazada', + 'gender': 'female', + 'pregnant': True, + 'normal_min': 60.0, + 'normal_max': 95.0, + 'critical_min': 45.0, + 'critical_max': 180.0 + }) + + # Crear análisis de prueba + self.analysis = self.Product.create({ + 'name': 'Glucosa en Sangre Test', + 'type': 'service', + 'is_analysis': True, + 'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id, + }) + + # Configurar parámetro en el análisis + self.env['product.template.parameter'].create({ + 'product_tmpl_id': self.analysis.id, + 'parameter_id': self.param_glucose.id, + 'sequence': 10 + }) + + def test_result_parameter_assignment(self): + """Test asignación de parámetro a resultado""" + # Crear test + test = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + # Crear resultado + result = self.Result.create({ + 'test_id': test.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 85.0 + }) + + self.assertEqual(result.parameter_id, self.param_glucose) + self.assertEqual(result.value_type, 'numeric') + self.assertEqual(result.unit, 'mg/dL') + + def test_applicable_range_selection(self): + """Test selección automática de rango aplicable""" + # Test para paciente masculino + test_male = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + result_male = self.Result.create({ + 'test_id': test_male.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 85.0 + }) + + # Debe usar el rango general + self.assertEqual(result_male.applicable_range_id, self.range_general) + self.assertFalse(result_male.is_out_of_range) + self.assertFalse(result_male.is_critical) + + # Test para paciente embarazada + test_pregnant = self.Test.create({ + 'patient_id': self.patient_female_pregnant.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + result_pregnant = self.Result.create({ + 'test_id': test_pregnant.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 98.0 # Fuera de rango para embarazada + }) + + # Debe usar el rango para embarazadas + self.assertEqual(result_pregnant.applicable_range_id, self.range_pregnant) + self.assertTrue(result_pregnant.is_out_of_range) + self.assertFalse(result_pregnant.is_critical) + + def test_out_of_range_detection(self): + """Test detección de valores fuera de rango""" + test = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + # Valor normal + result_normal = self.Result.create({ + 'test_id': test.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 85.0 + }) + self.assertFalse(result_normal.is_out_of_range) + self.assertFalse(result_normal.is_critical) + + # Valor alto pero no crítico + result_high = self.Result.create({ + 'test_id': test.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 115.0 + }) + self.assertTrue(result_high.is_out_of_range) + self.assertFalse(result_high.is_critical) + + # Valor crítico alto + result_critical = self.Result.create({ + 'test_id': test.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 250.0 + }) + self.assertTrue(result_critical.is_out_of_range) + self.assertTrue(result_critical.is_critical) + + def test_selection_parameter_result(self): + """Test resultado con parámetro de selección""" + # Crear parámetro de selección + param_culture = self.Parameter.create({ + 'code': 'CULT_TEST', + 'name': 'Cultivo Test', + 'value_type': 'selection', + 'selection_values': 'Negativo,Positivo' + }) + + test = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + result = self.Result.create({ + 'test_id': test.id, + 'parameter_id': param_culture.id, + 'value_selection': 'Positivo' + }) + + self.assertEqual(result.value_type, 'selection') + self.assertEqual(result.value_selection, 'Positivo') + self.assertFalse(result.applicable_range_id) # Selection no tiene rangos + + def test_text_parameter_result(self): + """Test resultado con parámetro de texto""" + param_observation = self.Parameter.create({ + 'code': 'OBS_TEST', + 'name': 'Observación Test', + 'value_type': 'text' + }) + + test = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + result = self.Result.create({ + 'test_id': test.id, + 'parameter_id': param_observation.id, + 'value_text': 'Muestra hemolizada levemente' + }) + + self.assertEqual(result.value_type, 'text') + self.assertEqual(result.value_text, 'Muestra hemolizada levemente') + + def test_boolean_parameter_result(self): + """Test resultado con parámetro booleano""" + param_pregnancy = self.Parameter.create({ + 'code': 'PREG_TEST', + 'name': 'Embarazo Test', + 'value_type': 'boolean' + }) + + test = self.Test.create({ + 'patient_id': self.patient_female_pregnant.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + result = self.Result.create({ + 'test_id': test.id, + 'parameter_id': param_pregnancy.id, + 'value_boolean': True + }) + + self.assertEqual(result.value_type, 'boolean') + self.assertTrue(result.value_boolean) + + def test_formatted_value_display(self): + """Test formato de visualización de valores""" + test = self.Test.create({ + 'patient_id': self.patient_male.id, + 'product_id': self.analysis.product_variant_id.id, + 'state': 'draft' + }) + + # Valor numérico + result_numeric = self.Result.create({ + 'test_id': test.id, + 'parameter_id': self.param_glucose.id, + 'value_numeric': 85.5 + }) + self.assertEqual(result_numeric.formatted_value, '85.5 mg/dL') + + # Valor de selección + param_selection = self.Parameter.create({ + 'code': 'SEL_FORMAT', + 'name': 'Selection Format', + 'value_type': 'selection', + 'selection_values': 'Opción A,Opción B' + }) + + result_selection = self.Result.create({ + 'test_id': test.id, + 'parameter_id': param_selection.id, + 'value_selection': 'Opción A' + }) + self.assertEqual(result_selection.formatted_value, 'Opción A') + + # Valor booleano + param_bool = self.Parameter.create({ + 'code': 'BOOL_FORMAT', + 'name': 'Boolean Format', + 'value_type': 'boolean' + }) + + result_bool = self.Result.create({ + 'test_id': test.id, + 'parameter_id': param_bool.id, + 'value_boolean': True + }) + self.assertEqual(result_bool.formatted_value, 'Sí') \ No newline at end of file diff --git a/test/run_parameter_tests.py b/test/run_parameter_tests.py new file mode 100644 index 0000000..68d9619 --- /dev/null +++ b/test/run_parameter_tests.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script para ejecutar los tests del catálogo de parámetros +""" + +import odoo +import logging +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + +_logger = logging.getLogger(__name__) + +def run_parameter_catalog_tests(db_name='lims_demo'): + """Ejecuta todos los tests del catálogo de parámetros""" + + print("\n" + "="*70) + print("EJECUTANDO TESTS DEL CATÁLOGO DE PARÁMETROS") + print("="*70 + "\n") + + # Importar los tests + try: + from odoo.addons.lims_management.tests import ( + test_analysis_parameter, + test_parameter_range, + test_result_parameter_integration, + test_auto_result_generation + ) + print("✓ Tests importados correctamente\n") + except ImportError as e: + print(f"✗ Error importando tests: {e}") + return + + # Lista de clases de test a ejecutar + test_classes = [ + (test_analysis_parameter.TestAnalysisParameter, "Parámetros de Análisis"), + (test_parameter_range.TestParameterRange, "Rangos de Referencia"), + (test_result_parameter_integration.TestResultParameterIntegration, "Integración Resultados-Parámetros"), + (test_auto_result_generation.TestAutoResultGeneration, "Generación Automática de Resultados"), + ] + + # Conectar a la base de datos + registry = odoo.registry(db_name) + + # Ejecutar cada conjunto de tests + total_tests = 0 + passed_tests = 0 + failed_tests = 0 + + for test_class, test_name in test_classes: + print(f"\n--- Ejecutando tests de {test_name} ---") + + with registry.cursor() as cr: + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + # Crear instancia del test + test_instance = test_class() + test_instance.env = env + test_instance.cr = cr + test_instance.uid = odoo.SUPERUSER_ID + + # Obtener todos los métodos de test + test_methods = [method for method in dir(test_instance) + if method.startswith('test_')] + + for method_name in test_methods: + total_tests += 1 + try: + # Ejecutar setUp + test_instance.setUp() + + # Ejecutar el test + method = getattr(test_instance, method_name) + method() + + print(f" ✓ {method_name}") + passed_tests += 1 + + except Exception as e: + print(f" ✗ {method_name}: {str(e)}") + failed_tests += 1 + _logger.exception(f"Test failed: {method_name}") + + finally: + # Rollback para no afectar otros tests + cr.rollback() + + # Resumen final + print("\n" + "="*70) + print("RESUMEN DE TESTS") + print("="*70) + print(f"Total de tests ejecutados: {total_tests}") + print(f"✓ Tests exitosos: {passed_tests}") + print(f"✗ Tests fallidos: {failed_tests}") + + if failed_tests == 0: + print("\n✅ TODOS LOS TESTS PASARON EXITOSAMENTE") + else: + print(f"\n⚠️ {failed_tests} TESTS FALLARON") + + return failed_tests == 0 + + +def run_specific_test(db_name='lims_demo', test_module=None, test_method=None): + """Ejecuta un test específico para debugging""" + + if not test_module: + print("Debe especificar el módulo de test") + return + + print(f"\nEjecutando test específico: {test_module}") + if test_method: + print(f"Método: {test_method}") + + # Importar el módulo de test + exec(f"from odoo.addons.lims_management.tests import {test_module}") + module = eval(test_module) + + # Encontrar la clase de test + test_class = None + for item in dir(module): + obj = getattr(module, item) + if isinstance(obj, type) and issubclass(obj, TransactionCase) and obj != TransactionCase: + test_class = obj + break + + if not test_class: + print("No se encontró clase de test en el módulo") + return + + registry = odoo.registry(db_name) + + with registry.cursor() as cr: + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + test_instance = test_class() + test_instance.env = env + test_instance.cr = cr + test_instance.uid = odoo.SUPERUSER_ID + + if test_method: + # Ejecutar método específico + if hasattr(test_instance, test_method): + try: + test_instance.setUp() + method = getattr(test_instance, test_method) + method() + print(f"✓ Test {test_method} pasó exitosamente") + except Exception as e: + print(f"✗ Test {test_method} falló: {str(e)}") + import traceback + traceback.print_exc() + else: + print(f"Método {test_method} no encontrado") + else: + # Ejecutar todos los métodos del módulo + test_methods = [m for m in dir(test_instance) if m.startswith('test_')] + for method_name in test_methods: + try: + test_instance.setUp() + method = getattr(test_instance, method_name) + method() + print(f"✓ {method_name}") + except Exception as e: + print(f"✗ {method_name}: {str(e)}") + finally: + cr.rollback() + + +if __name__ == '__main__': + import sys + + # Verificar argumentos + if len(sys.argv) > 1: + if sys.argv[1] == '--specific': + # Modo de test específico + module = sys.argv[2] if len(sys.argv) > 2 else None + method = sys.argv[3] if len(sys.argv) > 3 else None + run_specific_test(test_module=module, test_method=method) + else: + # Usar base de datos especificada + run_parameter_catalog_tests(db_name=sys.argv[1]) + else: + # Ejecutar todos los tests con DB por defecto + success = run_parameter_catalog_tests() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test/test_parameters_simple.py b/test/test_parameters_simple.py new file mode 100644 index 0000000..1bc3bf4 --- /dev/null +++ b/test/test_parameters_simple.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script simplificado para probar el catálogo de parámetros +""" + +import odoo +import logging + +_logger = logging.getLogger(__name__) + +def test_parameter_catalog(cr): + """Prueba el funcionamiento del catálogo de parámetros""" + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + # Limpiar parámetros de test anteriores + test_params = env['lims.analysis.parameter'].search([ + ('code', 'like', 'TEST_%') + ]) + if test_params: + print(f"Limpiando {len(test_params)} parámetros de test anteriores...") + test_params.unlink() + + print("\n" + "="*60) + print("TEST: CATÁLOGO DE PARÁMETROS") + print("="*60 + "\n") + + # Test 1: Crear parámetro numérico + print("1. Creando parámetro numérico...") + try: + param_numeric = env['lims.analysis.parameter'].create({ + 'code': 'TEST_NUM_001', + 'name': 'Test Numérico', + 'value_type': 'numeric', + 'unit': 'mg/dL', + 'description': 'Parámetro de prueba numérico' + }) + print(f" ✓ Parámetro creado: {param_numeric.name} ({param_numeric.code})") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 2: Validación - parámetro numérico sin unidad + print("\n2. Validando requerimiento de unidad...") + try: + env['lims.analysis.parameter'].create({ + 'code': 'TEST_NUM_002', + 'name': 'Test Sin Unidad', + 'value_type': 'numeric', + # Sin unit - debe fallar + }) + print(" ✗ Error: Se permitió crear parámetro numérico sin unidad") + return False + except Exception as e: + if 'unidad de medida' in str(e): + print(" ✓ Validación correcta: Se requiere unidad para parámetros numéricos") + else: + print(f" ✗ Error inesperado: {e}") + return False + + # Test 3: Crear parámetro de selección + print("\n3. Creando parámetro de selección...") + try: + param_selection = env['lims.analysis.parameter'].create({ + 'code': 'TEST_SEL_001', + 'name': 'Test Selección', + 'value_type': 'selection', + 'selection_values': 'Positivo,Negativo,Indeterminado' + }) + print(f" ✓ Parámetro de selección creado con valores: {param_selection.selection_values}") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 4: Crear rango de referencia + print("\n4. Creando rangos de referencia...") + try: + range_general = env['lims.parameter.range'].create({ + 'parameter_id': param_numeric.id, + 'name': 'Rango General', + 'normal_min': 70.0, + 'normal_max': 100.0, + 'critical_min': 50.0, + 'critical_max': 200.0 + }) + print(f" ✓ Rango general creado: {range_general.normal_min} - {range_general.normal_max}") + + range_male = env['lims.parameter.range'].create({ + 'parameter_id': param_numeric.id, + 'name': 'Hombre Adulto', + 'gender': 'male', + 'age_min': 18, + 'age_max': 65, + 'normal_min': 75.0, + 'normal_max': 105.0 + }) + print(f" ✓ Rango específico creado: Hombre {range_male.age_min}-{range_male.age_max} años") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 5: Configurar parámetro en análisis + print("\n5. Configurando parámetros en análisis...") + try: + # Obtener un análisis existente + analysis = env['product.template'].search([ + ('is_analysis', '=', True) + ], limit=1) + + if not analysis: + print(" ⚠️ No se encontraron análisis para configurar") + else: + config = env['product.template.parameter'].create({ + 'product_tmpl_id': analysis.id, + 'parameter_id': param_numeric.id, + 'sequence': 999 + }) + print(f" ✓ Parámetro configurado en análisis: {analysis.name}") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 6: Generación automática de resultados + print("\n6. Probando generación automática de resultados...") + try: + # Buscar una prueba existente + test = env['lims.test'].search([ + ('state', '=', 'draft') + ], limit=1) + + if test and analysis: + # Cambiar el producto de la prueba para trigger la regeneración + original_product = test.product_id + test.product_id = analysis.product_variant_id.id + + # Verificar que se generó el resultado + result = test.result_ids.filtered(lambda r: r.parameter_id == param_numeric) + if result: + print(f" ✓ Resultado generado automáticamente para parámetro: {param_numeric.name}") + else: + print(" ⚠️ No se generó resultado automático") + + # Restaurar producto original + test.product_id = original_product.id + else: + print(" ⚠️ No se encontraron pruebas en borrador para probar") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 7: Verificar datos demo cargados + print("\n7. Verificando datos demo del catálogo...") + try: + param_count = env['lims.analysis.parameter'].search_count([]) + range_count = env['lims.parameter.range'].search_count([]) + config_count = env['product.template.parameter'].search_count([]) + + print(f" - Parámetros totales: {param_count}") + print(f" - Rangos de referencia: {range_count}") + print(f" - Configuraciones parámetro-análisis: {config_count}") + + # Verificar algunos parámetros específicos + hemoglobin = env.ref('lims_management.param_hemoglobin', raise_if_not_found=False) + if hemoglobin: + print(f" ✓ Parámetro demo encontrado: {hemoglobin.display_name}") + print(f" - Rangos asociados: {len(hemoglobin.range_ids)}") + except Exception as e: + print(f" ✗ Error: {e}") + return False + + # Test 8: Buscar rango aplicable + print("\n8. Probando búsqueda de rango aplicable...") + try: + # Crear paciente de prueba + patient = env['res.partner'].create({ + 'name': 'Paciente Test Rango', + 'is_patient': True, + 'gender': 'male', + 'birthdate_date': '1990-01-01' # 34 años aprox + }) + + # Buscar rango aplicable + Range = env['lims.parameter.range'] + applicable = Range._find_applicable_range( + param_numeric.id, + gender='male', + age=34, + is_pregnant=False + ) + + if applicable: + print(f" ✓ Rango aplicable encontrado: {applicable.name}") + print(f" - Valores normales: {applicable.normal_min} - {applicable.normal_max}") + else: + print(" ⚠️ No se encontró rango aplicable") + + # Limpiar + patient.unlink() + except Exception as e: + print(f" ✗ Error: {e}") + return False + + print("\n" + "="*60) + print("✅ TODOS LOS TESTS PASARON EXITOSAMENTE") + print("="*60) + return True + + +if __name__ == '__main__': + db_name = 'lims_demo' + registry = odoo.registry(db_name) + + with registry.cursor() as cr: + try: + success = test_parameter_catalog(cr) + if not success: + print("\n⚠️ ALGUNOS TESTS FALLARON") + except Exception as e: + print(f"\n✗ Error crítico: {e}") + import traceback + traceback.print_exc() \ No newline at end of file