Merge pull request 'fix(#51): Corregir errores de compatibilidad con Odoo 18 y validación' (#53) from feature/51-parameter-catalog into dev

This commit is contained in:
luis_portillo 2025-07-16 00:36:19 +00:00
commit 6c3fa0bc4d
58 changed files with 7660 additions and 412 deletions

View File

@ -19,7 +19,11 @@
"Bash(move lab_logo.png lims_management/static/img/lab_logo.png)",
"WebFetch(domain:github.com)",
"WebFetch(domain:apps.odoo.com)",
"Bash(dir:*)"
"Bash(dir:*)",
"Bash(find:*)",
"Bash(true)",
"Bash(bash:*)",
"Bash(grep:*)"
],
"deny": []
}

View File

@ -132,8 +132,8 @@ At the start of each work session, read these documents to understand requiremen
### Odoo 18 Specific Conventions
#### View Definitions
- **CRITICAL**: Use `<list>` instead of `<tree>` - using `<tree>` causes `ValueError: Wrong value for ir.ui.view.type: 'tree'`
- View mode in actions must be `list,form` not `tree,form`
- **CRITICAL**: Use `<list>` instead of `<tree>` in view XML - using `<tree>` causes error "El nodo raíz de una vista list debe ser <list>, no <tree>"
- View mode in actions must be `tree,form` not `list,form` (paradójicamente, el modo se llama "tree" pero el XML debe usar `<list>`)
#### Visibility Attributes
- Use `invisible` attribute directly instead of `attrs`:
@ -257,4 +257,96 @@ Automatically installed via `scripts/install_hooks.sh`:
### Branch Naming
- Feature branches: `feature/XX-description` (where XX is issue number)
- Always create PRs to 'dev' branch, not 'main'
- Always create PRs to 'dev' branch, not 'main'
## Desarrollo de nuevos modelos y vistas
### Orden de carga en __manifest__.py
Al agregar archivos al manifest, seguir SIEMPRE este orden:
1. security/*.xml (grupos y categorías)
2. security/ir.model.access.csv
3. data/*.xml (secuencias, categorías, datos base)
4. views/*_views.xml en este orden específico:
- Modelos base (sin dependencias)
- Modelos dependientes
- Vistas que referencian acciones
- menus.xml (SIEMPRE al final de views)
5. wizards/*.xml
6. reports/*.xml
7. demo/*.xml
### Desarrollo de modelos relacionados
Cuando crees modelos que se relacionan entre sí en el mismo issue:
#### Fase 1: Modelos base
1. Crear modelos SIN campos One2many
2. Solo incluir campos básicos y Many2one si el modelo referenciado ya existe
3. Probar que la instancia levante
#### Fase 2: Relaciones
1. Agregar campos One2many en los modelos padre
2. Verificar que todos los inverse_name existan
3. Probar nuevamente
#### Fase 3: Vistas complejas
1. Agregar vistas con referencias a acciones
2. Verificar que las acciones referenciadas ya estén definidas
### Contextos en vistas XML
- En formularios: usar `id` (NO `active_id`)
- En acciones de ventana: usar `active_id`
- En campos One2many: usar `parent` para referenciar el registro padre
### Checklist antes de reiniciar instancia
- [ ] ¿Los modelos referenciados en relaciones ya existen?
- [ ] ¿Las acciones/vistas referenciadas se cargan ANTES?
- [ ] ¿Los grupos en ir.model.access.csv coinciden con los de security.xml?
- [ ] ¿Usaste `id` en lugar de `active_id` en contextos de formulario?
- [ ] ¿Verificaste que todos los campos en las vistas existen en los modelos?
- [ ] ¿Los nombres de métodos/acciones coinciden exactamente con los definidos en Python?
- [ ] ¿Los widgets utilizados son válidos en Odoo 18?
### Desarrollo de vistas - Mejores prácticas
#### Antes de crear vistas:
1. **Verificar campos del modelo**: SIEMPRE revisar qué campos existen con `grep "fields\." models/archivo.py`
2. **Verificar métodos disponibles**: Buscar métodos con `grep "def action_" models/archivo.py`
3. **Verificar campos relacionados**: Confirmar que los campos related tienen la ruta correcta
#### Orden de creación de vistas:
1. **Primero**: Definir todas las acciones (ir.actions.act_window) en un solo lugar
2. **Segundo**: Crear las vistas (form, list, search, etc.)
3. **Tercero**: Crear los menús que referencian las acciones
4. **Cuarto**: Si hay referencias cruzadas entre archivos, considerar consolidar en un solo archivo
#### Widgets válidos en Odoo 18:
- Numéricos: `float`, `integer`, `monetary` (NO `float_time` para datos generales)
- Texto: `text`, `char`, `html` (NO `text_emojis`)
- Booleanos: `boolean`, `boolean_toggle`, `boolean_button`
- Selección: `selection`, `radio`, `selection_badge`
- Relaciones: `many2one`, `many2many_tags`
- Estado: `statusbar`, `badge`, `progressbar`
#### Errores comunes y soluciones:
##### Error: "External ID not found"
- **Causa**: Referencia a un ID que aún no fue cargado
- **Solución**: Reorganizar orden en __manifest__.py o mover definición al mismo archivo
##### Error: "Field 'X' does not exist"
- **Causa**: Vista referencia campo inexistente en el modelo
- **Solución**: Verificar modelo y agregar campo o corregir nombre en vista
##### Error: "action_X is not a valid action"
- **Causa**: Nombre de método incorrecto en botón
- **Solución**: Verificar nombre exacto del método en el modelo Python
##### Error: "Invalid widget"
- **Causa**: Uso de widget no existente o deprecated
- **Solución**: Usar widgets estándar de Odoo 18
#### Estrategia de depuración:
1. Leer el error completo en los logs
2. Identificar archivo y línea exacta del problema
3. Verificar que el elemento referenciado existe y está accesible
4. Si es necesario, simplificar la vista temporalmente para aislar el problema

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
# Plan de Implementación - Issue #51: Catálogo de Parámetros de Laboratorio
## Objetivo
Implementar un catálogo maestro de parámetros de laboratorio con configuración por análisis y rangos de referencia flexibles basados en edad, sexo y otras condiciones del paciente.
## Arquitectura Propuesta
### Modelos Principales
1. **lims.analysis.parameter** - Catálogo maestro de parámetros
2. **product.template.parameter** - Asociación parámetro-análisis
3. **lims.parameter.range** - Rangos de referencia flexibles
4. **lims.result** (modificado) - Usar parameter_id en lugar de parameter_name
## Fases de Implementación
### Fase 1: Creación de Modelos Base (Tasks 1-4)
**Objetivo**: Establecer la estructura de datos fundamental
#### Task 1: Crear modelo lims.analysis.parameter
- Crear archivo `lims_management/models/analysis_parameter.py`
- Definir campos: name, code, value_type, unit, selection_values, description, active
- Implementar constraints y validaciones
- Crear vistas (list, form) para gestión del catálogo
- Agregar menú de configuración
- Crear permisos de seguridad
#### Task 2: Crear modelo product.template.parameter
- Crear archivo `lims_management/models/product_template_parameter.py`
- Definir relación entre product.template y lims.analysis.parameter
- Implementar campos: sequence, required, instructions
- Agregar constraint de unicidad
- Crear vista embebida en product.template
- Actualizar herencia de product.template
#### Task 3: Crear modelo lims.parameter.range
- Crear archivo `lims_management/models/parameter_range.py`
- Implementar campos de condiciones: gender, age_min, age_max, pregnant
- Implementar campos de valores: normal_min/max, critical_min/max
- Crear método _compute_name()
- Agregar constraint de unicidad
- Crear vistas de configuración
#### Task 4: Agregar método _compute_age() en res.partner
- Extender modelo res.partner
- Implementar cálculo de edad basado en birth_date
- Agregar campo is_pregnant (Boolean)
- Crear tests unitarios para el cálculo
### Fase 2: Migración y Adaptación (Tasks 5-7)
**Objetivo**: Adaptar el sistema existente al nuevo modelo
#### Task 5: Modificar modelo lims.result
- Cambiar parameter_name (Char) a parameter_id (Many2one)
- Mantener parameter_name como campo related (compatibilidad)
- Implementar _compute_applicable_range()
- Actualizar _compute_is_out_of_range() para usar rangos flexibles
- Crear script de migración de datos
#### Task 6: Actualizar generación automática de resultados
- Modificar _generate_test_results() en lims.test
- Generar líneas basadas en product.template.parameter
- Respetar orden (sequence) y obligatoriedad
- Asignar tipos de dato correctos
#### Task 7: Eliminar modelo obsoleto lims.analysis.range
- Remover archivo del modelo
- Eliminar referencias en product.template
- Actualizar vistas que lo referencian
- Limpiar datos de demo
- Actualizar __init__.py y __manifest__.py
### Fase 3: Interfaz de Usuario (Tasks 8-10)
**Objetivo**: Crear interfaces intuitivas para configuración y uso
#### Task 8: Crear vistas de configuración de parámetros
- Vista de catálogo de parámetros (búsqueda, filtros)
- Formulario de parámetro con smart buttons
- Vista de configuración de parámetros por análisis
- Vista de rangos con filtros por parámetro
#### Task 9: Actualizar vistas de ingreso de resultados
- Adaptar formulario de lims.result
- Mostrar tipo de dato esperado
- Validación en tiempo real
- Indicadores visuales de valores fuera de rango
- Mostrar rango aplicable según paciente
#### Task 10: Crear wizards de configuración masiva
- Wizard para copiar configuración entre análisis
- Wizard para importar parámetros desde CSV
- Wizard para aplicar rangos a múltiples parámetros
### Fase 4: Datos y Validación (Tasks 11-13)
**Objetivo**: Poblar el sistema con datos útiles y validar funcionamiento
#### Task 11: Crear datos de demostración
- Parámetros comunes de hematología
- Parámetros de química sanguínea
- Configuración para análisis existentes
- Rangos por edad/sexo realistas
- Casos de prueba especiales
#### Task 12: Desarrollar tests automatizados
- Tests unitarios para modelos
- Tests de integración para flujos
- Tests de validación de rangos
- Tests de migración de datos
- Tests de rendimiento
#### Task 13: Actualizar reportes
- Modificar report_test_result
- Incluir información del catálogo
- Mostrar rangos aplicables
- Resaltar valores anormales
- Agregar interpretación cuando esté disponible
## Consideraciones Técnicas
### Migración de Datos
- Script Python para migrar parameter_name existentes
- Crear parámetros automáticamente desde histórico
- Mantener compatibilidad durante transición
- Backup antes de migración
### Performance
- Índices en campos de búsqueda frecuente
- Cache para rangos aplicables
- Lazy loading en vistas con muchos parámetros
### Seguridad
- Solo administradores pueden crear/modificar catálogo
- Técnicos pueden ver pero no editar parámetros
- Logs de auditoría para cambios en rangos
## Cronograma Estimado
- **Fase 1**: 2-3 días (Modelos base y estructura)
- **Fase 2**: 2 días (Migración y adaptación)
- **Fase 3**: 2 días (Interfaces de usuario)
- **Fase 4**: 1-2 días (Datos y validación)
**Total estimado**: 7-9 días de desarrollo
## Riesgos y Mitigaciones
1. **Riesgo**: Pérdida de datos durante migración
- **Mitigación**: Scripts de backup y rollback
2. **Riesgo**: Resistencia al cambio de usuarios
- **Mitigación**: Mantener compatibilidad temporal, capacitación
3. **Riesgo**: Complejidad en rangos múltiples
- **Mitigación**: UI intuitiva, valores por defecto sensatos
## Criterios de Éxito
- [ ] Todos los tests automatizados pasan
- [ ] Migración sin pérdida de datos
- [ ] Validación automática funcional
- [ ] Reportes muestran información correcta
- [ ] Performance aceptable (< 2s carga de resultados)
- [ ] Documentación actualizada
## Próximos Pasos
1. Revisar y aprobar este plan
2. Comenzar con Task 1: Crear modelo lims.analysis.parameter
3. Seguir el orden de las fases para mantener coherencia
4. Validar cada fase antes de continuar
---
**Nota**: Este plan está sujeto a ajustes según se descubran nuevos requerimientos o complejidades durante la implementación.

View File

@ -36,6 +36,7 @@ odoo_command = [
"-d", DB_NAME,
"-i", MODULES_TO_INSTALL,
"--load-language", "es_ES",
"--without-demo=", # Forzar carga de datos demo
"--stop-after-init"
]
@ -99,34 +100,62 @@ EOF
print("\nCreando datos de demostración de pruebas de laboratorio...")
sys.stdout.flush()
if os.path.exists("/app/test/create_test_demo_data.py"):
with open("/app/test/create_test_demo_data.py", "r") as f:
test_script_content = f.read()
# Usar el nuevo script consolidado de datos demo
demo_script_path = "/app/test/create_demo_data.py"
if os.path.exists(demo_script_path):
with open(demo_script_path, "r") as f:
demo_script_content = f.read()
create_tests_command = f"""
create_demo_command = f"""
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
{test_script_content}
{demo_script_content}
EOF
"""
result = subprocess.run(
create_tests_command,
create_demo_command,
shell=True,
capture_output=True,
text=True,
check=False
)
print("--- Create Test Demo Data stdout ---")
print("--- Create Demo Data stdout ---")
print(result.stdout)
print("--- Create Test Demo Data stderr ---")
print("--- Create Demo Data stderr ---")
print(result.stderr)
sys.stdout.flush()
if result.returncode == 0:
print("Datos de demostración de pruebas creados exitosamente.")
print("Datos de demostración creados exitosamente.")
else:
print(f"Advertencia: Fallo al crear datos de demostración de pruebas (código {result.returncode})")
print(f"Advertencia: Fallo al crear datos de demostración (código {result.returncode})")
else:
# Fallback al script anterior si existe
old_script_path = "/app/test/create_test_demo_data.py"
if os.path.exists(old_script_path):
print("Usando script de demostración anterior...")
with open(old_script_path, "r") as f:
test_script_content = f.read()
create_tests_command = f"""
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
{test_script_content}
EOF
"""
result = subprocess.run(
create_tests_command,
shell=True,
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
print("Datos de demostración de pruebas creados exitosamente.")
else:
print(f"Advertencia: Fallo al crear datos de demostración de pruebas (código {result.returncode})")
# --- Actualizar logo de la empresa ---
print("\nActualizando logo de la empresa...")

View File

@ -34,13 +34,23 @@
'views/sale_order_views.xml',
'views/stock_lot_views.xml',
'views/lims_test_views.xml',
'views/lims_result_views.xml',
'views/lims_result_bulk_entry_views.xml',
'views/res_config_settings_views.xml',
'views/product_template_parameter_views.xml',
'views/parameter_range_views.xml',
'views/analysis_parameter_views.xml',
'views/product_template_parameter_config_views.xml',
'views/parameter_dashboard_views.xml',
'views/menus.xml',
],
'demo': [
'demo/z_lims_demo.xml',
'demo/z_analysis_demo.xml',
'demo/z_sample_demo.xml',
'demo/parameter_demo.xml',
'demo/parameter_range_demo.xml',
'demo/analysis_parameter_config_demo.xml',
'demo/z_automatic_generation_demo.xml',
],
'installable': True,

View File

@ -0,0 +1,363 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Configuración de parámetros para Hemograma Completo -->
<record id="config_hemograma_hgb" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_hemoglobin"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_hct" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_hematocrit"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_rbc" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_rbc"/>
<field name="sequence">30</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_wbc" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_wbc"/>
<field name="sequence">40</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_plt" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_platelets"/>
<field name="sequence">50</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_neut" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_neutrophils"/>
<field name="sequence">60</field>
<field name="required">True</field>
</record>
<record id="config_hemograma_lymph" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemograma"/>
<field name="parameter_id" ref="param_lymphocytes"/>
<field name="sequence">70</field>
<field name="required">True</field>
</record>
<!-- Configuración de parámetros para Perfil Lipídico -->
<record id="config_lipidos_chol" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
<field name="parameter_id" ref="param_cholesterol_total"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_lipidos_hdl" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
<field name="parameter_id" ref="param_cholesterol_hdl"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<record id="config_lipidos_ldl" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
<field name="parameter_id" ref="param_cholesterol_ldl"/>
<field name="sequence">30</field>
<field name="required">True</field>
</record>
<record id="config_lipidos_trig" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
<field name="parameter_id" ref="param_triglycerides"/>
<field name="sequence">40</field>
<field name="required">True</field>
</record>
<!-- Configuración de parámetros para Glucosa -->
<record id="config_glucosa" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_glucosa"/>
<field name="parameter_id" ref="param_glucose"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<!-- Configuración de parámetros para Urocultivo -->
<record id="config_urocultivo_result" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
<field name="parameter_id" ref="param_culture_result"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_urocultivo_organism" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
<field name="parameter_id" ref="param_isolated_organism"/>
<field name="sequence">20</field>
<field name="required">False</field>
<field name="instructions">Completar solo si el cultivo es positivo</field>
</record>
<record id="config_urocultivo_count" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
<field name="parameter_id" ref="param_colony_count"/>
<field name="sequence">30</field>
<field name="required">False</field>
<field name="instructions">Completar solo si el cultivo es positivo. Formato: >100,000 UFC/mL</field>
</record>
<!-- Configuración de parámetros para Tiempo de Protrombina -->
<record id="config_tp_time" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_tp"/>
<field name="parameter_id" ref="param_pt"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_tp_inr" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_tp"/>
<field name="parameter_id" ref="param_inr"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<!-- Configuración de parámetros para Hemocultivo -->
<record id="config_hemocultivo_result" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemocultivo"/>
<field name="parameter_id" ref="param_culture_result"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_hemocultivo_organism" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_hemocultivo"/>
<field name="parameter_id" ref="param_isolated_organism"/>
<field name="sequence">20</field>
<field name="required">False</field>
</record>
<!-- Configuración de parámetros para Coprocultivo -->
<record id="config_coprocultivo_result" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_coprocultivo"/>
<field name="parameter_id" ref="param_culture_result"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_coprocultivo_organism" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_coprocultivo"/>
<field name="parameter_id" ref="param_isolated_organism"/>
<field name="sequence">20</field>
<field name="required">False</field>
</record>
<!-- Crear análisis adicionales comunes -->
<!-- Análisis: Química Sanguínea -->
<record id="analysis_quimica_sanguinea" model="product.template">
<field name="name">Química Sanguínea Básica</field>
<field name="is_analysis">True</field>
<field name="analysis_type">chemistry</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
<field name="sample_volume_ml">3.0</field>
<field name="technical_specifications">
Panel básico de química sanguínea que incluye glucosa, creatinina, urea, ALT y AST.
</field>
</record>
<!-- Configurar parámetros para Química Sanguínea -->
<record id="config_quimica_glucose" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
<field name="parameter_id" ref="param_glucose"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_quimica_crea" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
<field name="parameter_id" ref="param_creatinine"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<record id="config_quimica_urea" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
<field name="parameter_id" ref="param_urea"/>
<field name="sequence">30</field>
<field name="required">True</field>
</record>
<record id="config_quimica_alt" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
<field name="parameter_id" ref="param_alt"/>
<field name="sequence">40</field>
<field name="required">True</field>
</record>
<record id="config_quimica_ast" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
<field name="parameter_id" ref="param_ast"/>
<field name="sequence">50</field>
<field name="required">True</field>
</record>
<!-- Análisis: Urianálisis Completo -->
<record id="analysis_urianalisis" model="product.template">
<field name="name">Urianálisis Completo</field>
<field name="is_analysis">True</field>
<field name="analysis_type">other</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="required_sample_type_id" ref="lims_management.sample_type_urine_container"/>
<field name="sample_volume_ml">10.0</field>
<field name="technical_specifications">
Examen completo de orina que incluye examen físico, químico y microscópico del sedimento.
</field>
</record>
<!-- Configurar parámetros para Urianálisis -->
<record id="config_urine_color" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_color"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_urine_appearance" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_appearance"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<record id="config_urine_ph" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_ph"/>
<field name="sequence">30</field>
<field name="required">True</field>
</record>
<record id="config_urine_density" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_density"/>
<field name="sequence">40</field>
<field name="required">True</field>
</record>
<record id="config_urine_protein" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_protein"/>
<field name="sequence">50</field>
<field name="required">True</field>
</record>
<record id="config_urine_glucose" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_glucose"/>
<field name="sequence">60</field>
<field name="required">True</field>
</record>
<record id="config_urine_blood" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_blood"/>
<field name="sequence">70</field>
<field name="required">True</field>
</record>
<record id="config_urine_leukocytes" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_leukocytes"/>
<field name="sequence">80</field>
<field name="required">True</field>
</record>
<record id="config_urine_bacteria" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
<field name="parameter_id" ref="param_urine_bacteria"/>
<field name="sequence">90</field>
<field name="required">True</field>
</record>
<!-- Análisis: Panel de Serología -->
<record id="analysis_serologia" model="product.template">
<field name="name">Panel de Serología Básica</field>
<field name="is_analysis">True</field>
<field name="analysis_type">immunology</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
<field name="sample_volume_ml">5.0</field>
<field name="technical_specifications">
Panel serológico que incluye HIV, Hepatitis B, Hepatitis C y VDRL.
</field>
</record>
<!-- Configurar parámetros para Serología -->
<record id="config_sero_hiv" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_serologia"/>
<field name="parameter_id" ref="param_hiv"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
<record id="config_sero_hbsag" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_serologia"/>
<field name="parameter_id" ref="param_hbsag"/>
<field name="sequence">20</field>
<field name="required">True</field>
</record>
<record id="config_sero_hcv" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_serologia"/>
<field name="parameter_id" ref="param_hcv"/>
<field name="sequence">30</field>
<field name="required">True</field>
</record>
<record id="config_sero_vdrl" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_serologia"/>
<field name="parameter_id" ref="param_vdrl"/>
<field name="sequence">40</field>
<field name="required">True</field>
</record>
<!-- Análisis: Prueba de Embarazo -->
<record id="analysis_prueba_embarazo" model="product.template">
<field name="name">Prueba de Embarazo en Sangre</field>
<field name="is_analysis">True</field>
<field name="analysis_type">immunology</field>
<field name="categ_id" ref="lims_management.product_category_analysis"/>
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="True"/>
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
<field name="sample_volume_ml">1.0</field>
<field name="technical_specifications">
Detección cualitativa de Beta-HCG en sangre.
</field>
</record>
<record id="config_pregnancy_test" model="product.template.parameter">
<field name="product_tmpl_id" ref="analysis_prueba_embarazo"/>
<field name="parameter_id" ref="param_pregnancy"/>
<field name="sequence">10</field>
<field name="required">True</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,339 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Parámetros de Hematología -->
<!-- Hemoglobina -->
<record id="param_hemoglobin" model="lims.analysis.parameter">
<field name="code">HGB</field>
<field name="name">Hemoglobina</field>
<field name="value_type">numeric</field>
<field name="unit">g/dL</field>
<field name="description">Concentración de hemoglobina en sangre</field>
</record>
<!-- Hematocrito -->
<record id="param_hematocrit" model="lims.analysis.parameter">
<field name="code">HCT</field>
<field name="name">Hematocrito</field>
<field name="value_type">numeric</field>
<field name="unit">%</field>
<field name="description">Porcentaje del volumen de glóbulos rojos</field>
</record>
<!-- Glóbulos Rojos -->
<record id="param_rbc" model="lims.analysis.parameter">
<field name="code">RBC</field>
<field name="name">Glóbulos Rojos</field>
<field name="value_type">numeric</field>
<field name="unit">millones/µL</field>
<field name="description">Recuento de eritrocitos</field>
</record>
<!-- Glóbulos Blancos -->
<record id="param_wbc" model="lims.analysis.parameter">
<field name="code">WBC</field>
<field name="name">Glóbulos Blancos</field>
<field name="value_type">numeric</field>
<field name="unit">mil/µL</field>
<field name="description">Recuento de leucocitos</field>
</record>
<!-- Plaquetas -->
<record id="param_platelets" model="lims.analysis.parameter">
<field name="code">PLT</field>
<field name="name">Plaquetas</field>
<field name="value_type">numeric</field>
<field name="unit">mil/µL</field>
<field name="description">Recuento de plaquetas</field>
</record>
<!-- Neutrófilos -->
<record id="param_neutrophils" model="lims.analysis.parameter">
<field name="code">NEUT</field>
<field name="name">Neutrófilos</field>
<field name="value_type">numeric</field>
<field name="unit">%</field>
<field name="description">Porcentaje de neutrófilos</field>
</record>
<!-- Linfocitos -->
<record id="param_lymphocytes" model="lims.analysis.parameter">
<field name="code">LYMPH</field>
<field name="name">Linfocitos</field>
<field name="value_type">numeric</field>
<field name="unit">%</field>
<field name="description">Porcentaje de linfocitos</field>
</record>
<!-- Parámetros de Química Clínica -->
<!-- Glucosa -->
<record id="param_glucose" model="lims.analysis.parameter">
<field name="code">GLU</field>
<field name="name">Glucosa</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Nivel de glucosa en sangre</field>
</record>
<!-- Creatinina -->
<record id="param_creatinine" model="lims.analysis.parameter">
<field name="code">CREA</field>
<field name="name">Creatinina</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Nivel de creatinina sérica</field>
</record>
<!-- Urea -->
<record id="param_urea" model="lims.analysis.parameter">
<field name="code">UREA</field>
<field name="name">Urea</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Nivel de urea en sangre</field>
</record>
<!-- Colesterol Total -->
<record id="param_cholesterol_total" model="lims.analysis.parameter">
<field name="code">CHOL</field>
<field name="name">Colesterol Total</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Nivel de colesterol total</field>
</record>
<!-- Colesterol HDL -->
<record id="param_cholesterol_hdl" model="lims.analysis.parameter">
<field name="code">HDL</field>
<field name="name">Colesterol HDL</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Colesterol de alta densidad</field>
</record>
<!-- Colesterol LDL -->
<record id="param_cholesterol_ldl" model="lims.analysis.parameter">
<field name="code">LDL</field>
<field name="name">Colesterol LDL</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Colesterol de baja densidad</field>
</record>
<!-- Triglicéridos -->
<record id="param_triglycerides" model="lims.analysis.parameter">
<field name="code">TRIG</field>
<field name="name">Triglicéridos</field>
<field name="value_type">numeric</field>
<field name="unit">mg/dL</field>
<field name="description">Nivel de triglicéridos</field>
</record>
<!-- ALT -->
<record id="param_alt" model="lims.analysis.parameter">
<field name="code">ALT</field>
<field name="name">Alanina Aminotransferasa (ALT)</field>
<field name="value_type">numeric</field>
<field name="unit">U/L</field>
<field name="description">Enzima hepática ALT</field>
</record>
<!-- AST -->
<record id="param_ast" model="lims.analysis.parameter">
<field name="code">AST</field>
<field name="name">Aspartato Aminotransferasa (AST)</field>
<field name="value_type">numeric</field>
<field name="unit">U/L</field>
<field name="description">Enzima hepática AST</field>
</record>
<!-- Parámetros de Urianálisis -->
<!-- Color de Orina -->
<record id="param_urine_color" model="lims.analysis.parameter">
<field name="code">U-COLOR</field>
<field name="name">Color</field>
<field name="value_type">selection</field>
<field name="selection_values">Amarillo claro,Amarillo,Amarillo oscuro,Ámbar,Rojizo,Marrón,Turbio</field>
<field name="description">Color de la muestra de orina</field>
</record>
<!-- Aspecto de Orina -->
<record id="param_urine_appearance" model="lims.analysis.parameter">
<field name="code">U-ASP</field>
<field name="name">Aspecto</field>
<field name="value_type">selection</field>
<field name="selection_values">Transparente,Ligeramente turbio,Turbio,Muy turbio</field>
<field name="description">Aspecto de la muestra de orina</field>
</record>
<!-- pH de Orina -->
<record id="param_urine_ph" model="lims.analysis.parameter">
<field name="code">U-PH</field>
<field name="name">pH</field>
<field name="value_type">numeric</field>
<field name="unit">unidades</field>
<field name="description">pH de la orina</field>
</record>
<!-- Densidad de Orina -->
<record id="param_urine_density" model="lims.analysis.parameter">
<field name="code">U-DENS</field>
<field name="name">Densidad</field>
<field name="value_type">numeric</field>
<field name="unit">g/mL</field>
<field name="description">Densidad específica de la orina</field>
</record>
<!-- Proteínas en Orina -->
<record id="param_urine_protein" model="lims.analysis.parameter">
<field name="code">U-PROT</field>
<field name="name">Proteínas</field>
<field name="value_type">selection</field>
<field name="selection_values">Negativo,Trazas,+,++,+++,++++</field>
<field name="description">Presencia de proteínas en orina</field>
</record>
<!-- Glucosa en Orina -->
<record id="param_urine_glucose" model="lims.analysis.parameter">
<field name="code">U-GLU</field>
<field name="name">Glucosa</field>
<field name="value_type">selection</field>
<field name="selection_values">Negativo,Trazas,+,++,+++,++++</field>
<field name="description">Presencia de glucosa en orina</field>
</record>
<!-- Sangre en Orina -->
<record id="param_urine_blood" model="lims.analysis.parameter">
<field name="code">U-SANG</field>
<field name="name">Sangre</field>
<field name="value_type">selection</field>
<field name="selection_values">Negativo,Trazas,+,++,+++</field>
<field name="description">Presencia de sangre en orina</field>
</record>
<!-- Leucocitos en Orina -->
<record id="param_urine_leukocytes" model="lims.analysis.parameter">
<field name="code">U-LEU</field>
<field name="name">Leucocitos</field>
<field name="value_type">numeric</field>
<field name="unit">por campo</field>
<field name="description">Leucocitos en sedimento urinario</field>
</record>
<!-- Bacterias en Orina -->
<record id="param_urine_bacteria" model="lims.analysis.parameter">
<field name="code">U-BACT</field>
<field name="name">Bacterias</field>
<field name="value_type">selection</field>
<field name="selection_values">Escasas,Moderadas,Abundantes</field>
<field name="description">Presencia de bacterias en orina</field>
</record>
<!-- Parámetros de Microbiología -->
<!-- Cultivo -->
<record id="param_culture_result" model="lims.analysis.parameter">
<field name="code">CULT</field>
<field name="name">Resultado del Cultivo</field>
<field name="value_type">selection</field>
<field name="selection_values">Negativo,Positivo</field>
<field name="description">Resultado del cultivo microbiológico</field>
</record>
<!-- Microorganismo Aislado -->
<record id="param_isolated_organism" model="lims.analysis.parameter">
<field name="code">MICRO</field>
<field name="name">Microorganismo Aislado</field>
<field name="value_type">text</field>
<field name="description">Identificación del microorganismo</field>
</record>
<!-- Recuento de Colonias -->
<record id="param_colony_count" model="lims.analysis.parameter">
<field name="code">UFC</field>
<field name="name">Recuento de Colonias</field>
<field name="value_type">text</field>
<field name="description">UFC/mL (Unidades Formadoras de Colonias)</field>
</record>
<!-- Parámetros de Coagulación -->
<!-- Tiempo de Protrombina -->
<record id="param_pt" model="lims.analysis.parameter">
<field name="code">TP</field>
<field name="name">Tiempo de Protrombina</field>
<field name="value_type">numeric</field>
<field name="unit">segundos</field>
<field name="description">Tiempo de coagulación PT</field>
</record>
<!-- INR -->
<record id="param_inr" model="lims.analysis.parameter">
<field name="code">INR</field>
<field name="name">INR</field>
<field name="value_type">numeric</field>
<field name="unit">ratio</field>
<field name="description">Índice Internacional Normalizado</field>
</record>
<!-- Tiempo de Tromboplastina Parcial -->
<record id="param_ptt" model="lims.analysis.parameter">
<field name="code">TTP</field>
<field name="name">Tiempo de Tromboplastina Parcial</field>
<field name="value_type">numeric</field>
<field name="unit">segundos</field>
<field name="description">Tiempo de coagulación PTT</field>
</record>
<!-- Parámetros de Inmunología -->
<!-- HIV -->
<record id="param_hiv" model="lims.analysis.parameter">
<field name="code">HIV</field>
<field name="name">HIV 1/2</field>
<field name="value_type">selection</field>
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
<field name="description">Anticuerpos anti-HIV</field>
</record>
<!-- Hepatitis B -->
<record id="param_hbsag" model="lims.analysis.parameter">
<field name="code">HBsAg</field>
<field name="name">Antígeno de Superficie Hepatitis B</field>
<field name="value_type">selection</field>
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
<field name="description">HBsAg</field>
</record>
<!-- Hepatitis C -->
<record id="param_hcv" model="lims.analysis.parameter">
<field name="code">HCV</field>
<field name="name">Anticuerpos Hepatitis C</field>
<field name="value_type">selection</field>
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
<field name="description">Anti-HCV</field>
</record>
<!-- VDRL -->
<record id="param_vdrl" model="lims.analysis.parameter">
<field name="code">VDRL</field>
<field name="name">VDRL</field>
<field name="value_type">selection</field>
<field name="selection_values">No Reactivo,Reactivo</field>
<field name="description">Prueba de sífilis VDRL</field>
</record>
<!-- Test de Embarazo -->
<record id="param_pregnancy" model="lims.analysis.parameter">
<field name="code">HCG</field>
<field name="name">Prueba de Embarazo</field>
<field name="value_type">selection</field>
<field name="selection_values">Negativo,Positivo</field>
<field name="description">Beta-HCG cualitativa</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,374 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Rangos para Hemoglobina -->
<record id="range_hgb_male_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_hemoglobin"/>
<field name="name">Hombre adulto</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">13.5</field>
<field name="normal_max">17.5</field>
<field name="critical_min">7.0</field>
<field name="critical_max">20.0</field>
</record>
<record id="range_hgb_female_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_hemoglobin"/>
<field name="name">Mujer adulta</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="pregnant">False</field>
<field name="normal_min">12.0</field>
<field name="normal_max">15.5</field>
<field name="critical_min">7.0</field>
<field name="critical_max">20.0</field>
</record>
<record id="range_hgb_female_pregnant" model="lims.parameter.range">
<field name="parameter_id" ref="param_hemoglobin"/>
<field name="name">Mujer embarazada</field>
<field name="gender">female</field>
<field name="age_min">15</field>
<field name="age_max">50</field>
<field name="pregnant">True</field>
<field name="normal_min">11.0</field>
<field name="normal_max">14.0</field>
<field name="critical_min">7.0</field>
<field name="critical_max">20.0</field>
</record>
<record id="range_hgb_child" model="lims.parameter.range">
<field name="parameter_id" ref="param_hemoglobin"/>
<field name="name">Niños 2-12 años</field>
<field name="gender">both</field>
<field name="age_min">2</field>
<field name="age_max">12</field>
<field name="normal_min">11.5</field>
<field name="normal_max">14.5</field>
<field name="critical_min">7.0</field>
<field name="critical_max">20.0</field>
</record>
<!-- Rangos para Hematocrito -->
<record id="range_hct_male_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_hematocrit"/>
<field name="name">Hombre adulto</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">41</field>
<field name="normal_max">53</field>
<field name="critical_min">20</field>
<field name="critical_max">60</field>
</record>
<record id="range_hct_female_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_hematocrit"/>
<field name="name">Mujer adulta</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">36</field>
<field name="normal_max">46</field>
<field name="critical_min">20</field>
<field name="critical_max">60</field>
</record>
<!-- Rangos para Glóbulos Rojos -->
<record id="range_rbc_male_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_rbc"/>
<field name="name">Hombre adulto</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">4.5</field>
<field name="normal_max">5.9</field>
</record>
<record id="range_rbc_female_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_rbc"/>
<field name="name">Mujer adulta</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">4.1</field>
<field name="normal_max">5.1</field>
</record>
<!-- Rangos para Glóbulos Blancos -->
<record id="range_wbc_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_wbc"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">4.5</field>
<field name="normal_max">11.0</field>
<field name="critical_min">2.0</field>
<field name="critical_max">30.0</field>
</record>
<record id="range_wbc_child" model="lims.parameter.range">
<field name="parameter_id" ref="param_wbc"/>
<field name="name">Niño</field>
<field name="gender">both</field>
<field name="age_min">2</field>
<field name="age_max">17</field>
<field name="normal_min">5.0</field>
<field name="normal_max">15.0</field>
<field name="critical_min">2.0</field>
<field name="critical_max">30.0</field>
</record>
<!-- Rangos para Plaquetas -->
<record id="range_platelets_all" model="lims.parameter.range">
<field name="parameter_id" ref="param_platelets"/>
<field name="name">Todos</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">150</field>
<field name="normal_max">400</field>
<field name="critical_min">50</field>
<field name="critical_max">1000</field>
</record>
<!-- Rangos para Neutrófilos -->
<record id="range_neutrophils_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_neutrophils"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">45</field>
<field name="normal_max">70</field>
</record>
<!-- Rangos para Linfocitos -->
<record id="range_lymphocytes_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_lymphocytes"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">20</field>
<field name="normal_max">45</field>
</record>
<!-- Rangos para Glucosa -->
<record id="range_glucose_fasting" model="lims.parameter.range">
<field name="parameter_id" ref="param_glucose"/>
<field name="name">Ayunas</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">70</field>
<field name="normal_max">100</field>
<field name="critical_min">40</field>
<field name="critical_max">500</field>
<field name="interpretation">Valores normales en ayunas. Prediabetes: 100-125 mg/dL. Diabetes: ≥126 mg/dL</field>
</record>
<!-- Rangos para Creatinina -->
<record id="range_creatinine_male" model="lims.parameter.range">
<field name="parameter_id" ref="param_creatinine"/>
<field name="name">Hombre adulto</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">0.7</field>
<field name="normal_max">1.3</field>
<field name="critical_max">6.0</field>
</record>
<record id="range_creatinine_female" model="lims.parameter.range">
<field name="parameter_id" ref="param_creatinine"/>
<field name="name">Mujer adulta</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">0.6</field>
<field name="normal_max">1.1</field>
<field name="critical_max">6.0</field>
</record>
<!-- Rangos para Urea -->
<record id="range_urea_adult" model="lims.parameter.range">
<field name="parameter_id" ref="param_urea"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">15</field>
<field name="normal_max">45</field>
<field name="critical_max">100</field>
</record>
<!-- Rangos para Colesterol Total -->
<record id="range_cholesterol_total" model="lims.parameter.range">
<field name="parameter_id" ref="param_cholesterol_total"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">0</field>
<field name="normal_max">200</field>
<field name="interpretation">Deseable: &lt;200 mg/dL. Límite alto: 200-239 mg/dL. Alto: ≥240 mg/dL</field>
</record>
<!-- Rangos para HDL -->
<record id="range_hdl_male" model="lims.parameter.range">
<field name="parameter_id" ref="param_cholesterol_hdl"/>
<field name="name">Hombre</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">40</field>
<field name="normal_max">100</field>
</record>
<record id="range_hdl_female" model="lims.parameter.range">
<field name="parameter_id" ref="param_cholesterol_hdl"/>
<field name="name">Mujer</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">50</field>
<field name="normal_max">100</field>
</record>
<!-- Rangos para LDL -->
<record id="range_ldl_all" model="lims.parameter.range">
<field name="parameter_id" ref="param_cholesterol_ldl"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">0</field>
<field name="normal_max">100</field>
<field name="interpretation">Óptimo: &lt;100 mg/dL. Casi óptimo: 100-129 mg/dL. Límite alto: 130-159 mg/dL. Alto: 160-189 mg/dL. Muy alto: ≥190 mg/dL</field>
</record>
<!-- Rangos para Triglicéridos -->
<record id="range_triglycerides_all" model="lims.parameter.range">
<field name="parameter_id" ref="param_triglycerides"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">0</field>
<field name="normal_max">150</field>
<field name="critical_max">500</field>
<field name="interpretation">Normal: &lt;150 mg/dL. Límite alto: 150-199 mg/dL. Alto: 200-499 mg/dL. Muy alto: ≥500 mg/dL</field>
</record>
<!-- Rangos para ALT -->
<record id="range_alt_male" model="lims.parameter.range">
<field name="parameter_id" ref="param_alt"/>
<field name="name">Hombre</field>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">10</field>
<field name="normal_max">40</field>
<field name="critical_max">1000</field>
</record>
<record id="range_alt_female" model="lims.parameter.range">
<field name="parameter_id" ref="param_alt"/>
<field name="name">Mujer</field>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">10</field>
<field name="normal_max">35</field>
<field name="critical_max">1000</field>
</record>
<!-- Rangos para AST -->
<record id="range_ast_all" model="lims.parameter.range">
<field name="parameter_id" ref="param_ast"/>
<field name="name">Adulto</field>
<field name="gender">both</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="normal_min">10</field>
<field name="normal_max">40</field>
<field name="critical_max">1000</field>
</record>
<!-- Rangos para pH de Orina -->
<record id="range_urine_ph" model="lims.parameter.range">
<field name="parameter_id" ref="param_urine_ph"/>
<field name="name">Normal</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">4.5</field>
<field name="normal_max">8.0</field>
</record>
<!-- Rangos para Densidad de Orina -->
<record id="range_urine_density" model="lims.parameter.range">
<field name="parameter_id" ref="param_urine_density"/>
<field name="name">Normal</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">1.003</field>
<field name="normal_max">1.030</field>
</record>
<!-- Rangos para Leucocitos en Orina -->
<record id="range_urine_leukocytes" model="lims.parameter.range">
<field name="parameter_id" ref="param_urine_leukocytes"/>
<field name="name">Normal</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">0</field>
<field name="normal_max">5</field>
</record>
<!-- Rangos para Tiempo de Protrombina -->
<record id="range_pt" model="lims.parameter.range">
<field name="parameter_id" ref="param_pt"/>
<field name="name">Normal</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">11</field>
<field name="normal_max">13.5</field>
<field name="critical_min">9</field>
<field name="critical_max">30</field>
</record>
<!-- Rangos para INR -->
<record id="range_inr_normal" model="lims.parameter.range">
<field name="parameter_id" ref="param_inr"/>
<field name="name">Sin anticoagulación</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">0.8</field>
<field name="normal_max">1.2</field>
</record>
<!-- Rangos para TTP -->
<record id="range_ptt" model="lims.parameter.range">
<field name="parameter_id" ref="param_ptt"/>
<field name="name">Normal</field>
<field name="gender">both</field>
<field name="age_min">0</field>
<field name="age_max">99</field>
<field name="normal_min">25</field>
<field name="normal_max">35</field>
<field name="critical_min">20</field>
<field name="critical_max">70</field>
</record>
</data>
</odoo>

View File

@ -19,25 +19,6 @@
</field>
</record>
<!-- Rangos de Referencia para Hemograma -->
<record id="range_hemograma_globulos_rojos_m" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_hemograma"/>
<field name="gender">male</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="min_value">4.5</field>
<field name="max_value">5.9</field>
<field name="unit_of_measure">millones/µL</field>
</record>
<record id="range_hemograma_globulos_rojos_f" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_hemograma"/>
<field name="gender">female</field>
<field name="age_min">18</field>
<field name="age_max">99</field>
<field name="min_value">4.0</field>
<field name="max_value">5.2</field>
<field name="unit_of_measure">millones/µL</field>
</record>
<!-- Análisis: Perfil Lipídico -->
<record id="analysis_perfil_lipidico" model="product.template">
@ -55,21 +36,6 @@
</field>
</record>
<!-- Rangos para Colesterol Total -->
<record id="range_colesterol_total" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">200</field>
<field name="unit_of_measure">mg/dL</field>
</record>
<!-- Rangos para Colesterol LDL -->
<record id="range_colesterol_ldl" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">100</field>
<field name="unit_of_measure">mg/dL</field>
</record>
<!-- Análisis: Glucosa -->

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from . import analysis_range
from . import analysis_parameter
from . import product_template_parameter
from . import parameter_range
from . import product
from . import partner
from . import sale_order

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class LimsAnalysisParameter(models.Model):
_name = 'lims.analysis.parameter'
_description = 'Catálogo de Parámetros de Laboratorio'
_order = 'name'
_rec_name = 'name'
name = fields.Char(
string='Nombre',
required=True,
help='Nombre descriptivo del parámetro (ej: Hemoglobina)'
)
code = fields.Char(
string='Código',
required=True,
help='Código único del parámetro (ej: HGB)'
)
value_type = fields.Selection([
('numeric', 'Numérico'),
('text', 'Texto'),
('boolean', 'Sí/No'),
('selection', 'Selección')
],
string='Tipo de Valor',
required=True,
default='numeric',
help='Tipo de dato que acepta este parámetro'
)
unit = fields.Char(
string='Unidad de Medida',
help='Unidad de medida del parámetro (ej: g/dL, mg/dL, %)'
)
selection_values = fields.Text(
string='Valores de Selección',
help='Para tipo "Selección", ingrese los valores posibles separados por comas'
)
description = fields.Text(
string='Descripción',
help='Descripción detallada del parámetro y su significado clínico'
)
active = fields.Boolean(
string='Activo',
default=True,
help='Si está desmarcado, el parámetro no estará disponible para nuevas configuraciones'
)
category_id = fields.Many2one(
'product.category',
string='Categoría',
domain="[('parent_id.name', '=', 'Análisis de Laboratorio')]",
help='Categoría del parámetro para agrupar en reportes'
)
# Relaciones
template_parameter_ids = fields.One2many(
'product.template.parameter',
'parameter_id',
string='Análisis que usan este parámetro'
)
range_ids = fields.One2many(
'lims.parameter.range',
'parameter_id',
string='Rangos de Referencia'
)
# Campos computados
analysis_count = fields.Integer(
string='Cantidad de Análisis',
compute='_compute_analysis_count',
store=True
)
@api.depends('template_parameter_ids')
def _compute_analysis_count(self):
for record in self:
record.analysis_count = len(record.template_parameter_ids)
@api.constrains('code')
def _check_code_unique(self):
for record in self:
if self.search_count([
('code', '=', record.code),
('id', '!=', record.id)
]) > 0:
raise ValidationError(f'El código "{record.code}" ya existe. Los códigos deben ser únicos.')
@api.constrains('value_type', 'selection_values')
def _check_selection_values(self):
for record in self:
if record.value_type == 'selection' and not record.selection_values:
raise ValidationError('Debe especificar los valores de selección para parámetros de tipo "Selección".')
@api.constrains('value_type', 'unit')
def _check_numeric_unit(self):
for record in self:
if record.value_type == 'numeric' and not record.unit:
raise ValidationError('Los parámetros numéricos deben tener una unidad de medida.')
def get_selection_list(self):
"""Devuelve la lista de valores de selección como una lista de Python"""
self.ensure_one()
if self.value_type == 'selection' and self.selection_values:
return [val.strip() for val in self.selection_values.split(',') if val.strip()]
return []
@api.model
def create(self, vals):
# Convertir código a mayúsculas
if 'code' in vals:
vals['code'] = vals['code'].upper()
return super(LimsAnalysisParameter, self).create(vals)
def write(self, vals):
# Convertir código a mayúsculas
if 'code' in vals:
vals['code'] = vals['code'].upper()
return super(LimsAnalysisParameter, self).write(vals)
def name_get(self):
result = []
for record in self:
name = f"[{record.code}] {record.name}"
if record.unit:
name += f" ({record.unit})"
result.append((record.id, name))
return result
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
args = args or []
if name:
args = ['|', ('code', operator, name), ('name', operator, name)] + args
return self._search(args, limit=limit, access_rights_uid=name_get_uid)

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class LimsAnalysisRange(models.Model):
_name = 'lims.analysis.range'
_description = 'Rangos de Referencia para Análisis Clínicos'
analysis_id = fields.Many2one(
'product.template',
string="Análisis",
required=True,
ondelete='cascade'
)
gender = fields.Selection([
('male', 'Masculino'),
('female', 'Femenino'),
('both', 'Ambos')
], string="Género", default='both')
age_min = fields.Integer(string="Edad Mínima", default=0)
age_max = fields.Integer(string="Edad Máxima", default=99)
min_value = fields.Float(string="Valor Mínimo")
max_value = fields.Float(string="Valor Máximo")
unit_of_measure = fields.Char(string="Unidad de Medida")

View File

@ -25,10 +25,27 @@ 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
)
parameter_code = fields.Char(
string='Código',
related='parameter_id.code',
store=True,
readonly=True
)
sequence = fields.Integer(
@ -36,12 +53,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,33 +76,63 @@ 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'
test_date = fields.Datetime(
related='test_id.create_date',
string='Fecha de la Prueba',
store=True
)
unit = fields.Char(
string='Unidad'
)
result_status = fields.Selection([
('normal', 'Normal'),
('abnormal', 'Anormal'),
('critical', 'Crítico')
], string='Estado', compute='_compute_result_status', store=True)
@api.depends('test_id', 'parameter_name')
def _compute_display_name(self):
@ -87,38 +143,146 @@ 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 = '' 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.depends('parameter_id', 'value_numeric', 'is_out_of_range', 'is_critical', 'parameter_value_type')
def _compute_result_status(self):
"""Calcula el estado visual del resultado."""
for record in self:
if record.parameter_value_type != 'numeric':
record.result_status = 'normal'
elif record.is_critical:
record.result_status = 'critical'
elif record.is_out_of_range:
record.result_status = 'abnormal'
else:
record.result_status = 'normal'
@api.constrains('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
def _check_value_type(self):
"""Asegura que el valor ingresado corresponda al tipo de parámetro."""
for record in self:
if not record.parameter_id:
continue
value_type = record.parameter_value_type
has_value = False
if value_type == 'numeric':
has_value = record.value_numeric not in [False, 0.0]
if record.value_text or record.value_selection:
raise ValidationError(
_('Para parámetros numéricos solo se debe ingresar el valor numérico.')
)
elif value_type == 'text':
has_value = bool(record.value_text)
if (record.value_numeric not in [False, 0.0]) or record.value_selection or record.value_boolean:
raise ValidationError(
_('Para parámetros de texto solo se debe ingresar el valor de texto.')
)
elif value_type == 'selection':
has_value = bool(record.value_selection)
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_boolean:
raise ValidationError(
_('Para parámetros de selección solo se debe elegir una opción.')
)
elif value_type == 'boolean':
has_value = True # Boolean siempre tiene valor (True o False)
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_selection:
raise ValidationError(
_('Para parámetros Sí/No solo se debe marcar el checkbox.')
)
# Solo requerir valor si la prueba no está en borrador
if not has_value and record.parameter_id and record.test_id.state != 'draft':
raise ValidationError(
_('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

View File

@ -146,7 +146,46 @@ class LimsTest(models.Model):
for vals in vals_list:
if vals.get('name', 'Nuevo') == 'Nuevo':
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
return super().create(vals_list)
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."""
for test in self:
if test.result_ids:
# Si ya tiene resultados, no generar nuevos
continue
# Obtener el product.template del análisis
product_tmpl = test.product_id.product_tmpl_id
# Buscar los parámetros configurados para este análisis
template_parameters = self.env['product.template.parameter'].search([
('product_tmpl_id', '=', product_tmpl.id)
], order='sequence, id')
# Crear una línea de resultado por cada parámetro
for param_config in template_parameters:
result_vals = {
'test_id': test.id,
'parameter_id': param_config.parameter_id.id,
'sequence': param_config.sequence,
'notes': param_config.instructions or ''
}
# Inicializar valores según el tipo
if param_config.parameter_value_type == 'boolean':
result_vals['value_boolean'] = False
self.env['lims.result'].create(result_vals)
if template_parameters:
_logger.info(f"Generados {len(template_parameters)} resultados para la prueba {test.name}")
else:
_logger.warning(f"No se encontraron parámetros configurados para el análisis {product_tmpl.name}")
def action_start_process(self):
"""Inicia el proceso de análisis."""
@ -236,6 +275,28 @@ class LimsTest(models.Model):
return True
def action_regenerate_results(self):
"""Regenera los resultados basados en la configuración actual del análisis."""
self.ensure_one()
if self.state not in ['draft', 'in_process']:
raise UserError(_('Solo se pueden regenerar resultados en pruebas en borrador o en proceso.'))
# Confirmar con el usuario
if self.result_ids:
# En producción, aquí se mostraría un wizard de confirmación
# Por ahora, eliminamos los resultados existentes
self.result_ids.unlink()
# Regenerar
self._generate_test_results()
self.message_post(
body=_('Resultados regenerados por %s') % self.env.user.name,
subject=_('Resultados Regenerados')
)
return True
def action_draft(self):
"""Regresa a borrador."""
self.ensure_one()

View File

@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class LimsParameterRange(models.Model):
_name = 'lims.parameter.range'
_description = 'Rangos de Referencia por Parámetro'
_order = 'parameter_id, gender desc, age_min'
_rec_name = 'name'
parameter_id = fields.Many2one(
'lims.analysis.parameter',
string='Parámetro',
required=True,
ondelete='cascade',
help='Parámetro al que aplica este rango de referencia'
)
name = fields.Char(
string='Descripción',
compute='_compute_name',
store=True,
help='Descripción automática del rango'
)
# Condiciones
gender = fields.Selection([
('male', 'Masculino'),
('female', 'Femenino'),
('both', 'Ambos')
],
string='Género',
default='both',
required=True,
help='Género al que aplica este rango'
)
age_min = fields.Integer(
string='Edad Mínima',
default=0,
help='Edad mínima en años (inclusive)'
)
age_max = fields.Integer(
string='Edad Máxima',
default=150,
help='Edad máxima en años (inclusive)'
)
pregnant = fields.Boolean(
string='Embarazada',
default=False,
help='Marcar si este rango es específico para mujeres embarazadas'
)
# Valores de referencia
normal_min = fields.Float(
string='Valor Normal Mínimo',
help='Límite inferior del rango normal'
)
normal_max = fields.Float(
string='Valor Normal Máximo',
help='Límite superior del rango normal'
)
critical_min = fields.Float(
string='Valor Crítico Mínimo',
help='Por debajo de este valor es crítico'
)
critical_max = fields.Float(
string='Valor Crítico Máximo',
help='Por encima de este valor es crítico'
)
# Información adicional
interpretation = fields.Text(
string='Interpretación',
help='Guía de interpretación clínica para este rango'
)
# Campos relacionados para facilitar búsquedas
parameter_name = fields.Char(
related='parameter_id.name',
string='Nombre del Parámetro',
store=True,
readonly=True
)
parameter_code = fields.Char(
related='parameter_id.code',
string='Código del Parámetro',
store=True,
readonly=True
)
parameter_unit = fields.Char(
related='parameter_id.unit',
string='Unidad',
readonly=True
)
@api.depends('parameter_id', 'gender', 'age_min', 'age_max', 'pregnant')
def _compute_name(self):
for record in self:
if not record.parameter_id:
record.name = 'Nuevo rango'
continue
parts = [record.parameter_id.name]
# Agregar género si no es ambos
if record.gender != 'both':
gender_name = dict(self._fields['gender'].selection).get(record.gender, '')
parts.append(gender_name)
# Agregar rango de edad
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")
# Agregar indicador de embarazo
if record.pregnant:
parts.append('Embarazada')
record.name = ' - '.join(parts)
@api.constrains('age_min', 'age_max')
def _check_age_range(self):
for record in self:
if record.age_min < 0:
raise ValidationError('La edad mínima no puede ser negativa.')
if record.age_max < record.age_min:
raise ValidationError('La edad máxima debe ser mayor o igual a la edad mínima.')
if record.age_max > 150:
raise ValidationError('La edad máxima no puede ser mayor a 150 años.')
@api.constrains('normal_min', 'normal_max')
def _check_normal_range(self):
for record in self:
if record.normal_min and record.normal_max and record.normal_min > record.normal_max:
raise ValidationError('El valor normal mínimo debe ser menor o igual al valor normal máximo.')
@api.constrains('critical_min', 'critical_max', 'normal_min', 'normal_max')
def _check_critical_range(self):
for record in self:
# Validar que crítico mínimo sea menor que normal mínimo
if record.critical_min and record.normal_min and record.critical_min > record.normal_min:
raise ValidationError('El valor crítico mínimo debe ser menor o igual al valor normal mínimo.')
# Validar que crítico máximo sea mayor que normal máximo
if record.critical_max and record.normal_max and record.critical_max < record.normal_max:
raise ValidationError('El valor crítico máximo debe ser mayor o igual al valor normal máximo.')
@api.constrains('gender', 'pregnant')
def _check_pregnant_gender(self):
for record in self:
if record.pregnant and record.gender == 'male':
raise ValidationError('No se puede marcar "Embarazada" para rangos masculinos.')
@api.constrains('parameter_id', 'gender', 'age_min', 'age_max', 'pregnant')
def _check_unique_range(self):
for record in self:
# Buscar rangos duplicados
domain = [
('parameter_id', '=', record.parameter_id.id),
('gender', '=', record.gender),
('age_min', '=', record.age_min),
('age_max', '=', record.age_max),
('pregnant', '=', record.pregnant),
('id', '!=', record.id)
]
if self.search_count(domain) > 0:
raise ValidationError('Ya existe un rango con estas mismas condiciones para este parámetro.')
def is_value_normal(self, value):
"""Verifica si un valor está dentro del rango normal"""
self.ensure_one()
if not value or not self.normal_min or not self.normal_max:
return True
return self.normal_min <= value <= self.normal_max
def is_value_critical(self, value):
"""Verifica si un valor está en rango crítico"""
self.ensure_one()
if not value:
return False
# Crítico por debajo
if self.critical_min and value < self.critical_min:
return True
# Crítico por encima
if self.critical_max and value > self.critical_max:
return True
return False
def get_value_status(self, value):
"""Devuelve el estado del valor: 'normal', 'abnormal', 'critical'"""
self.ensure_one()
if not value:
return 'normal'
if self.is_value_critical(value):
return 'critical'
elif not self.is_value_normal(value):
return 'abnormal'
else:
return 'normal'

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from datetime import date
from dateutil.relativedelta import relativedelta
class ResPartner(models.Model):
_inherit = 'res.partner'
@ -17,6 +20,19 @@ class ResPartner(models.Model):
('female', 'Femenino'),
('other', 'Otro')
], string="Género")
# Nuevos campos para el cálculo de rangos
age = fields.Integer(
string="Edad",
compute='_compute_age',
store=False,
help="Edad calculada en años basada en la fecha de nacimiento"
)
is_pregnant = fields.Boolean(
string="Embarazada",
help="Marcar si la paciente está embarazada (solo aplica para género femenino)"
)
is_doctor = fields.Boolean(string="Es Médico")
doctor_license = fields.Char(string="Licencia Médica", copy=False)
@ -25,6 +41,25 @@ class ResPartner(models.Model):
('patient_identifier_unique', 'unique(patient_identifier)', 'El identificador del paciente debe ser único.'),
('doctor_license_unique', 'unique(doctor_license)', 'La licencia médica debe ser única.')
]
@api.depends('birthdate_date')
def _compute_age(self):
"""Calcula la edad en años basada en la fecha de nacimiento"""
today = date.today()
for partner in self:
if partner.birthdate_date:
# Calcular diferencia usando relativedelta para precisión
delta = relativedelta(today, partner.birthdate_date)
partner.age = delta.years
else:
partner.age = 0
@api.constrains('is_pregnant', 'gender')
def _check_pregnant_gender(self):
"""Valida que solo pacientes de género femenino puedan estar embarazadas"""
for partner in self:
if partner.is_pregnant and partner.gender != 'female':
raise ValidationError('Solo las pacientes de género femenino pueden estar marcadas como embarazadas.')
@api.model_create_multi
def create(self, vals_list):
@ -32,3 +67,25 @@ class ResPartner(models.Model):
if vals.get('is_patient') and not vals.get('patient_identifier'):
vals['patient_identifier'] = self.env['ir.sequence'].next_by_code('res.partner.patient_identifier')
return super(ResPartner, self).create(vals_list)
def get_age_at_date(self, target_date=None):
"""
Calcula la edad del paciente en una fecha específica.
:param target_date: Fecha en la que calcular la edad. Si es None, usa la fecha actual.
:return: Edad en años
"""
self.ensure_one()
if not self.birthdate_date:
return 0
if not target_date:
target_date = date.today()
elif isinstance(target_date, str):
target_date = fields.Date.from_string(target_date)
if target_date < self.birthdate_date:
return 0
delta = relativedelta(target_date, self.birthdate_date)
return delta.years

View File

@ -22,10 +22,11 @@ class ProductTemplate(models.Model):
string="Especificaciones Técnicas"
)
value_range_ids = fields.One2many(
'lims.analysis.range',
'analysis_id',
string="Rangos de Referencia"
parameter_ids = fields.One2many(
'product.template.parameter',
'product_tmpl_id',
string="Parámetros del Análisis",
help="Parámetros que se medirán en este análisis"
)
is_sample_type = fields.Boolean(

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class ProductTemplateParameter(models.Model):
_name = 'product.template.parameter'
_description = 'Parámetros por Análisis'
_order = 'product_tmpl_id, sequence, id'
_rec_name = 'parameter_id'
product_tmpl_id = fields.Many2one(
'product.template',
string='Análisis',
required=True,
ondelete='cascade',
domain=[('is_analysis', '=', True)],
help='Análisis al que pertenece este parámetro'
)
parameter_id = fields.Many2one(
'lims.analysis.parameter',
string='Parámetro',
required=True,
ondelete='restrict',
help='Parámetro de laboratorio'
)
sequence = fields.Integer(
string='Secuencia',
default=10,
help='Orden en que aparecerá el parámetro en los resultados'
)
required = fields.Boolean(
string='Obligatorio',
default=True,
help='Si está marcado, este parámetro debe tener un valor en los resultados'
)
instructions = fields.Text(
string='Instrucciones específicas',
help='Instrucciones especiales para este parámetro en este análisis'
)
# Campos relacionados para facilitar búsquedas y vistas
parameter_name = fields.Char(
related='parameter_id.name',
string='Nombre del Parámetro',
store=True,
readonly=True
)
parameter_code = fields.Char(
related='parameter_id.code',
string='Código',
store=True,
readonly=True
)
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
)
_sql_constraints = [
('unique_param_per_analysis',
'UNIQUE(product_tmpl_id, parameter_id)',
'El parámetro ya está configurado para este análisis. Cada parámetro solo puede aparecer una vez por análisis.')
]
@api.constrains('sequence')
def _check_sequence(self):
for record in self:
if record.sequence < 0:
raise ValidationError('La secuencia debe ser un número positivo.')
def name_get(self):
result = []
for record in self:
name = f"{record.product_tmpl_id.name} - [{record.parameter_code}] {record.parameter_name}"
if record.parameter_unit:
name += f" ({record.parameter_unit})"
result.append((record.id, name))
return result
@api.model
def create(self, vals):
# Si no se especifica secuencia, asignar la siguiente disponible
if 'sequence' not in vals and 'product_tmpl_id' in vals:
max_sequence = self.search([
('product_tmpl_id', '=', vals['product_tmpl_id'])
], order='sequence desc', limit=1).sequence
vals['sequence'] = (max_sequence or 0) + 10
return super(ProductTemplateParameter, self).create(vals)
def copy_data(self, default=None):
default = dict(default or {})
# Al duplicar, incrementar la secuencia
default['sequence'] = self.sequence + 10
return super(ProductTemplateParameter, self).copy_data(default)

View File

@ -1,5 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,base.group_user,1,1,1,1
access_lims_analysis_parameter_user,lims.analysis.parameter.user,model_lims_analysis_parameter,base.group_user,1,0,0,0
access_lims_analysis_parameter_manager,lims.analysis.parameter.manager,model_lims_analysis_parameter,group_lims_admin,1,1,1,1
access_product_template_parameter_user,product.template.parameter.user,model_product_template_parameter,base.group_user,1,0,0,0
access_product_template_parameter_manager,product.template.parameter.manager,model_product_template_parameter,group_lims_admin,1,1,1,1
access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_range,base.group_user,1,0,0,0
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,1,1,1,1
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0
access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
access_lims_test_user,lims.test.user,model_lims_test,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_lims_analysis_range_user access_lims_analysis_parameter_user lims.analysis.range.user lims.analysis.parameter.user model_lims_analysis_range model_lims_analysis_parameter base.group_user 1 1 0 1 0 1 0
3 access_lims_analysis_parameter_manager lims.analysis.parameter.manager model_lims_analysis_parameter group_lims_admin 1 1 1 1
4 access_product_template_parameter_user product.template.parameter.user model_product_template_parameter base.group_user 1 0 0 0
5 access_product_template_parameter_manager product.template.parameter.manager model_product_template_parameter group_lims_admin 1 1 1 1
6 access_lims_parameter_range_user lims.parameter.range.user model_lims_parameter_range base.group_user 1 0 0 0
7 access_lims_parameter_range_manager lims.parameter.range.manager model_lims_parameter_range group_lims_admin 1 1 1 1
8 access_sale_order_receptionist sale.order.receptionist sale.model_sale_order group_lims_receptionist 1 1 1 0
9 access_stock_lot_user stock.lot.user stock.model_stock_lot base.group_user 1 1 1 1
10 access_lims_test_user lims.test.user model_lims_test base.group_user 1 1 1 1

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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, '')

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_lims_analysis_parameter_form" model="ir.ui.view">
<field name="name">lims.analysis.parameter.form</field>
<field name="model">lims.analysis.parameter</field>
<field name="arch" type="xml">
<form string="Parámetro de Análisis">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(lims_management.action_product_template_parameter)d"
type="action"
class="oe_stat_button"
icon="fa-flask"
context="{'search_default_parameter_id': id}">
<field name="analysis_count" widget="statinfo" string="Análisis"/>
</button>
<button name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive">
<field name="active" widget="boolean_button"
options="{'terminology': 'archive'}"/>
</button>
</div>
<widget name="web_ribbon" title="Archivado" bg_color="bg-danger" invisible="active"/>
<div class="oe_title">
<h1>
<field name="code" placeholder="Código" class="oe_inline"/>
</h1>
<h2>
<field name="name" placeholder="Nombre del parámetro" class="oe_inline"/>
</h2>
</div>
<group>
<group string="Información General">
<field name="value_type"/>
<field name="unit" invisible="value_type != 'numeric'"/>
<field name="selection_values"
invisible="value_type != 'selection'"
placeholder="Positivo, Negativo, No concluyente"/>
<field name="active" invisible="1"/>
</group>
<group string="Detalles">
<field name="description" widget="text" nolabel="1" colspan="2"/>
</group>
</group>
<notebook>
<page string="Rangos de Referencia" name="ranges">
<field name="range_ids" context="{'default_parameter_id': id}">
<list editable="bottom">
<field name="name"/>
<field name="gender"/>
<field name="age_min"/>
<field name="age_max"/>
<field name="pregnant" optional="show"/>
<field name="normal_min"/>
<field name="normal_max"/>
<field name="critical_min" optional="show"/>
<field name="critical_max" optional="show"/>
</list>
</field>
</page>
<page string="Análisis Configurados" name="analysis">
<field name="template_parameter_ids">
<list>
<field name="product_tmpl_id"/>
<field name="sequence"/>
<field name="required"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- List View -->
<record id="view_lims_analysis_parameter_list" model="ir.ui.view">
<field name="name">lims.analysis.parameter.list</field>
<field name="model">lims.analysis.parameter</field>
<field name="arch" type="xml">
<list string="Parámetros de Análisis">
<field name="code"/>
<field name="name"/>
<field name="value_type"/>
<field name="unit" optional="show"/>
<field name="analysis_count" optional="show"/>
<field name="active" invisible="1"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="view_lims_analysis_parameter_search" model="ir.ui.view">
<field name="name">lims.analysis.parameter.search</field>
<field name="model">lims.analysis.parameter</field>
<field name="arch" type="xml">
<search string="Buscar Parámetros">
<field name="name" string="Parámetro"
filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
<field name="code"/>
<filter string="Numéricos" name="numeric" domain="[('value_type', '=', 'numeric')]"/>
<filter string="Texto" name="text" domain="[('value_type', '=', 'text')]"/>
<filter string="Sí/No" name="boolean" domain="[('value_type', '=', 'boolean')]"/>
<filter string="Selección" name="selection" domain="[('value_type', '=', 'selection')]"/>
<separator/>
<filter string="Activos" name="active" domain="[('active', '=', True)]"/>
<filter string="Archivados" name="archived" domain="[('active', '=', False)]"/>
<group expand="0" string="Agrupar por">
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'value_type'}"/>
<filter string="Estado" name="group_active" context="{'group_by': 'active'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_lims_analysis_parameter" model="ir.actions.act_window">
<field name="name">Parámetros de Análisis</field>
<field name="res_model">lims.analysis.parameter</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_lims_analysis_parameter_search"/>
<field name="context">{'search_default_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear nuevo parámetro
</p>
<p>
Los parámetros definen qué valores se pueden registrar en los análisis de laboratorio.
Cada parámetro tiene un tipo de dato, unidad de medida y rangos de referencia.
</p>
</field>
</record>
</odoo>

View File

@ -1,22 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Vista de Lista para Rangos de Referencia -->
<record id="view_lims_analysis_range_tree" model="ir.ui.view">
<field name="name">lims.analysis.range.tree</field>
<field name="model">lims.analysis.range</field>
<field name="arch" type="xml">
<list string="Rangos de Referencia" editable="bottom">
<field name="gender"/>
<field name="age_min"/>
<field name="age_max"/>
<field name="min_value"/>
<field name="max_value"/>
<field name="unit_of_measure"/>
</list>
</field>
</record>
<!-- Hereda la vista de formulario de producto para añadir la pestaña de Análisis -->
<record id="view_product_template_form_lims" model="ir.ui.view">
<field name="name">product.template.form.lims</field>
@ -36,9 +20,21 @@
<field name="technical_specifications"/>
</group>
</group>
<separator string="Rangos de Referencia"/>
<field name="value_range_ids"
view_id="lims_management.view_lims_analysis_range_tree"/>
<separator string="Parámetros del Análisis"/>
<field name="parameter_ids"
context="{'default_product_tmpl_id': id}">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="parameter_id"
options="{'no_create': True}"
domain="[('active', '=', True)]"/>
<field name="parameter_code"/>
<field name="parameter_value_type"/>
<field name="parameter_unit"/>
<field name="required"/>
<field name="instructions" optional="show"/>
</list>
</field>
</page>
</xpath>
<!-- Añade el campo is_analysis cerca del nombre del producto para fácil acceso -->

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Specialized Form View for Bulk Result Entry -->
<record id="view_lims_test_result_entry_form" model="ir.ui.view">
<field name="name">lims.test.result.entry.form</field>
<field name="model">lims.test</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Ingreso Rápido de Resultados">
<header>
<field name="state" widget="statusbar" statusbar_visible="draft,in_process,result_entered,validated"/>
<button name="action_start_process" string="Iniciar Análisis"
type="object" class="oe_highlight"
invisible="state != 'draft'"/>
<button name="action_enter_results" string="Guardar Resultados"
type="object" class="oe_highlight"
invisible="state != 'in_process'"/>
<button name="action_validate" string="Validar Resultados"
type="object" class="oe_highlight"
invisible="state != 'result_entered'"
groups="lims_management.group_lims_admin"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
<h2>
<field name="patient_id" readonly="1"/>
</h2>
<h3>
<field name="product_id" readonly="1"/>
</h3>
</div>
<group>
<group>
<field name="sample_id" readonly="1"/>
<field name="technician_id" readonly="state != 'in_process'"/>
</group>
<group>
<field name="create_date" readonly="1"/>
<field name="validation_date" readonly="1"/>
</group>
</group>
<separator string="Ingreso de Resultados"/>
<field name="result_ids"
readonly="state in ['validated', 'cancelled']"
context="{'form_view_ref': 'lims_management.view_lims_result_form'}">
<list string="Resultados" editable="bottom" create="0" delete="0">
<field name="sequence" invisible="1"/>
<field name="parameter_id" readonly="1" force_save="1"/>
<field name="parameter_code" readonly="1"/>
<field name="parameter_value_type" invisible="1"/>
<!-- Entrada rápida de valores -->
<field name="value_numeric"
invisible="parameter_value_type != 'numeric'"
widget="float"
options="{'digits': [16, 4]}"
decoration-danger="is_critical"
decoration-warning="is_out_of_range and not is_critical"/>
<field name="value_text"
invisible="parameter_value_type != 'text'"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"
widget="selection"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"/>
<!-- Información de referencia -->
<field name="parameter_unit"
invisible="parameter_value_type != 'numeric'"
readonly="1"/>
<field name="applicable_range_id"
widget="many2one_tags"
readonly="1"
options="{'no_open': True}"/>
<!-- Indicadores -->
<field name="result_status"
widget="badge"
decoration-success="result_status == 'normal'"
decoration-warning="result_status == 'abnormal'"
decoration-danger="result_status == 'critical'"/>
<!-- Campos ocultos -->
<field name="is_out_of_range" invisible="1"/>
<field name="is_critical" invisible="1"/>
<!-- Notas rápidas -->
<field name="notes" optional="show"/>
</list>
</field>
<group string="Observaciones Generales" invisible="state == 'draft'">
<field name="notes" nolabel="1"
placeholder="Ingrese observaciones generales sobre la prueba..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action for Quick Result Entry -->
<record id="action_lims_result_entry" model="ir.actions.act_window">
<field name="name">Ingreso Rápido de Resultados</field>
<field name="res_model">lims.test</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="view_lims_test_result_entry_form"/>
<field name="search_view_id" ref="view_lims_test_search"/>
<field name="domain">[('state', 'in', ['in_process', 'result_entered'])]</field>
<field name="context">{'search_default_my_tests': 1, 'search_default_in_process': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay pruebas pendientes de resultados
</p>
<p>
Las pruebas aparecerán aquí cuando estén listas para
el ingreso de resultados.
</p>
</field>
</record>
<!-- Result Summary Dashboard -->
<record id="view_lims_result_pivot" model="ir.ui.view">
<field name="name">lims.result.pivot</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<pivot string="Análisis de Resultados">
<field name="parameter_id" type="row"/>
<field name="result_status" type="col"/>
<field name="test_id" type="measure"/>
</pivot>
</field>
</record>
<record id="view_lims_result_graph" model="ir.ui.view">
<field name="name">lims.result.graph</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<graph string="Distribución de Resultados" type="pie">
<field name="result_status"/>
<field name="test_id" type="measure"/>
</graph>
</field>
</record>
<!-- Action for Result Analysis -->
<record id="action_lims_result_analysis" model="ir.actions.act_window">
<field name="name">Análisis de Resultados</field>
<field name="res_model">lims.result</field>
<field name="view_mode">pivot,graph,list</field>
<field name="help" type="html">
<p>
Análisis estadístico de los resultados de laboratorio.
</p>
</field>
</record>
</odoo>

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View for lims.result -->
<record id="view_lims_result_form" model="ir.ui.view">
<field name="name">lims.result.form</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<form string="Resultado de Análisis">
<sheet>
<group>
<group string="Información del Test">
<field name="test_id" readonly="1"/>
<field name="patient_id" readonly="1"/>
<field name="test_date" readonly="1"/>
</group>
<group string="Parámetro">
<field name="parameter_id" readonly="1"/>
<field name="parameter_code" readonly="1"/>
<field name="parameter_value_type" invisible="1"/>
<field name="parameter_unit" invisible="parameter_value_type != 'numeric'"/>
</group>
</group>
<group string="Valor del Resultado">
<group>
<field name="value_numeric"
invisible="parameter_value_type != 'numeric'"
widget="float"
options="{'digits': [16, 4]}"
decoration-danger="is_out_of_range"
decoration-warning="is_critical"/>
<field name="value_text"
invisible="parameter_value_type != 'text'"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"
widget="selection"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"/>
</group>
<group>
<field name="is_out_of_range" readonly="1"/>
<field name="is_critical" readonly="1"/>
</group>
</group>
<group string="Rango de Referencia" invisible="parameter_value_type != 'numeric'">
<field name="applicable_range_id" readonly="1">
<form>
<group>
<field name="normal_min"/>
<field name="normal_max"/>
<field name="critical_min"/>
<field name="critical_max"/>
</group>
</form>
</field>
</group>
<group string="Observaciones">
<field name="notes" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- List View for lims.result -->
<record id="view_lims_result_list" model="ir.ui.view">
<field name="name">lims.result.list</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<list string="Resultados de Análisis" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="parameter_id" options="{'no_create': True, 'no_open': True}"/>
<field name="parameter_code" optional="show"/>
<field name="parameter_value_type" invisible="1"/>
<field name="value_numeric"
invisible="parameter_value_type != 'numeric'"
decoration-danger="is_out_of_range"
decoration-warning="is_critical"/>
<field name="value_text"
invisible="parameter_value_type != 'text'"/>
<field name="value_selection"
invisible="parameter_value_type != 'selection'"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"/>
<field name="parameter_unit"
invisible="parameter_value_type != 'numeric'"
optional="show"/>
<field name="is_out_of_range" invisible="1"/>
<field name="is_critical" invisible="1"/>
<field name="applicable_range_id" optional="hide"/>
<field name="notes" optional="show"/>
</list>
</field>
</record>
<!-- Search View for lims.result -->
<record id="view_lims_result_search" model="ir.ui.view">
<field name="name">lims.result.search</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<search string="Buscar Resultados">
<field name="test_id"/>
<field name="parameter_id"/>
<field name="parameter_name"/>
<field name="patient_id"/>
<separator/>
<filter string="Fuera de Rango" name="out_of_range"
domain="[('is_out_of_range', '=', True)]"/>
<filter string="Críticos" name="critical"
domain="[('is_critical', '=', True)]"/>
<separator/>
<filter string="Numéricos" name="numeric"
domain="[('parameter_value_type', '=', 'numeric')]"/>
<filter string="Texto" name="text"
domain="[('parameter_value_type', '=', 'text')]"/>
<filter string="Selección" name="selection"
domain="[('parameter_value_type', '=', 'selection')]"/>
<filter string="Sí/No" name="boolean"
domain="[('parameter_value_type', '=', 'boolean')]"/>
<group expand="0" string="Agrupar por">
<filter string="Test" name="group_test" context="{'group_by': 'test_id'}"/>
<filter string="Parámetro" name="group_parameter" context="{'group_by': 'parameter_id'}"/>
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'parameter_value_type'}"/>
</group>
</search>
</field>
</record>
<!-- Action for lims.result -->
<record id="action_lims_result" model="ir.actions.act_window">
<field name="name">Resultados de Análisis</field>
<field name="res_model">lims.result</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_lims_result_search"/>
<field name="context">{'search_default_out_of_range': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay resultados registrados
</p>
<p>
Los resultados se crean automáticamente al generar las pruebas
de laboratorio basándose en los parámetros configurados.
</p>
</field>
</record>
</odoo>

View File

@ -24,6 +24,10 @@
<button name="action_draft" string="Volver a Borrador"
type="object"
invisible="state != 'cancelled'"/>
<button name="action_regenerate_results" string="Regenerar Resultados"
type="object"
invisible="state not in ['draft', 'in_process']"
confirm="¿Está seguro de regenerar los resultados? Esto eliminará los resultados actuales."/>
<field name="state" widget="statusbar"
statusbar_visible="draft,in_process,result_entered,validated"/>
</header>
@ -55,21 +59,53 @@
<page string="Resultados">
<field name="result_ids"
readonly="state in ['validated', 'cancelled']"
context="{'default_test_id': id}">
<list string="Resultados" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="parameter_name"/>
context="{'default_test_id': id, 'default_patient_id': patient_id, 'default_test_date': create_date}"
mode="list">
<list string="Resultados" editable="bottom"
decoration-danger="is_out_of_range and not is_critical"
decoration-warning="is_critical"
decoration-success="not is_out_of_range and not is_critical and parameter_value_type == 'numeric'">
<field name="sequence" widget="handle" optional="show"/>
<field name="parameter_id"
options="{'no_create': True, 'no_open': True}"
readonly="1"/>
<field name="parameter_code" optional="show" readonly="1"/>
<field name="parameter_value_type" invisible="1"/>
<!-- Campos de valor con mejores widgets -->
<field name="value_numeric"
invisible="value_text or value_selection"
decoration-danger="is_out_of_range"/>
invisible="parameter_value_type != 'numeric'"
widget="float"
options="{'digits': [16, 4]}"
class="oe_edit_only"/>
<field name="value_text"
invisible="value_numeric or value_selection"/>
invisible="parameter_value_type != 'text'"
class="oe_edit_only"/>
<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'"
widget="selection"
class="oe_edit_only"/>
<field name="value_boolean"
invisible="parameter_value_type != 'boolean'"
widget="boolean_toggle"
class="oe_edit_only"/>
<!-- Unidad y rangos -->
<field name="parameter_unit"
invisible="parameter_value_type != 'numeric'"
optional="show"
readonly="1"/>
<field name="applicable_range_id"
optional="hide"
readonly="1"/>
<!-- Indicadores de estado -->
<field name="is_out_of_range" invisible="1"/>
<field name="is_critical" invisible="1"/>
<!-- Campo de estado visual -->
<field name="result_status"
widget="badge"
optional="show"
decoration-success="result_status == 'normal'"
decoration-warning="result_status == 'abnormal'"
decoration-danger="result_status == 'critical'"/>
<field name="notes" optional="show"/>
</list>
</field>
@ -89,8 +125,8 @@
</record>
<!-- Vista lista para lims.test -->
<record id="view_lims_test_list" model="ir.ui.view">
<field name="name">lims.test.list</field>
<record id="view_lims_test_tree" model="ir.ui.view">
<field name="name">lims.test.tree</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<list string="Pruebas de Laboratorio">
@ -191,23 +227,5 @@
</field>
</record>
<!-- Acción para lims.test -->
<record id="action_lims_test" model="ir.actions.act_window">
<field name="name">Pruebas de Laboratorio</field>
<field name="res_model">lims.test</field>
<field name="view_mode">list,kanban,form</field>
<field name="search_view_id" ref="view_lims_test_search"/>
<field name="context">{'search_default_my_tests': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear primera prueba de laboratorio
</p>
<p>
Aquí podrá gestionar las pruebas de laboratorio,
ingresar resultados y validarlos.
</p>
</field>
</record>
</data>
</odoo>

View File

@ -102,12 +102,64 @@
action="action_lims_lab_sample"
sequence="16"/>
<!-- Submenú de Laboratorio -->
<menuitem
id="lims_menu_laboratory"
name="Laboratorio"
parent="lims_menu_root"
sequence="20"/>
<!-- Acción para lims.test -->
<record id="action_lims_test" model="ir.actions.act_window">
<field name="name">Pruebas de Laboratorio</field>
<field name="res_model">lims.test</field>
<field name="view_mode">list,kanban,form</field>
<field name="context">{'search_default_my_tests': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear primera prueba de laboratorio
</p>
<p>
Aquí podrá gestionar las pruebas de laboratorio,
ingresar resultados y validarlos.
</p>
</field>
</record>
<!-- Menú para Pruebas -->
<menuitem id="menu_lims_tests"
name="Pruebas"
parent="lims_menu_root"
parent="lims_menu_laboratory"
action="action_lims_test"
sequence="10"/>
<!-- Menú para Ingreso de Resultados -->
<menuitem id="menu_lims_result_entry"
name="Ingreso de Resultados"
parent="lims_menu_laboratory"
action="action_lims_result_entry"
sequence="25"/>
<!-- Menú para Resultados -->
<menuitem id="menu_lims_result"
name="Resultados"
parent="lims_menu_laboratory"
action="action_lims_result"
sequence="30"/>
<!-- Submenú de Reportes -->
<menuitem
id="lims_menu_reports"
name="Reportes"
parent="lims_menu_root"
sequence="90"/>
<!-- Menú para Análisis de Resultados en Reportes -->
<menuitem id="menu_lims_result_analysis"
name="Análisis de Resultados"
parent="lims_menu_reports"
action="action_lims_result_analysis"
sequence="20"/>
<!-- Submenú de Configuración -->
<menuitem
@ -168,11 +220,56 @@
action="action_lims_sample_type_catalog"
sequence="20"/>
<!-- Acción para abrir configuración de laboratorio -->
<record id="action_lims_config_settings" model="ir.actions.act_window">
<field name="name">Configuración</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'lims_management'}</field>
</record>
<!-- Menú de Panel de Parámetros -->
<menuitem id="menu_lims_parameter_dashboard"
name="Panel de Parámetros"
parent="lims_menu_config"
action="action_lims_parameter_dashboard"
sequence="10"/>
<!-- Menú de Parámetros de Análisis -->
<menuitem id="menu_lims_analysis_parameter"
name="Parámetros de Análisis"
parent="lims_menu_config"
action="action_lims_analysis_parameter"
sequence="20"/>
<!-- Menú de Rangos de Referencia -->
<menuitem id="menu_lims_parameter_range"
name="Rangos de Referencia"
parent="lims_menu_config"
action="action_lims_parameter_range"
sequence="25"/>
<!-- Menú de Config. Parámetros-Análisis -->
<menuitem id="menu_product_template_parameter_config"
name="Config. Parámetros-Análisis"
parent="lims_menu_config"
action="action_product_template_parameter_config"
sequence="30"/>
<!-- Menú de Estadísticas -->
<menuitem id="menu_lims_parameter_statistics"
name="Estadísticas"
parent="lims_menu_config"
action="action_lims_parameter_statistics"
sequence="40"/>
<!-- Menú de configuración de ajustes -->
<menuitem id="menu_lims_config_settings"
name="Ajustes"
parent="lims_menu_config"
action="lims_management.action_lims_config_settings"
action="action_lims_config_settings"
sequence="100"/>
</data>
</odoo>

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Kanban View for Parameters Dashboard -->
<record id="view_lims_analysis_parameter_kanban" model="ir.ui.view">
<field name="name">lims.analysis.parameter.kanban</field>
<field name="model">lims.analysis.parameter</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="code"/>
<field name="name"/>
<field name="value_type"/>
<field name="unit"/>
<field name="analysis_count"/>
<field name="active"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="code"/> - <field name="name"/>
</strong>
</div>
</div>
<div class="o_kanban_record_body">
<div class="text-muted">
<span>Tipo: </span>
<field name="value_type" widget="badge"/>
</div>
<div t-if="record.unit.raw_value" class="text-muted">
<span>Unidad: </span>
<field name="unit"/>
</div>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<span t-if="!record.active.raw_value"
class="badge badge-danger">Archivado</span>
</div>
<div class="oe_kanban_bottom_right">
<field name="analysis_count" widget="badge"/>
<span> análisis</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Graph View for Parameter Usage Statistics -->
<record id="view_product_template_parameter_graph" model="ir.ui.view">
<field name="name">product.template.parameter.graph</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<graph string="Uso de Parámetros en Análisis" type="bar">
<field name="parameter_id"/>
<field name="product_tmpl_id" type="measure"/>
</graph>
</field>
</record>
<!-- Dashboard Action for Parameters -->
<record id="action_lims_parameter_dashboard" model="ir.actions.act_window">
<field name="name">Panel de Parámetros</field>
<field name="res_model">lims.analysis.parameter</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_lims_analysis_parameter_search"/>
<field name="context">{'search_default_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay parámetros configurados
</p>
<p>
Configure los parámetros que se utilizarán en los análisis clínicos.
Cada parámetro puede tener múltiples rangos de referencia según
las características del paciente.
</p>
</field>
</record>
<!-- Parameter Statistics Action -->
<record id="action_lims_parameter_statistics" model="ir.actions.act_window">
<field name="name">Estadísticas de Parámetros</field>
<field name="res_model">product.template.parameter</field>
<field name="view_mode">graph,pivot,list</field>
<field name="help" type="html">
<p>
Visualización estadística del uso de parámetros en los diferentes análisis.
</p>
</field>
</record>
<!-- Configuration Summary Dashboard -->
<record id="view_lims_config_summary_form" model="ir.ui.view">
<field name="name">lims.config.summary.form</field>
<field name="model">res.config.settings</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="lims_management.res_config_settings_view_form_lims"/>
<field name="arch" type="xml">
<xpath expr="//app[@name='lims_management']//block[@name='lims_settings']" position="after">
<div class="row mt16" id="lims_configuration_stats">
<div class="col-12">
<h2>Estadísticas de Configuración</h2>
</div>
<div class="col-lg-3 col-md-6">
<div class="card">
<div class="card-body text-center">
<h4>Parámetros</h4>
<p class="text-muted">Total configurados</p>
<button name="%(action_lims_analysis_parameter)d"
string="Ver Parámetros"
type="action"
class="btn-link"/>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card">
<div class="card-body text-center">
<h4>Rangos</h4>
<p class="text-muted">Rangos de referencia</p>
<button name="%(action_lims_parameter_range)d"
string="Ver Rangos"
type="action"
class="btn-link"/>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card">
<div class="card-body text-center">
<h4>Análisis</h4>
<p class="text-muted">Con parámetros</p>
<button name="%(action_product_template_parameter_config)d"
string="Ver Configuración"
type="action"
class="btn-link"/>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card">
<div class="card-body text-center">
<h4>Estadísticas</h4>
<p class="text-muted">Uso de parámetros</p>
<button name="%(action_lims_parameter_statistics)d"
string="Ver Estadísticas"
type="action"
class="btn-link"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_lims_parameter_range_form" model="ir.ui.view">
<field name="name">lims.parameter.range.form</field>
<field name="model">lims.parameter.range</field>
<field name="arch" type="xml">
<form string="Rango de Referencia">
<sheet>
<group>
<group string="Parámetro">
<field name="parameter_id"
options="{'no_create': True}"
context="{'form_view_ref': 'lims_management.view_lims_analysis_parameter_form'}"/>
<field name="parameter_unit"/>
</group>
<group string="Condiciones">
<field name="gender"/>
<field name="age_min"/>
<field name="age_max"/>
<field name="pregnant" invisible="gender == 'male'"/>
</group>
</group>
<group string="Valores de Referencia">
<group>
<label for="normal_min"/>
<div class="o_row">
<field name="normal_min" class="oe_inline"/>
<span class="oe_inline"> - </span>
<field name="normal_max" class="oe_inline"/>
<field name="parameter_unit" class="oe_inline" readonly="1"/>
</div>
</group>
<group>
<label for="critical_min"/>
<div class="o_row">
<span class="oe_inline">&lt; </span>
<field name="critical_min" class="oe_inline"/>
<span class="oe_inline"> o &gt; </span>
<field name="critical_max" class="oe_inline"/>
<field name="parameter_unit" class="oe_inline" readonly="1"/>
</div>
</group>
</group>
<group string="Interpretación Clínica">
<field name="interpretation" nolabel="1"
placeholder="Ingrese guías de interpretación clínica para este rango..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- List View -->
<record id="view_lims_parameter_range_list" model="ir.ui.view">
<field name="name">lims.parameter.range.list</field>
<field name="model">lims.parameter.range</field>
<field name="arch" type="xml">
<list string="Rangos de Referencia" editable="bottom">
<field name="parameter_id" optional="hide"/>
<field name="name"/>
<field name="gender"/>
<field name="age_min"/>
<field name="age_max"/>
<field name="pregnant" optional="show"/>
<field name="normal_min"/>
<field name="normal_max"/>
<field name="critical_min" optional="show"/>
<field name="critical_max" optional="show"/>
<field name="parameter_unit" optional="show"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="view_lims_parameter_range_search" model="ir.ui.view">
<field name="name">lims.parameter.range.search</field>
<field name="model">lims.parameter.range</field>
<field name="arch" type="xml">
<search string="Buscar Rangos">
<field name="parameter_id"/>
<field name="parameter_name"/>
<field name="parameter_code"/>
<field name="name"/>
<filter string="Masculino" name="male" domain="[('gender', '=', 'male')]"/>
<filter string="Femenino" name="female" domain="[('gender', '=', 'female')]"/>
<filter string="Ambos" name="both" domain="[('gender', '=', 'both')]"/>
<separator/>
<filter string="Embarazadas" name="pregnant" domain="[('pregnant', '=', True)]"/>
<separator/>
<filter string="Pediátrico (&lt;18)" name="pediatric"
domain="[('age_min', '&lt;', 18)]"/>
<filter string="Adulto (18-65)" name="adult"
domain="[('age_min', '&gt;=', 18), ('age_max', '&lt;=', 65)]"/>
<filter string="Geriátrico (&gt;65)" name="geriatric"
domain="[('age_max', '&gt;', 65)]"/>
<group expand="0" string="Agrupar por">
<filter string="Parámetro" name="group_parameter"
context="{'group_by': 'parameter_id'}"/>
<filter string="Género" name="group_gender"
context="{'group_by': 'gender'}"/>
<filter string="Embarazo" name="group_pregnant"
context="{'group_by': 'pregnant'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_lims_parameter_range" model="ir.actions.act_window">
<field name="name">Rangos de Referencia</field>
<field name="res_model">lims.parameter.range</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_lims_parameter_range_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear nuevo rango de referencia
</p>
<p>
Los rangos de referencia definen los valores normales y críticos
para cada parámetro según edad, género y otras condiciones del paciente.
</p>
</field>
</record>
</odoo>

View File

@ -11,6 +11,8 @@
<field name="name"/>
<field name="gender"/>
<field name="birthdate_date"/>
<field name="age" optional="show"/>
<field name="is_pregnant" optional="show"/>
</list>
</field>
</record>
@ -43,7 +45,9 @@
<field name="patient_identifier" invisible="not is_patient" readonly="patient_identifier"/>
<field name="origin" readonly="id" invisible="not is_patient"/>
<field name="birthdate_date" invisible="not is_patient"/>
<field name="age" invisible="not is_patient or not birthdate_date"/>
<field name="gender" invisible="not is_patient"/>
<field name="is_pregnant" invisible="not is_patient or gender != 'female'"/>
</group>
<group>
<field name="is_doctor"/>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View for Configuration -->
<record id="view_product_template_parameter_config_form" model="ir.ui.view">
<field name="name">product.template.parameter.config.form</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<form string="Configuración de Parámetro en Análisis">
<sheet>
<group>
<group string="Análisis">
<field name="product_tmpl_id"
readonly="1"
options="{'no_open': True}"/>
</group>
<group string="Parámetro">
<field name="parameter_id"
readonly="1"
options="{'no_open': True}"/>
<field name="parameter_code"/>
<field name="parameter_value_type"/>
<field name="parameter_unit" invisible="parameter_value_type != 'numeric'"/>
</group>
</group>
<group string="Configuración">
<group>
<field name="sequence"/>
<field name="required"/>
</group>
<group>
<field name="instructions" widget="text"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- List View for Configuration -->
<record id="view_product_template_parameter_config_list" model="ir.ui.view">
<field name="name">product.template.parameter.config.list</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<list string="Configuración de Parámetros por Análisis">
<field name="product_tmpl_id"/>
<field name="parameter_id"/>
<field name="parameter_code"/>
<field name="parameter_value_type"/>
<field name="parameter_unit" optional="show"/>
<field name="sequence"/>
<field name="required"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="view_product_template_parameter_config_search" model="ir.ui.view">
<field name="name">product.template.parameter.config.search</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<search string="Buscar Configuración">
<field name="product_tmpl_id" string="Análisis"/>
<field name="parameter_id" string="Parámetro"/>
<field name="parameter_code"/>
<field name="parameter_name"/>
<filter string="Requeridos" name="required"
domain="[('required', '=', True)]"/>
<filter string="Opcionales" name="optional"
domain="[('required', '=', False)]"/>
<separator/>
<filter string="Numéricos" name="numeric"
domain="[('parameter_value_type', '=', 'numeric')]"/>
<filter string="Texto" name="text"
domain="[('parameter_value_type', '=', 'text')]"/>
<filter string="Sí/No" name="boolean"
domain="[('parameter_value_type', '=', 'boolean')]"/>
<filter string="Selección" name="selection"
domain="[('parameter_value_type', '=', 'selection')]"/>
<group expand="0" string="Agrupar por">
<filter string="Análisis" name="group_analysis"
context="{'group_by': 'product_tmpl_id'}"/>
<filter string="Parámetro" name="group_parameter"
context="{'group_by': 'parameter_id'}"/>
<filter string="Tipo de Valor" name="group_value_type"
context="{'group_by': 'parameter_value_type'}"/>
<filter string="Requerido" name="group_required"
context="{'group_by': 'required'}"/>
</group>
</search>
</field>
</record>
<!-- Pivot View for Analysis -->
<record id="view_product_template_parameter_pivot" model="ir.ui.view">
<field name="name">product.template.parameter.pivot</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<pivot string="Matriz de Parámetros por Análisis">
<field name="product_tmpl_id" type="row"/>
<field name="parameter_id" type="col"/>
<field name="required" type="measure"/>
</pivot>
</field>
</record>
<!-- Action -->
<record id="action_product_template_parameter_config" model="ir.actions.act_window">
<field name="name">Configuración Parámetros-Análisis</field>
<field name="res_model">product.template.parameter</field>
<field name="view_mode">list,form,pivot</field>
<field name="search_view_id" ref="view_product_template_parameter_config_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Configurar parámetros en análisis
</p>
<p>
Esta vista muestra la configuración de qué parámetros
están incluidos en cada análisis clínico.
</p>
</field>
</record>
</odoo>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_product_template_parameter_form" model="ir.ui.view">
<field name="name">product.template.parameter.form</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<form string="Parámetro del Análisis">
<sheet>
<group>
<group string="Información General">
<field name="product_tmpl_id" readonly="1"/>
<field name="parameter_id"
options="{'no_create': True}"
context="{'form_view_ref': 'lims_management.view_lims_analysis_parameter_form'}"/>
<field name="sequence"/>
<field name="required"/>
</group>
<group string="Detalles del Parámetro">
<field name="parameter_code"/>
<field name="parameter_value_type"/>
<field name="parameter_unit"/>
</group>
</group>
<group string="Instrucciones Específicas">
<field name="instructions" nolabel="1" placeholder="Ingrese instrucciones especiales para este parámetro en este análisis..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- List View -->
<record id="view_product_template_parameter_list" model="ir.ui.view">
<field name="name">product.template.parameter.list</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<list string="Parámetros por Análisis" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="parameter_id"
options="{'no_create': True}"
domain="[('active', '=', True)]"/>
<field name="parameter_code"/>
<field name="parameter_value_type"/>
<field name="parameter_unit"/>
<field name="required"/>
<field name="instructions" optional="show"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="view_product_template_parameter_search" model="ir.ui.view">
<field name="name">product.template.parameter.search</field>
<field name="model">product.template.parameter</field>
<field name="arch" type="xml">
<search string="Buscar Parámetros">
<field name="product_tmpl_id"/>
<field name="parameter_id"/>
<field name="parameter_name"/>
<field name="parameter_code"/>
<filter string="Obligatorios" name="required" domain="[('required', '=', True)]"/>
<filter string="Opcionales" name="optional" domain="[('required', '=', False)]"/>
<separator/>
<filter string="Numéricos" name="numeric" domain="[('parameter_value_type', '=', 'numeric')]"/>
<filter string="Texto" name="text" domain="[('parameter_value_type', '=', 'text')]"/>
<group expand="0" string="Agrupar por">
<filter string="Análisis" name="group_product" context="{'group_by': 'product_tmpl_id'}"/>
<filter string="Parámetro" name="group_parameter" context="{'group_by': 'parameter_id'}"/>
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'parameter_value_type'}"/>
<filter string="Obligatorio" name="group_required" context="{'group_by': 'required'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_product_template_parameter" model="ir.actions.act_window">
<field name="name">Parámetros por Análisis</field>
<field name="res_model">product.template.parameter</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_product_template_parameter_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Configurar parámetros para análisis
</p>
<p>
Aquí puede ver y configurar qué parámetros se miden en cada análisis,
su orden de aparición y si son obligatorios u opcionales.
</p>
</field>
</record>
</odoo>

View File

@ -23,15 +23,5 @@
</field>
</record>
<!-- Acción para abrir configuración de laboratorio -->
<record id="action_lims_config_settings" model="ir.actions.act_window">
<field name="name">Configuración</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'lims_management'}</field>
</record>
</data>
</odoo>

87
test/check_new_results.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para verificar los resultados recién creados
"""
import odoo
def check_results(cr):
"""Verificar resultados de las pruebas LAB-2025-00034, 00035 y 00036"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("🔍 Buscando las pruebas recién creadas...")
# Buscar las pruebas por nombre
test_names = ['LAB-2025-00034', 'LAB-2025-00035', 'LAB-2025-00036']
tests = env['lims.test'].search([('name', 'in', test_names)])
print(f"\n Pruebas encontradas: {len(tests)}")
for test in tests:
print(f"\n📋 Prueba: {test.name}")
print(f" Análisis: {test.product_id.name}")
print(f" Resultados: {len(test.result_ids)}")
for result in test.result_ids:
print(f"\n Resultado ID {result.id}:")
print(f" Parámetro: {result.parameter_id.name}")
print(f" Tipo: {result.parameter_value_type}")
print(f" value_numeric: {result.value_numeric}")
print(f" value_text: '{result.value_text}'")
print(f" value_selection: '{result.value_selection}'")
print(f" value_boolean: {result.value_boolean}")
# Verificar si es problemático
if result.parameter_value_type == 'selection':
values_count = 0
if result.value_numeric not in [False, 0.0]:
values_count += 1
if result.value_text:
values_count += 1
if result.value_selection:
values_count += 1
if result.value_boolean:
values_count += 1
if values_count > 1 or (values_count == 1 and not result.value_selection):
print(f" ❌ PROBLEMÁTICO: {values_count} valores establecidos")
# Intentar corregir
print(" 🔧 Corrigiendo...")
try:
# Primero intentar con SQL directo para evitar validaciones
cr.execute("""
UPDATE lims_result
SET value_numeric = NULL,
value_text = NULL,
value_boolean = FALSE
WHERE id = %s
""", (result.id,))
print(" ✓ Corregido con SQL directo")
except Exception as e:
print(f" ❌ Error al corregir: {e}")
# Verificar si la orden S00029 sigue en estado sale
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if order:
print(f"\n📊 Estado de la orden S00029: {order.state}")
# Si está en sale, las muestras deberían estar generadas
if order.state == 'sale':
print(f" Muestras generadas: {len(order.generated_sample_ids)}")
for sample in order.generated_sample_ids:
print(f" - {sample.name}: {sample.sample_state}")
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
check_results(cr)
cr.commit()
print("\n✅ Cambios guardados exitosamente")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()

59
test/check_views.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import odoo
def check_views(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== VERIFICANDO VISTAS DE lims.test ===\n")
# Buscar todas las vistas del modelo
views = env['ir.ui.view'].search([('model', '=', 'lims.test')])
print(f"Total de vistas encontradas: {len(views)}\n")
for view in views:
print(f"ID: {view.id}")
print(f"Nombre: {view.name}")
print(f"Tipo: {view.type}")
print(f"Prioridad: {view.priority}")
print(f"Activa: {view.active}")
print(f"XML ID: {view.xml_id}")
print("-" * 50)
# Verificar la acción
print("\n=== VERIFICANDO ACCIÓN ===\n")
action = env.ref('lims_management.action_lims_test', raise_if_not_found=False)
if action:
print(f"Acción encontrada: {action.name}")
print(f"Modelo: {action.res_model}")
print(f"View mode: {action.view_mode}")
print(f"View ID: {action.view_id.name if action.view_id else 'No definida'}")
print(f"Search view ID: {action.search_view_id.name if action.search_view_id else 'No definida'}")
# Verificar las vistas específicas
print("\nVistas específicas de la acción:")
for view_ref in action.view_ids:
print(f" - Tipo: {view_ref.view_mode}, Vista: {view_ref.view_id.name if view_ref.view_id else 'Por defecto'}")
else:
print("⚠️ No se encontró la acción")
# Probar obtener la vista por defecto
print("\n=== PROBANDO OBTENER VISTA POR DEFECTO ===\n")
try:
view_id, view_type = env['lims.test'].get_view()
print(f"Vista por defecto: ID={view_id}, Tipo={view_type}")
# Intentar obtener vista tree específicamente
tree_view = env['lims.test'].get_view(view_type='tree')
print(f"Vista tree: ID={tree_view[0]}, Tipo={tree_view[1]}")
except Exception as e:
print(f"❌ Error al obtener vista: {e}")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
check_views(cr)

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import odoo
def check_views_and_action(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== VERIFICANDO ACCIÓN DE lims.test ===\n")
# Verificar la acción
action = env.ref('lims_management.action_lims_test', raise_if_not_found=False)
if action:
print(f"ID de la acción: {action.id}")
print(f"Nombre de la acción: {action.name}")
print(f"Modelo: {action.res_model}")
print(f"View mode: {action.view_mode}")
print(f"View ID: {action.view_id.name if action.view_id else 'No definida'}")
print(f"Contexto: {action.context}")
# Verificar las vistas disponibles
print("\n=== VISTAS DE lims.test ===\n")
views = env['ir.ui.view'].search([('model', '=', 'lims.test')])
for view in views:
print(f"- {view.name} (tipo: {view.type}, XML ID: {view.xml_id})")
# Simular la apertura de la acción
print("\n=== SIMULANDO APERTURA DE LA ACCIÓN ===\n")
try:
# Intentar obtener las vistas que usaría la acción
model = env['lims.test']
for view_mode in action.view_mode.split(','):
view_mode = view_mode.strip()
print(f"\nIntentando obtener vista '{view_mode}':")
try:
view_id, view_type = model.get_view(view_type=view_mode)
actual_view = env['ir.ui.view'].browse(view_id)
print(f" ✓ Vista encontrada: {actual_view.name} (ID: {view_id})")
except Exception as e:
print(f" ✗ Error: {e}")
except Exception as e:
print(f"Error general: {e}")
else:
print("✗ No se encontró la acción 'lims_management.action_lims_test'")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
check_views_and_action(cr)

View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para confirmar la orden S00027
"""
import odoo
def confirm_order(cr):
"""Confirmar la orden S00027"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la orden S00027
order = env['sale.order'].search([('name', '=', 'S00027')], limit=1)
if not order:
print("❌ No se encontró la orden S00027")
return
print(f"✓ Orden encontrada: {order.name}")
print(f" Cliente: {order.partner_id.name}")
print(f" Estado actual: {order.state}")
print(f" Líneas de orden: {len(order.order_line)}")
for line in order.order_line:
print(f" - {line.product_id.name} (cant: {line.product_uom_qty})")
if order.state == 'sale':
print("\n✓ La orden ya está confirmada")
print(f" Muestras generadas: {len(order.generated_sample_ids)}")
for sample in order.generated_sample_ids:
print(f" - {sample.name}: {sample.sample_state}")
# Ver las pruebas generadas
tests = env['lims.test'].search([
('sale_order_line_id.order_id', '=', order.id)
])
print(f"\n Pruebas de laboratorio: {len(tests)}")
for test in tests:
print(f" - {test.name}: {test.product_id.name} ({test.state})")
if test.result_ids:
print(f" Resultados: {len(test.result_ids)}")
for result in test.result_ids[:3]: # Mostrar solo los primeros 3
print(f" - {result.parameter_id.name} ({result.parameter_value_type})")
else:
print("\n🔄 Confirmando orden...")
try:
order.action_confirm()
print("✅ ¡Orden confirmada exitosamente!")
print(f" Nuevo estado: {order.state}")
print(f" Muestras generadas: {len(order.generated_sample_ids)}")
# Verificar las pruebas generadas
tests = env['lims.test'].search([
('sale_order_line_id.order_id', '=', order.id)
])
print(f" Pruebas generadas: {len(tests)}")
for test in tests:
print(f" - {test.name}: {test.product_id.name}")
except Exception as e:
print(f"❌ Error al confirmar: {str(e)}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
confirm_order(cr)
cr.commit()
print("\n✅ Transacción completada")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()

415
test/create_demo_data.py Normal file
View File

@ -0,0 +1,415 @@
# -*- coding: utf-8 -*-
"""
Script para crear datos de demostración completos para el módulo LIMS.
Incluye órdenes de laboratorio, muestras, pruebas y resultados.
"""
import odoo
from datetime import datetime, timedelta
import random
import logging
_logger = logging.getLogger(__name__)
def create_demo_lab_data(cr):
"""Crea datos completos de demostración para laboratorio"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== INICIANDO CREACIÓN DE DATOS DE DEMOSTRACIÓN ===")
# Verificar que los parámetros y rangos se cargaron correctamente
param_count = env['lims.analysis.parameter'].search_count([])
range_count = env['lims.parameter.range'].search_count([])
print(f"Parámetros encontrados: {param_count}")
print(f"Rangos de referencia encontrados: {range_count}")
if param_count == 0 or range_count == 0:
print("⚠️ No se encontraron parámetros o rangos. Asegúrese de que los datos XML se cargaron.")
return
# Obtener pacientes de demostración
patients = []
patient_refs = [
'lims_management.demo_patient_1',
'lims_management.demo_patient_2',
'lims_management.demo_patient_3',
'lims_management.demo_patient_4'
]
for ref in patient_refs:
patient = env.ref(ref, raise_if_not_found=False)
if patient:
patients.append(patient)
if not patients:
print("⚠️ No se encontraron pacientes de demostración")
return
print(f"Pacientes encontrados: {len(patients)}")
# Obtener doctores
doctors = []
doctor_refs = ['lims_management.demo_doctor_1', 'lims_management.demo_doctor_2']
for ref in doctor_refs:
doctor = env.ref(ref, raise_if_not_found=False)
if doctor:
doctors.append(doctor)
if not doctors:
# Crear un doctor de demo si no existe
doctors = [env['res.partner'].create({
'name': 'Dr. Demo',
'is_doctor': True
})]
# Obtener análisis disponibles
analyses = []
analysis_refs = [
'lims_management.analysis_hemograma',
'lims_management.analysis_perfil_lipidico',
'lims_management.analysis_glucosa',
'lims_management.analysis_quimica_sanguinea',
'lims_management.analysis_urianalisis',
'lims_management.analysis_serologia',
'lims_management.analysis_urocultivo',
'lims_management.analysis_tp',
'lims_management.analysis_prueba_embarazo'
]
for ref in analysis_refs:
analysis = env.ref(ref, raise_if_not_found=False)
if analysis:
analyses.append(analysis)
print(f"Análisis encontrados: {len(analyses)}")
if not analyses:
print("⚠️ No se encontraron análisis de demostración")
return
# Crear órdenes de laboratorio
orders_created = []
# Orden 1: Chequeo general para paciente adulto masculino
if len(patients) > 0 and len(analyses) >= 4:
order1 = env['sale.order'].create({
'partner_id': patients[0].id,
'doctor_id': doctors[0].id if doctors else False,
'is_lab_request': True,
# 'lab_request_priority': 'normal', # Campo no existe aún
'note': 'Chequeo general anual - Control de salud preventivo',
'order_line': [
(0, 0, {
'product_id': analyses[0].product_variant_id.id, # Hemograma
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[1].product_variant_id.id, # Perfil Lipídico
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[2].product_variant_id.id, # Glucosa
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[3].product_variant_id.id, # Química Sanguínea
'product_uom_qty': 1
})
]
})
order1.action_confirm()
orders_created.append(order1)
print(f"✓ Orden {order1.name} creada para {order1.partner_id.name}")
# Orden 2: Control prenatal para paciente embarazada
if len(patients) > 2 and len(analyses) >= 5:
# Usar María González (índice 2) que es femenina
patients[2].is_pregnant = True
order2 = env['sale.order'].create({
'partner_id': patients[2].id,
'doctor_id': doctors[-1].id if doctors else False,
'is_lab_request': True,
# 'lab_request_priority': 'high', # Campo no existe aún
'note': 'Control prenatal - 20 semanas de gestación',
'order_line': [
(0, 0, {
'product_id': analyses[0].product_variant_id.id, # Hemograma
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[2].product_variant_id.id, # Glucosa
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[4].product_variant_id.id if len(analyses) > 4 else analyses[0].product_variant_id.id, # Urianálisis
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[5].product_variant_id.id if len(analyses) > 5 else analyses[1].product_variant_id.id, # Serología
'product_uom_qty': 1
})
]
})
order2.action_confirm()
orders_created.append(order2)
print(f"✓ Orden {order2.name} creada para {order2.partner_id.name} (embarazada)")
# Orden 3: Urgencia - Sospecha de infección
if len(patients) > 1 and len(analyses) >= 3:
order3 = env['sale.order'].create({
'partner_id': patients[1].id,
'doctor_id': doctors[0].id if doctors else False,
'is_lab_request': True,
# 'lab_request_priority': 'urgent', # Campo no existe aún
'note': 'Urgencia - Fiebre de 39°C, dolor lumbar, sospecha de infección urinaria',
'order_line': [
(0, 0, {
'product_id': analyses[4].product_variant_id.id if len(analyses) > 4 else analyses[0].product_variant_id.id, # Urianálisis
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[6].product_variant_id.id if len(analyses) > 6 else analyses[1].product_variant_id.id, # Urocultivo
'product_uom_qty': 1
}),
(0, 0, {
'product_id': analyses[0].product_variant_id.id, # Hemograma (para ver leucocitos)
'product_uom_qty': 1
})
]
})
order3.action_confirm()
orders_created.append(order3)
print(f"✓ Orden urgente {order3.name} creada para {order3.partner_id.name}")
# Orden 4: Control pediátrico
if len(patients) > 3:
order4 = env['sale.order'].create({
'partner_id': patients[3].id,
'doctor_id': doctors[-1].id if doctors else False,
'is_lab_request': True,
# 'lab_request_priority': 'normal', # Campo no existe aún
'note': 'Control pediátrico - Evaluación de anemia, niña con palidez',
'order_line': [
(0, 0, {
'product_id': analyses[0].product_variant_id.id, # Hemograma completo
'product_uom_qty': 1
})
]
})
order4.action_confirm()
orders_created.append(order4)
print(f"✓ Orden pediátrica {order4.name} creada para {order4.partner_id.name}")
print(f"\n📋 Total de órdenes creadas: {len(orders_created)}")
# Procesar muestras y generar resultados
for idx, order in enumerate(orders_created):
print(f"\n--- Procesando orden {idx + 1}/{len(orders_created)}: {order.name} ---")
# Generar muestras si no existen
if not order.generated_sample_ids:
order.action_generate_samples()
print(f" ✓ Muestras generadas: {len(order.generated_sample_ids)}")
# Procesar cada muestra
for sample in order.generated_sample_ids:
# Marcar como recolectada
if sample.sample_state == 'pending_collection':
sample.action_collect()
print(f" ✓ Muestra {sample.name} recolectada")
# Procesar pruebas de esta muestra
for test in sample.test_ids:
print(f" - Procesando prueba: {test.product_id.name}")
# Iniciar proceso si está en borrador
if test.state == 'draft':
test.action_start_process()
# La generación automática de resultados ya debería haberse ejecutado
if test.result_ids:
print(f" ✓ Resultados generados automáticamente: {len(test.result_ids)}")
# Simular ingreso de valores en los resultados
simulate_test_results(env, test)
# Marcar como resultados ingresados
if test.state == 'in_process':
test.action_enter_results()
print(f" ✓ Resultados ingresados")
# Validar las primeras 2 órdenes completas y algunas pruebas de la tercera
should_validate = (idx < 2) or (idx == 2 and test == sample.test_ids[0])
if should_validate and test.state == 'result_entered':
test.action_validate()
print(f" ✓ Prueba validada")
else:
print(f" ⚠️ No se generaron resultados automáticamente")
# Resumen final
print("\n" + "="*60)
print("RESUMEN DE DATOS CREADOS")
print("="*60)
# Contar registros creados
total_samples = env['stock.lot'].search_count([('is_lab_sample', '=', True)])
total_tests = env['lims.test'].search_count([])
total_results = env['lims.result'].search_count([])
tests_by_state = {}
for state in ['draft', 'in_process', 'result_entered', 'validated', 'cancelled']:
count = env['lims.test'].search_count([('state', '=', state)])
if count > 0:
tests_by_state[state] = count
print(f"\n📊 Estadísticas:")
print(f" - Órdenes de laboratorio: {len(orders_created)}")
print(f" - Muestras totales: {total_samples}")
print(f" - Pruebas totales: {total_tests}")
print(f" - Resultados totales: {total_results}")
print(f"\n📈 Pruebas por estado:")
for state, count in tests_by_state.items():
print(f" - {state}: {count}")
# Verificar algunos resultados fuera de rango
out_of_range = env['lims.result'].search_count([('is_out_of_range', '=', True)])
critical = env['lims.result'].search_count([('is_critical', '=', True)])
if out_of_range or critical:
print(f"\n⚠️ Valores anormales:")
print(f" - Fuera de rango: {out_of_range}")
print(f" - Críticos: {critical}")
print("\n✅ Datos de demostración creados exitosamente")
def simulate_test_results(env, test):
"""Simular el ingreso de resultados realistas para una prueba"""
for result in test.result_ids:
param = result.parameter_id
if param.value_type == 'numeric':
# Generar valor numérico considerando el rango normal
if result.applicable_range_id:
range_obj = result.applicable_range_id
# Probabilidades: 75% normal, 20% anormal, 5% crítico
rand = random.random()
if rand < 0.75: # Valor normal
# Generar valor dentro del rango normal
value = random.uniform(range_obj.normal_min, range_obj.normal_max)
elif rand < 0.95: # Valor anormal pero no crítico
# Decidir si va por arriba o por abajo
if random.random() < 0.5 and range_obj.normal_min > 0:
# Por debajo del normal
value = random.uniform(range_obj.normal_min * 0.7, range_obj.normal_min * 0.95)
else:
# Por encima del normal
value = random.uniform(range_obj.normal_max * 1.05, range_obj.normal_max * 1.3)
else: # Valor crítico (5%)
if range_obj.critical_min and random.random() < 0.5:
# Crítico bajo
value = random.uniform(range_obj.critical_min * 0.5, range_obj.critical_min * 0.9)
elif range_obj.critical_max:
# Crítico alto
value = random.uniform(range_obj.critical_max * 1.1, range_obj.critical_max * 1.5)
else:
# Si no hay valores críticos definidos, usar un valor muy anormal
value = range_obj.normal_max * 2.0
# Redondear según el tipo de parámetro
if param.code in ['HGB', 'CREA', 'GLU', 'CHOL', 'HDL', 'LDL', 'TRIG']:
result.value_numeric = round(value, 1)
elif param.code in ['U-PH', 'U-DENS']:
result.value_numeric = round(value, 3)
else:
result.value_numeric = round(value, 2)
# Agregar notas para valores anormales en algunos casos
if result.is_out_of_range and random.random() < 0.3:
if param.code == 'GLU' and result.value_numeric > 126:
result.notes = "Hiperglucemia - Sugerir control de diabetes"
elif param.code == 'WBC' and result.value_numeric > 11:
result.notes = "Leucocitosis - Posible proceso infeccioso"
elif param.code == 'HGB' and result.value_numeric < range_obj.normal_min:
result.notes = "Anemia - Evaluar causa"
else:
# Sin rango definido, usar valores típicos
result.value_numeric = round(random.uniform(10, 100), 2)
elif param.value_type == 'selection':
# Seleccionar una opción con pesos realistas
if param.selection_values:
options = [opt.strip() for opt in param.selection_values.split(',')]
# Para cultivos, 70% negativo, 30% positivo
if param.code in ['CULT', 'HIV', 'HBsAg', 'HCV', 'VDRL']:
if 'Negativo' in options or 'No Reactivo' in options:
negative_option = 'Negativo' if 'Negativo' in options else 'No Reactivo'
positive_option = 'Positivo' if 'Positivo' in options else 'Reactivo'
result.value_selection = negative_option if random.random() < 0.7 else positive_option
else:
result.value_selection = random.choice(options)
# Para orina, distribución más realista
elif param.code == 'U-COLOR':
weights = [0.1, 0.6, 0.2, 0.05, 0.02, 0.02, 0.01] # Amarillo más común
result.value_selection = random.choices(options, weights=weights[:len(options)])[0]
elif param.code == 'U-ASP':
weights = [0.7, 0.2, 0.08, 0.02] # Transparente más común
result.value_selection = random.choices(options, weights=weights[:len(options)])[0]
else:
# Primera opción más probable (generalmente es la normal)
weights = [0.7] + [0.3/(len(options)-1)]*(len(options)-1)
result.value_selection = random.choices(options, weights=weights)[0]
elif param.value_type == 'boolean':
# Para pruebas de embarazo, considerar el género del paciente
if param.code == 'HCG' and test.patient_id.gender == 'female' and test.patient_id.is_pregnant:
result.value_boolean = True
else:
# 85% probabilidad de False (negativo) para la mayoría de pruebas
result.value_boolean = random.random() > 0.85
elif param.value_type == 'text':
# Generar texto según el parámetro
if param.code == 'MICRO':
# Solo si el cultivo es positivo
culture_result = test.result_ids.filtered(
lambda r: r.parameter_id.code == 'CULT'
)
if culture_result and culture_result.value_selection == 'Positivo':
organisms = ['E. coli', 'Klebsiella pneumoniae', 'Proteus mirabilis',
'Enterococcus faecalis', 'Staphylococcus aureus',
'Pseudomonas aeruginosa', 'Streptococcus agalactiae']
result.value_text = random.choice(organisms)
else:
result.value_text = "No se aisló microorganismo"
elif param.code == 'UFC':
# Solo si hay microorganismo
micro_result = test.result_ids.filtered(
lambda r: r.parameter_id.code == 'MICRO'
)
if micro_result and micro_result.value_text and micro_result.value_text != "No se aisló microorganismo":
counts = ['>100,000', '>50,000', '>10,000', '<10,000']
weights = [0.5, 0.3, 0.15, 0.05]
result.value_text = random.choices(counts, weights=weights)[0] + " UFC/mL"
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
create_demo_lab_data(cr)
cr.commit()

View File

@ -1,225 +0,0 @@
# -*- coding: utf-8 -*-
import odoo
from datetime import datetime, timedelta
def create_test_demo_data(cr):
"""Crea datos de demostración para lims.test y lims.result"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar algunos pacientes y análisis existentes
patients = env['res.partner'].search([('is_patient', '=', True)], limit=3)
if not patients:
print("No se encontraron pacientes para crear pruebas de demostración")
return
# Buscar análisis disponibles
hemograma = env.ref('lims_management.analysis_hemograma', raise_if_not_found=False)
glucosa = env.ref('lims_management.analysis_glucosa', raise_if_not_found=False)
if not hemograma or not glucosa:
print("No se encontraron análisis de demostración")
return
# Buscar o crear una orden de laboratorio simple
lab_order = env['sale.order'].search([
('is_lab_request', '=', True),
('state', '=', 'sale')
], limit=1)
if not lab_order:
# Crear una orden básica si no existe
lab_order = env['sale.order'].create({
'partner_id': patients[0].id,
'is_lab_request': True,
'order_line': [(0, 0, {
'product_id': hemograma.product_variant_id.id,
'product_uom_qty': 1
}), (0, 0, {
'product_id': glucosa.product_variant_id.id,
'product_uom_qty': 1
})]
})
lab_order.action_confirm()
# Obtener las líneas de orden
order_lines = lab_order.order_line
if not order_lines:
print("No se encontraron líneas de orden")
return
# Buscar muestras existentes
samples = env['stock.lot'].search([
('is_lab_sample', '=', True),
('patient_id', '=', lab_order.partner_id.id)
], limit=2)
if not samples:
print("No se encontraron muestras de laboratorio")
return
# Crear prueba 1: Hemograma en proceso
test1 = env['lims.test'].create({
'sale_order_line_id': order_lines[0].id,
'sample_id': samples[0].id,
'state': 'draft'
})
# Iniciar proceso
test1.action_start_process()
# Crear resultados para hemograma
results_data = [
{
'test_id': test1.id,
'parameter_name': 'Glóbulos Rojos',
'sequence': 10,
'value_numeric': 4.5,
'unit': '10^6/µL',
'normal_min': 4.2,
'normal_max': 5.4
},
{
'test_id': test1.id,
'parameter_name': 'Glóbulos Blancos',
'sequence': 20,
'value_numeric': 12.5, # Fuera de rango
'unit': '10^3/µL',
'normal_min': 4.5,
'normal_max': 11.0,
'notes': 'Valor elevado - posible infección'
},
{
'test_id': test1.id,
'parameter_name': 'Hemoglobina',
'sequence': 30,
'value_numeric': 14.2,
'unit': 'g/dL',
'normal_min': 12.0,
'normal_max': 16.0
},
{
'test_id': test1.id,
'parameter_name': 'Plaquetas',
'sequence': 40,
'value_numeric': 250,
'unit': '10^3/µL',
'normal_min': 150,
'normal_max': 400
}
]
for result_data in results_data:
env['lims.result'].create(result_data)
print(f"Creada prueba {test1.name} con 4 resultados")
# Crear prueba 2: Glucosa con resultado ingresado
if len(order_lines) > 1:
test2 = env['lims.test'].create({
'sale_order_line_id': order_lines[1].id,
'sample_id': samples[0].id,
'state': 'draft'
})
test2.action_start_process()
# Crear resultado de glucosa
env['lims.result'].create({
'test_id': test2.id,
'parameter_name': 'Glucosa en Ayunas',
'sequence': 10,
'value_numeric': 125, # Fuera de rango
'unit': 'mg/dL',
'normal_min': 70,
'normal_max': 110,
'notes': 'Valor elevado - prediabetes'
})
# Marcar resultados como ingresados
test2.action_enter_results()
print(f"Creada prueba {test2.name} con resultado ingresado")
# Crear prueba 3: Uroanálisis con valores mixtos (si hay más pacientes)
if len(patients) > 1 and len(samples) > 1:
# Crear una orden adicional
urine_analysis = env['product.template'].search([
('is_analysis', '=', True),
('name', 'ilike', 'orina')
], limit=1)
if urine_analysis:
lab_order2 = env['sale.order'].create({
'partner_id': patients[1].id,
'is_lab_request': True,
'order_line': [(0, 0, {
'product_id': urine_analysis.product_variant_id.id,
'product_uom_qty': 1
})]
})
lab_order2.action_confirm()
test3 = env['lims.test'].create({
'sale_order_line_id': lab_order2.order_line[0].id,
'sample_id': samples[1].id,
'state': 'draft'
})
test3.action_start_process()
# Crear resultados mixtos
urine_results = [
{
'test_id': test3.id,
'parameter_name': 'Color',
'sequence': 10,
'value_text': 'Amarillo claro'
},
{
'test_id': test3.id,
'parameter_name': 'pH',
'sequence': 20,
'value_numeric': 6.5,
'normal_min': 4.6,
'normal_max': 8.0
},
{
'test_id': test3.id,
'parameter_name': 'Densidad',
'sequence': 30,
'value_numeric': 1.020,
'normal_min': 1.005,
'normal_max': 1.030
},
{
'test_id': test3.id,
'parameter_name': 'Proteínas',
'sequence': 40,
'value_text': 'Negativo'
},
{
'test_id': test3.id,
'parameter_name': 'Glucosa',
'sequence': 50,
'value_text': 'Negativo'
}
]
for result_data in urine_results:
env['lims.result'].create(result_data)
# Ingresar y validar resultados
test3.action_enter_results()
if test3.state == 'result_entered':
test3.action_validate()
print(f"Creada prueba {test3.name} validada con resultados mixtos")
print("\nDatos de demostración de pruebas creados exitosamente")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
create_test_demo_data(cr)
cr.commit()

103
test/diagnose_s00029.py Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para diagnosticar el problema específico con S00029
"""
import odoo
import traceback
def diagnose_order(cr):
"""Diagnosticar el problema con S00029"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la orden
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if not order:
print("❌ No se encontró la orden S00029")
return
print(f"✓ Orden encontrada: {order.name}")
print(f" Estado: {order.state}")
# Intentar confirmar para ver el error exacto
print("\n🔄 Intentando confirmar para capturar el error...")
try:
order.action_confirm()
print("✅ ¡Orden confirmada sin problemas!")
except Exception as e:
print(f"❌ Error: {str(e)}")
# Si el error es sobre parámetros de selección, investigar
if "parámetros de selección" in str(e):
print("\n🔍 Investigando las pruebas generadas...")
# Buscar las pruebas asociadas a esta orden
tests = env['lims.test'].search([
('sale_order_line_id.order_id', '=', order.id)
])
print(f"\n Pruebas encontradas: {len(tests)}")
for test in tests:
print(f"\n 📋 Prueba: {test.name}")
print(f" Análisis: {test.product_id.name}")
print(f" Resultados: {len(test.result_ids)}")
# Revisar los resultados problemáticos
for result in test.result_ids:
if result.parameter_value_type == 'selection':
print(f"\n ⚠️ Resultado de selección: {result.parameter_id.name}")
print(f" - value_numeric: {result.value_numeric}")
print(f" - value_text: '{result.value_text}'")
print(f" - value_selection: '{result.value_selection}'")
print(f" - value_boolean: {result.value_boolean}")
# Verificar si tiene múltiples valores
values_count = 0
if result.value_numeric is not False and result.value_numeric is not None:
values_count += 1
if result.value_text:
values_count += 1
if result.value_selection:
values_count += 1
if result.value_boolean:
values_count += 1
if values_count > 1:
print(f" ❌ PROBLEMA: {values_count} valores establecidos!")
# Limpiar valores incorrectos
print(" 🔧 Limpiando valores incorrectos...")
result.write({
'value_numeric': False,
'value_text': False,
'value_boolean': False,
'value_selection': False # También limpiar selection por ahora
})
print(" ✓ Valores limpiados")
# Intentar confirmar nuevamente
print("\n🔄 Intentando confirmar después de limpiar...")
try:
order.action_confirm()
print("✅ ¡Orden confirmada exitosamente!")
cr.commit()
print("✅ Cambios guardados")
except Exception as e2:
print(f"❌ Nuevo error: {str(e2)}")
cr.rollback()
# Si sigue fallando, mostrar más detalles
print("\n📊 Información adicional del error:")
traceback.print_exc()
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
diagnose_order(cr)
except Exception as e:
print(f"Error general: {e}")
traceback.print_exc()

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para encontrar y corregir resultados problemáticos
"""
import odoo
def find_problematic_results(cr):
"""Encontrar resultados con múltiples valores"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("🔍 Buscando resultados problemáticos...")
# Buscar todos los resultados de tipo selección
results = env['lims.result'].search([
('parameter_value_type', '=', 'selection')
])
print(f"\n Total de resultados de selección: {len(results)}")
problematic_count = 0
for result in results:
# Contar cuántos campos de valor están establecidos
values_set = []
if result.value_numeric not in [False, 0.0]:
values_set.append(f"numeric={result.value_numeric}")
if result.value_text:
values_set.append(f"text='{result.value_text}'")
if result.value_selection:
values_set.append(f"selection='{result.value_selection}'")
if result.value_boolean:
values_set.append(f"boolean={result.value_boolean}")
if len(values_set) > 1 or (len(values_set) == 1 and 'selection' not in values_set[0]):
problematic_count += 1
print(f"\n ❌ Resultado problemático ID {result.id}:")
print(f" Prueba: {result.test_id.name}")
print(f" Parámetro: {result.parameter_id.name}")
print(f" Valores establecidos: {', '.join(values_set)}")
# Corregir: limpiar todos los valores excepto selection
print(" 🔧 Corrigiendo...")
result.with_context(skip_validation=True).write({
'value_numeric': False,
'value_text': False,
'value_boolean': False,
# No tocar value_selection por ahora
})
print(" ✓ Corregido")
if problematic_count == 0:
print("\n✅ No se encontraron resultados problemáticos")
else:
print(f"\n📊 Resumen: {problematic_count} resultados corregidos")
# Buscar la orden S00029 para verificar su estado
print("\n🔍 Verificando orden S00029...")
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if order:
print(f" Estado actual: {order.state}")
if order.state == 'draft':
print("\n🔄 Intentando confirmar S00029...")
try:
order.action_confirm()
print("✅ ¡Orden confirmada exitosamente!")
print(f" Nuevo estado: {order.state}")
# Buscar las pruebas generadas
tests = env['lims.test'].search([
('sale_order_line_id.order_id', '=', order.id)
])
print(f" Pruebas generadas: {len(tests)}")
for test in tests:
print(f" - {test.name}: {test.product_id.name}")
except Exception as e:
print(f"❌ Error al confirmar: {str(e)}")
else:
print(" La orden ya está confirmada")
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
find_problematic_results(cr)
# Hacer commit manual si todo salió bien
cr.commit()
print("\n✅ Cambios guardados exitosamente")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()

152
test/fix_order_s00029.py Normal file
View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para diagnosticar y corregir el problema con la orden S00029
"""
import odoo
import traceback
from datetime import date
def fix_order(cr):
"""Diagnosticar y corregir la orden S00029"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la orden
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if not order:
print("❌ No se encontró la orden S00029")
return
print(f"✓ Orden encontrada: {order.name}")
print(f" Cliente: {order.partner_id.name}")
print(f" Género: {order.partner_id.gender}")
print(f" Fecha nacimiento: {order.partner_id.birthdate_date}")
# Calcular edad
patient_age = None
if order.partner_id.birthdate_date:
today = date.today()
patient_age = today.year - order.partner_id.birthdate_date.year
if today.month < order.partner_id.birthdate_date.month or \
(today.month == order.partner_id.birthdate_date.month and today.day < order.partner_id.birthdate_date.day):
patient_age -= 1
print(f" Edad: {patient_age} años")
# Buscar el parámetro problemático (Prueba de Embarazo)
print("\n🔍 Buscando parámetros de selección problemáticos...")
# Query directa para encontrar el problema
cr.execute("""
SELECT
lap.id,
lap.name,
lap.code,
lap.value_type,
lap.selection_values,
COUNT(DISTINCT lpr.default_value_selection) as unique_defaults,
STRING_AGG(DISTINCT lpr.default_value_selection, ', ') as default_values
FROM lims_analysis_parameter lap
JOIN lims_parameter_range lpr ON lpr.parameter_id = lap.id
WHERE lap.value_type = 'selection'
AND lpr.default_value_selection IS NOT NULL
GROUP BY lap.id, lap.name, lap.code, lap.value_type, lap.selection_values
HAVING COUNT(DISTINCT lpr.default_value_selection) > 1
""")
problematic = cr.fetchall()
if problematic:
print("\n⚠️ Parámetros con múltiples valores por defecto:")
for p in problematic:
print(f" - {p[1]} (código: {p[2]})")
print(f" Valores posibles: {p[4]}")
print(f" Valores por defecto encontrados: {p[6]}")
# Ver específicamente el parámetro HCG (Prueba de Embarazo)
cr.execute("""
SELECT
lpr.id,
lpr.age_min,
lpr.age_max,
lpr.gender,
lpr.default_value_selection,
lpr.is_pregnant_specific
FROM lims_parameter_range lpr
JOIN lims_analysis_parameter lap ON lap.id = lpr.parameter_id
WHERE lap.code = 'HCG'
ORDER BY lpr.age_min, lpr.gender
""")
hcg_ranges = cr.fetchall()
if hcg_ranges:
print("\n📊 Rangos del parámetro HCG (Prueba de Embarazo):")
for r in hcg_ranges:
print(f" - Rango ID {r[0]}: edad {r[1]}-{r[2]}, género {r[3]}, embarazo: {r[5]}, default: '{r[4]}'")
# Intentar arreglar el problema
print("\n🔧 Intentando corregir el problema...")
# Opción 1: Eliminar valores por defecto conflictivos
# Solo dejar el valor por defecto para el caso específico de embarazo
cr.execute("""
UPDATE lims_parameter_range
SET default_value_selection = NULL
WHERE parameter_id IN (
SELECT id FROM lims_analysis_parameter WHERE code = 'HCG'
)
AND is_pregnant_specific = false
""")
print(f"✓ Eliminados valores por defecto de rangos no específicos de embarazo")
# Verificar la corrección
cr.execute("""
SELECT COUNT(*)
FROM lims_parameter_range lpr
JOIN lims_analysis_parameter lap ON lap.id = lpr.parameter_id
WHERE lap.code = 'HCG'
AND lpr.default_value_selection IS NOT NULL
""")
remaining = cr.fetchone()[0]
print(f" Rangos con valor por defecto restantes: {remaining}")
# Intentar confirmar la orden nuevamente
print("\n🔄 Intentando confirmar la orden después de la corrección...")
try:
order.action_confirm()
print("✅ ¡Orden confirmada exitosamente!")
print(f" Nuevo estado: {order.state}")
print(f" Muestras generadas: {len(order.generated_sample_ids)}")
# Confirmar los cambios
cr.commit()
print("\n✅ Cambios guardados en la base de datos")
except Exception as e:
print(f"❌ Error al confirmar: {str(e)}")
traceback.print_exc()
cr.rollback()
print("\n⚠️ Cambios revertidos")
# Si sigue fallando, mostrar más información
print("\n📋 Información adicional para debugging:")
for line in order.order_line:
if line.product_id.is_analysis:
print(f"\n Análisis: {line.product_id.name}")
template = line.product_id.product_tmpl_id
for param_config in template.parameter_ids:
print(f" - Parámetro: {param_config.parameter_id.name}")
print(f" Tipo: {param_config.parameter_value_type}")
if param_config.parameter_value_type == 'selection':
print(f" ⚠️ Es de tipo selección")
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
fix_order(cr)
except Exception as e:
print(f"Error general: {e}")
traceback.print_exc()

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para investigar y confirmar la orden S00029
Error: "Para parámetros de selección solo se debe elegir una opción"
"""
import odoo
import sys
import json
import traceback
def investigate_order(cr):
"""Investigar la orden S00029 y sus detalles"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la orden S00029
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if not order:
print("❌ No se encontró la orden S00029")
return
print(f"✓ Orden encontrada: {order.name}")
print(f" Cliente: {order.partner_id.name}")
print(f" Estado: {order.state}")
print(f" Es solicitud de lab: {order.is_lab_request}")
print(f" Doctor: {order.doctor_id.name if order.doctor_id else 'N/A'}")
print(f" Líneas de orden: {len(order.order_line)}")
# Revisar las líneas de la orden
print("\n📋 Líneas de orden:")
for i, line in enumerate(order.order_line, 1):
print(f"\n Línea {i}:")
print(f" Producto: {line.product_id.name}")
print(f" Es análisis: {line.product_id.is_analysis}")
print(f" Cantidad: {line.product_uom_qty}")
# Verificar parámetros del análisis
if line.product_id.is_analysis:
params = line.product_id.parameter_ids
print(f" Parámetros: {len(params)}")
# Ver detalles de parámetros con tipo selección
selection_params = params.filtered(lambda p: p.value_type == 'selection')
if selection_params:
print(f" ⚠️ Parámetros de selección encontrados: {len(selection_params)}")
for param in selection_params:
print(f" - {param.name} (código: {param.code})")
print(f" Opciones: {param.selection_options}")
# Verificar si hay valores predeterminados múltiples
cr.execute("""
SELECT COUNT(*)
FROM lims_parameter_range
WHERE parameter_id = %s
AND default_value_selection IS NOT NULL
""", (param.id,))
default_count = cr.fetchone()[0]
if default_count > 1:
print(f" ⚠️ PROBLEMA: {default_count} valores predeterminados para este parámetro")
# Intentar confirmar la orden
print("\n🔄 Intentando confirmar la orden...")
try:
# Primero, verificar si hay muestras generadas
print(f" Muestras generadas antes: {len(order.generated_sample_ids)}")
# Intentar confirmar
order.action_confirm()
print("✅ Orden confirmada exitosamente!")
print(f" Nuevo estado: {order.state}")
print(f" Muestras generadas después: {len(order.generated_sample_ids)}")
except Exception as e:
print(f"❌ Error al confirmar: {str(e)}")
print("\n📊 Traceback completo:")
traceback.print_exc()
# Investigar más a fondo el error
if "parámetros de selección" in str(e):
print("\n🔍 Investigando parámetros de selección...")
# Buscar todos los parámetros de selección en la base
cr.execute("""
SELECT
lap.id,
lap.name,
lap.code,
lap.selection_options,
COUNT(lpr.id) as range_count,
COUNT(lpr.default_value_selection) as default_count
FROM lims_analysis_parameter lap
LEFT JOIN lims_parameter_range lpr ON lpr.parameter_id = lap.id
WHERE lap.value_type = 'selection'
GROUP BY lap.id, lap.name, lap.code, lap.selection_options
HAVING COUNT(lpr.default_value_selection) > 1
""")
problematic_params = cr.fetchall()
if problematic_params:
print("\n⚠️ Parámetros problemáticos encontrados:")
for param in problematic_params:
print(f" - {param[1]} (código: {param[2]})")
print(f" Opciones: {param[3]}")
print(f" Rangos totales: {param[4]}")
print(f" Valores predeterminados: {param[5]}")
# Ver los valores predeterminados
cr.execute("""
SELECT
default_value_selection,
age_min,
age_max,
gender
FROM lims_parameter_range
WHERE parameter_id = %s
AND default_value_selection IS NOT NULL
""", (param[0],))
defaults = cr.fetchall()
print(" Valores predeterminados por rango:")
for default in defaults:
print(f" - '{default[0]}' para edad {default[1]}-{default[2]}, género: {default[3]}")
return order
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.registry(db_name)
with registry.cursor() as cr:
investigate_order(cr)
except Exception as e:
print(f"Error general: {e}")
traceback.print_exc()

View File

@ -0,0 +1,170 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para investigar y confirmar la orden S00029
Error: "Para parámetros de selección solo se debe elegir una opción"
"""
import odoo
import sys
import json
import traceback
def investigate_order(cr):
"""Investigar la orden S00029 y sus detalles"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la orden S00029
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if not order:
print("❌ No se encontró la orden S00029")
return
print(f"✓ Orden encontrada: {order.name}")
print(f" Cliente: {order.partner_id.name}")
print(f" Estado: {order.state}")
print(f" Es solicitud de lab: {order.is_lab_request}")
print(f" Doctor: {order.doctor_id.name if order.doctor_id else 'N/A'}")
print(f" Líneas de orden: {len(order.order_line)}")
# Revisar las líneas de la orden
print("\n📋 Líneas de orden:")
for i, line in enumerate(order.order_line, 1):
print(f"\n Línea {i}:")
print(f" Producto: {line.product_id.name}")
print(f" Es análisis: {line.product_id.is_analysis}")
print(f" Cantidad: {line.product_uom_qty}")
# Verificar parámetros del análisis
if line.product_id.is_analysis:
# Los parámetros están en product.template, no en product.product
template = line.product_id.product_tmpl_id
params = template.parameter_ids
print(f" Parámetros configurados: {len(params)}")
# Ver detalles de parámetros con tipo selección
selection_params = params.filtered(lambda p: p.parameter_value_type == 'selection')
if selection_params:
print(f" ⚠️ Parámetros de selección encontrados: {len(selection_params)}")
for param_config in selection_params:
param = param_config.parameter_id
print(f" - {param.name} (código: {param.code})")
print(f" Opciones: {param.selection_options}")
# Verificar rangos y valores predeterminados
cr.execute("""
SELECT
id,
age_min,
age_max,
gender,
default_value_selection
FROM lims_parameter_range
WHERE parameter_id = %s
ORDER BY age_min, gender
""", (param.id,))
ranges = cr.fetchall()
print(f" Rangos definidos: {len(ranges)}")
# Verificar si hay múltiples valores por defecto para el mismo grupo
default_selections = [r[4] for r in ranges if r[4]]
if len(default_selections) > 1:
print(f" ⚠️ PROBLEMA: {len(default_selections)} valores predeterminados encontrados")
for r in ranges:
if r[4]:
print(f" - Rango ID {r[0]}: edad {r[1]}-{r[2]}, género {r[3]}, default: '{r[4]}'")
# Información del paciente para determinar qué rangos aplicarían
print(f"\n👤 Información del paciente:")
print(f" Nombre: {order.partner_id.name}")
print(f" Género: {order.partner_id.gender}")
print(f" Fecha nacimiento: {order.partner_id.birthdate_date}")
if order.partner_id.birthdate_date:
from datetime import date
today = date.today()
age = today.year - order.partner_id.birthdate_date.year
if today.month < order.partner_id.birthdate_date.month or \
(today.month == order.partner_id.birthdate_date.month and today.day < order.partner_id.birthdate_date.day):
age -= 1
print(f" Edad: {age} años")
# Intentar confirmar la orden
print("\n🔄 Intentando confirmar la orden...")
try:
# Primero, verificar si hay muestras generadas
print(f" Muestras generadas antes: {len(order.generated_sample_ids)}")
# Intentar confirmar
order.action_confirm()
print("✅ Orden confirmada exitosamente!")
print(f" Nuevo estado: {order.state}")
print(f" Muestras generadas después: {len(order.generated_sample_ids)}")
except Exception as e:
print(f"❌ Error al confirmar: {str(e)}")
print("\n📊 Traceback completo:")
traceback.print_exc()
# Investigar más a fondo el error de parámetros de selección
if "parámetros de selección" in str(e):
print("\n🔍 Analizando el problema de parámetros de selección...")
# Buscar específicamente parámetros problemáticos para este paciente
patient_age = None
if order.partner_id.birthdate_date:
from datetime import date
today = date.today()
patient_age = today.year - order.partner_id.birthdate_date.year
if today.month < order.partner_id.birthdate_date.month or \
(today.month == order.partner_id.birthdate_date.month and today.day < order.partner_id.birthdate_date.day):
patient_age -= 1
patient_gender = order.partner_id.gender or 'other'
print(f"\n Buscando rangos aplicables para:")
print(f" - Edad: {patient_age}")
print(f" - Género: {patient_gender}")
# Verificar cada análisis de la orden
for line in order.order_line:
if line.product_id.is_analysis:
template = line.product_id.product_tmpl_id
for param_config in template.parameter_ids:
if param_config.parameter_value_type == 'selection':
param = param_config.parameter_id
# Buscar rangos aplicables
query = """
SELECT
id,
age_min,
age_max,
gender,
default_value_selection
FROM lims_parameter_range
WHERE parameter_id = %s
AND (age_min IS NULL OR age_min <= %s)
AND (age_max IS NULL OR age_max >= %s)
AND (gender IS NULL OR gender = %s OR gender = 'other')
AND default_value_selection IS NOT NULL
"""
cr.execute(query, (param.id, patient_age or 0, patient_age or 999, patient_gender))
applicable_ranges = cr.fetchall()
if len(applicable_ranges) > 1:
print(f"\n ⚠️ CONFLICTO en {param.name}:")
print(f" {len(applicable_ranges)} rangos con valores por defecto aplicables:")
for r in applicable_ranges:
print(f" - Rango ID {r[0]}: edad {r[1]}-{r[2]}, género {r[3]}, default: '{r[4]}'")
return order
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
investigate_order(cr)
except Exception as e:
print(f"Error general: {e}")
traceback.print_exc()

42
test/list_lab_orders.py Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para listar todas las órdenes de laboratorio
"""
import odoo
def list_orders(cr):
"""Listar todas las órdenes de laboratorio"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar todas las órdenes de laboratorio
orders = env['sale.order'].search([('is_lab_request', '=', True)], order='name desc')
print(f"📋 Total de órdenes de laboratorio: {len(orders)}\n")
for order in orders:
print(f"Orden: {order.name}")
print(f" Cliente: {order.partner_id.name}")
print(f" Estado: {order.state}")
print(f" Líneas: {len(order.order_line)}")
# Mostrar análisis
for line in order.order_line:
print(f" - {line.product_id.name}")
# Si tiene prueba de embarazo, mostrar
has_pregnancy_test = any('embarazo' in line.product_id.name.lower() for line in order.order_line)
if has_pregnancy_test:
print(" ⚠️ Incluye prueba de embarazo")
print()
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
list_orders(cr)
except Exception as e:
print(f"Error: {e}")

186
test/run_parameter_tests.py Normal file
View File

@ -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)

65
test/test_menu_action.py Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import odoo
import json
def test_menu_action(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== VERIFICANDO MENÚ Y ACCIÓN DE PRUEBAS ===\n")
# Verificar el menú
menu = env.ref('lims_management.menu_lims_tests', raise_if_not_found=False)
if menu:
print(f"✓ Menú encontrado: {menu.name}")
print(f" - ID: {menu.id}")
print(f" - Acción: {menu.action.name if menu.action else 'Sin acción'}")
print(f" - Padre: {menu.parent_id.name if menu.parent_id else 'Sin padre'}")
else:
print("✗ No se encontró el menú")
return
# Verificar la acción
action = env.ref('lims_management.action_lims_test', raise_if_not_found=False)
if action:
print(f"\n✓ Acción encontrada: {action.name}")
print(f" - ID: {action.id}")
print(f" - Modelo: {action.res_model}")
print(f" - View mode: {action.view_mode}")
# Simular la apertura de la acción
print("\n=== SIMULANDO APERTURA DE LA ACCIÓN ===")
# Obtener las vistas
model = env['lims.test']
print("\nVistas disponibles para lims.test:")
views = env['ir.ui.view'].search([('model', '=', 'lims.test')])
for view in views:
print(f" - {view.name} (tipo: {view.type})")
# Verificar que se puede crear una instancia
print("\n✓ Modelo lims.test existe y es accesible")
# Verificar acciones en formato JSON
print("\n=== DATOS DE LA ACCIÓN (JSON) ===")
action_dict = {
'id': action.id,
'name': action.name,
'res_model': action.res_model,
'view_mode': action.view_mode,
'context': action.context,
'domain': action.domain,
'type': action.type,
}
print(json.dumps(action_dict, indent=2))
else:
print("✗ No se encontró la acción")
print("\n=== VERIFICACIÓN COMPLETADA ===")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
test_menu_action(cr)

View File

@ -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()

29
test/update_module.py Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import odoo
def update_module(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== ACTUALIZANDO MÓDULO lims_management ===\n")
# Buscar el módulo
module = env['ir.module.module'].search([('name', '=', 'lims_management')])
if module:
print(f"Módulo encontrado: {module.name}")
print(f"Estado actual: {module.state}")
# Actualizar el módulo
module.button_immediate_upgrade()
print("Módulo actualizado exitosamente")
else:
print("❌ No se encontró el módulo lims_management")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
update_module(cr)
cr.commit()

159
test/verify_demo_data.py Normal file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para verificar los datos de demostración cargados.
"""
import odoo
import json
def verify_demo_data(cr):
"""Verificar datos de demostración"""
# Verificar parámetros
cr.execute("""
SELECT COUNT(*) as total,
COUNT(DISTINCT value_type) as tipos
FROM lims_analysis_parameter
""")
params = cr.fetchone()
# Verificar rangos
cr.execute("""
SELECT COUNT(*) as total,
COUNT(DISTINCT parameter_id) as parametros_con_rangos
FROM lims_parameter_range
""")
ranges = cr.fetchone()
# Verificar configuración de parámetros en análisis
cr.execute("""
SELECT pt.name as analisis,
COUNT(ptp.id) as parametros_configurados
FROM product_template pt
LEFT JOIN product_template_parameter ptp ON ptp.product_tmpl_id = pt.id
WHERE pt.is_analysis = true
GROUP BY pt.id, pt.name
ORDER BY pt.name
""")
analysis_config = cr.fetchall()
# Verificar órdenes de laboratorio
cr.execute("""
SELECT COUNT(*) as total_ordenes,
COUNT(DISTINCT partner_id) as pacientes_distintos,
COUNT(CASE WHEN state = 'sale' THEN 1 END) as confirmadas
FROM sale_order
WHERE is_lab_request = true
""")
orders = cr.fetchone()
# Verificar muestras
cr.execute("""
SELECT COUNT(*) as total_muestras,
COUNT(DISTINCT sample_state) as estados_distintos
FROM stock_lot
WHERE is_lab_sample = true
""")
samples = cr.fetchone()
# Verificar pruebas
cr.execute("""
SELECT COUNT(*) as total_pruebas,
COUNT(CASE WHEN state = 'validated' THEN 1 END) as validadas,
COUNT(CASE WHEN state = 'result_entered' THEN 1 END) as con_resultados
FROM lims_test
""")
tests = cr.fetchone()
# Verificar resultados
cr.execute("""
SELECT COUNT(*) as total_resultados,
COUNT(CASE WHEN is_out_of_range = true THEN 1 END) as fuera_rango,
COUNT(CASE WHEN is_critical = true THEN 1 END) as criticos
FROM lims_result
""")
results = cr.fetchone()
return {
'parametros': {
'total': params[0],
'tipos_distintos': params[1]
},
'rangos': {
'total': ranges[0],
'parametros_con_rangos': ranges[1]
},
'analisis_configurados': [
{'analisis': row[0], 'parametros': row[1]}
for row in analysis_config
],
'ordenes': {
'total': orders[0],
'pacientes_distintos': orders[1],
'confirmadas': orders[2]
},
'muestras': {
'total': samples[0],
'estados_distintos': samples[1]
},
'pruebas': {
'total': tests[0],
'validadas': tests[1],
'con_resultados': tests[2]
},
'resultados': {
'total': results[0],
'fuera_rango': results[1],
'criticos': results[2]
}
}
if __name__ == '__main__':
import sys
sys.path.insert(0, '/usr/lib/python3/dist-packages')
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
data = verify_demo_data(cr)
print("\n" + "="*60)
print("VERIFICACIÓN DE DATOS DE DEMOSTRACIÓN")
print("="*60)
print(f"\n📊 PARÁMETROS DE ANÁLISIS:")
print(f" - Total: {data['parametros']['total']}")
print(f" - Tipos distintos: {data['parametros']['tipos_distintos']}")
print(f"\n📏 RANGOS DE REFERENCIA:")
print(f" - Total: {data['rangos']['total']}")
print(f" - Parámetros con rangos: {data['rangos']['parametros_con_rangos']}")
print(f"\n🧪 ANÁLISIS CONFIGURADOS:")
for item in data['analisis_configurados']:
if item['parametros'] > 0:
print(f" - {item['analisis']}: {item['parametros']} parámetros")
print(f"\n📋 ÓRDENES DE LABORATORIO:")
print(f" - Total: {data['ordenes']['total']}")
print(f" - Pacientes distintos: {data['ordenes']['pacientes_distintos']}")
print(f" - Confirmadas: {data['ordenes']['confirmadas']}")
print(f"\n🧪 MUESTRAS:")
print(f" - Total: {data['muestras']['total']}")
print(f" - Estados distintos: {data['muestras']['estados_distintos']}")
print(f"\n🔬 PRUEBAS:")
print(f" - Total: {data['pruebas']['total']}")
print(f" - Validadas: {data['pruebas']['validadas']}")
print(f" - Con resultados: {data['pruebas']['con_resultados']}")
print(f"\n📊 RESULTADOS:")
print(f" - Total: {data['resultados']['total']}")
print(f" - Fuera de rango: {data['resultados']['fuera_rango']}")
print(f" - Críticos: {data['resultados']['criticos']}")
print("\n" + "="*60)