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:
commit
6c3fa0bc4d
|
@ -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": []
|
||||
}
|
||||
|
|
98
CLAUDE.md
98
CLAUDE.md
|
@ -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
|
1215
documents/logs/log_odoo_init.txt
Normal file
1215
documents/logs/log_odoo_init.txt
Normal file
File diff suppressed because it is too large
Load Diff
173
documents/plans/issue-51-implementation-plan.md
Normal file
173
documents/plans/issue-51-implementation-plan.md
Normal 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.
|
49
init_odoo.py
49
init_odoo.py
|
@ -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...")
|
||||
|
|
|
@ -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,
|
||||
|
|
363
lims_management/demo/analysis_parameter_config_demo.xml
Normal file
363
lims_management/demo/analysis_parameter_config_demo.xml
Normal 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>
|
339
lims_management/demo/parameter_demo.xml
Normal file
339
lims_management/demo/parameter_demo.xml
Normal 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>
|
374
lims_management/demo/parameter_range_demo.xml
Normal file
374
lims_management/demo/parameter_range_demo.xml
Normal 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: <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: <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: <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>
|
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
144
lims_management/models/analysis_parameter.py
Normal file
144
lims_management/models/analysis_parameter.py
Normal 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)
|
|
@ -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")
|
|
@ -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 = 'Sí' if record.value_boolean else 'No'
|
||||
else:
|
||||
record.is_out_of_range = False
|
||||
record.value_display = ''
|
||||
|
||||
@api.constrains('value_numeric', 'value_text', 'value_selection')
|
||||
def _check_single_value_type(self):
|
||||
"""Asegura que solo un tipo de valor esté lleno."""
|
||||
@api.depends('parameter_id', 'patient_id', 'test_date')
|
||||
def _compute_applicable_range(self):
|
||||
"""Determina el rango de referencia aplicable según el paciente."""
|
||||
for record in self:
|
||||
filled_values = 0
|
||||
if record.value_numeric:
|
||||
filled_values += 1
|
||||
if record.value_text:
|
||||
filled_values += 1
|
||||
if record.value_selection:
|
||||
filled_values += 1
|
||||
if not record.parameter_id or not record.patient_id:
|
||||
record.applicable_range_id = False
|
||||
continue
|
||||
|
||||
if filled_values > 1:
|
||||
# Calcular edad del paciente en la fecha del test
|
||||
if record.test_date:
|
||||
age = record.patient_id.get_age_at_date(record.test_date.date())
|
||||
else:
|
||||
age = record.patient_id.age
|
||||
|
||||
# Buscar rango más específico
|
||||
domain = [
|
||||
('parameter_id', '=', record.parameter_id.id),
|
||||
('age_min', '<=', age),
|
||||
('age_max', '>=', age),
|
||||
'|',
|
||||
('gender', '=', record.patient_id.gender),
|
||||
('gender', '=', 'both')
|
||||
]
|
||||
|
||||
# Considerar embarazo si aplica
|
||||
if record.patient_id.gender == 'female' and record.patient_id.is_pregnant:
|
||||
domain.append(('pregnant', '=', True))
|
||||
|
||||
# Ordenar para obtener el más específico primero
|
||||
ranges = self.env['lims.parameter.range'].search(
|
||||
domain,
|
||||
order='gender desc, pregnant desc',
|
||||
limit=1
|
||||
)
|
||||
|
||||
record.applicable_range_id = ranges[0] if ranges else False
|
||||
|
||||
@api.depends('value_numeric', 'applicable_range_id', 'parameter_value_type')
|
||||
def _compute_is_out_of_range(self):
|
||||
"""Determina si el valor está fuera del rango normal y si es crítico."""
|
||||
for record in self:
|
||||
record.is_out_of_range = False
|
||||
record.is_critical = False
|
||||
|
||||
# Solo aplica para valores numéricos
|
||||
if record.parameter_value_type != 'numeric' or record.value_numeric is False:
|
||||
continue
|
||||
|
||||
if not record.applicable_range_id:
|
||||
continue
|
||||
|
||||
range_obj = record.applicable_range_id
|
||||
status = range_obj.get_value_status(record.value_numeric)
|
||||
|
||||
record.is_out_of_range = (status != 'normal')
|
||||
record.is_critical = (status == 'critical')
|
||||
|
||||
@api.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
|
|
@ -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()
|
||||
|
|
214
lims_management/models/parameter_range.py
Normal file
214
lims_management/models/parameter_range.py
Normal 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'
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
109
lims_management/models/product_template_parameter.py
Normal file
109
lims_management/models/product_template_parameter.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
80
lims_management/tests/README.md
Normal file
80
lims_management/tests/README.md
Normal 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
|
5
lims_management/tests/__init__.py
Normal file
5
lims_management/tests/__init__.py
Normal 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
|
175
lims_management/tests/test_analysis_parameter.py
Normal file
175
lims_management/tests/test_analysis_parameter.py
Normal 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)
|
283
lims_management/tests/test_auto_result_generation.py
Normal file
283
lims_management/tests/test_auto_result_generation.py
Normal 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
|
249
lims_management/tests/test_parameter_range.py
Normal file
249
lims_management/tests/test_parameter_range.py
Normal 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)
|
291
lims_management/tests/test_result_parameter_integration.py
Normal file
291
lims_management/tests/test_result_parameter_integration.py
Normal 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, 'Sí')
|
136
lims_management/views/analysis_parameter_views.xml
Normal file
136
lims_management/views/analysis_parameter_views.xml
Normal 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>
|
|
@ -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 -->
|
||||
|
|
162
lims_management/views/lims_result_bulk_entry_views.xml
Normal file
162
lims_management/views/lims_result_bulk_entry_views.xml
Normal 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>
|
148
lims_management/views/lims_result_views.xml
Normal file
148
lims_management/views/lims_result_views.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
159
lims_management/views/parameter_dashboard_views.xml
Normal file
159
lims_management/views/parameter_dashboard_views.xml
Normal 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>
|
125
lims_management/views/parameter_range_views.xml
Normal file
125
lims_management/views/parameter_range_views.xml
Normal 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">< </span>
|
||||
<field name="critical_min" class="oe_inline"/>
|
||||
<span class="oe_inline"> o > </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 (<18)" name="pediatric"
|
||||
domain="[('age_min', '<', 18)]"/>
|
||||
<filter string="Adulto (18-65)" name="adult"
|
||||
domain="[('age_min', '>=', 18), ('age_max', '<=', 65)]"/>
|
||||
<filter string="Geriátrico (>65)" name="geriatric"
|
||||
domain="[('age_max', '>', 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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
93
lims_management/views/product_template_parameter_views.xml
Normal file
93
lims_management/views/product_template_parameter_views.xml
Normal 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>
|
|
@ -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
87
test/check_new_results.py
Normal 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
59
test/check_views.py
Normal 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)
|
51
test/check_views_action.py
Normal file
51
test/check_views_action.py
Normal 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)
|
76
test/confirm_order_s00027.py
Normal file
76
test/confirm_order_s00027.py
Normal 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
415
test/create_demo_data.py
Normal 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()
|
|
@ -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
103
test/diagnose_s00029.py
Normal 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()
|
96
test/find_problematic_results.py
Normal file
96
test/find_problematic_results.py
Normal 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
152
test/fix_order_s00029.py
Normal 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()
|
133
test/investigate_order_s00029.py
Normal file
133
test/investigate_order_s00029.py
Normal 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()
|
170
test/investigate_order_s00029_v2.py
Normal file
170
test/investigate_order_s00029_v2.py
Normal 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
42
test/list_lab_orders.py
Normal 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
186
test/run_parameter_tests.py
Normal 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
65
test/test_menu_action.py
Normal 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)
|
221
test/test_parameters_simple.py
Normal file
221
test/test_parameters_simple.py
Normal 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
29
test/update_module.py
Normal 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
159
test/verify_demo_data.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user