Compare commits

...

13 Commits

Author SHA1 Message Date
Luis Ernesto Portillo Zaldivar
3c9d8f5bf0 fix: Agregar dependencia del módulo stock en __manifest__.py
Resuelve el error "Model 'stock.lot' does not exist in registry"
agregando 'stock' a la lista de dependencias del módulo.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 11:26:46 -06:00
a1219640f1 Merge pull request 'fix: Corregir generación de secuencias en lims.test' (#76) from feature/71-laboratory-dashboards into dev
Reviewed-on: #76
2025-07-23 23:01:55 +00:00
Luis Ernesto Portillo Zaldivar
754c5f5572 fix: Corregir generación de secuencias en lims.test
- Eliminar método create duplicado que sobrescribía la lógica de secuencias
- Consolidar la generación de secuencias en un único método create
- Agregar contexto especial para evitar validaciones durante la inicialización
- Ahora todos los tests se crean con códigos secuenciales (LAB-YYYY-NNNNN)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 16:59:26 -06:00
Luis Ernesto Portillo Zaldivar
64f44b9d2c feat: Implementar autocompletado de notas para resultados críticos en lims.result 2025-07-21 17:32:32 -06:00
73e3014036 Merge pull request 'feat(#71): Implementar dashboards para administrador del laboratorio' (#74) from feature/71-laboratory-dashboards into dev
Reviewed-on: #74
2025-07-21 22:53:31 +00:00
Luis Ernesto Portillo Zaldivar
53eada8432 feat(#71): Mejorar script de creación de órdenes de laboratorio - 2 órdenes por paciente con manejo de errores 2025-07-21 16:09:39 -06:00
Luis Ernesto Portillo Zaldivar
1ff44b1654 feat(#71): Agregar DUI salvadoreño y números de teléfono de El Salvador a pacientes 2025-07-21 14:36:18 -06:00
cf1e40726f Merge pull request 'feat(#71): Dashboards para administrador del laboratorio' (#73) from feature/71-laboratory-dashboards into dev
Reviewed-on: #73
2025-07-18 18:13:41 +00:00
Luis Ernesto Portillo Zaldivar
02237c6d8c fix(#71): Corregir errores en dashboards y scripts de inicialización
- Cambiar 'tree' por 'list' en view_mode de todas las acciones de dashboard
- Corregir sintaxis de filtros de fecha usando context_today() y relativedelta
- Eliminar campo booleano is_out_of_range como medida en gráfico
- Corregir referencia a sample.state en lugar de sample.sample_state
- Reemplazar sample.test_ids por búsqueda de tests asociados
- Eliminar consulta SQL directa a columna logo inexistente
- Corregir método invalidate_cache() por _invalidate_cache()
- Agregar sección de notificaciones en CLAUDE.md

Los dashboards ahora funcionan correctamente sin errores de JavaScript.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 12:11:01 -06:00
Luis Ernesto Portillo Zaldivar
753b84936e fix(#71): Corregir sintaxis de view_ids en acciones de dashboards
- Cambiar graph_view_id y pivot_view_id por view_ids con sintaxis correcta
- Usar eval con lista de tuplas según formato Odoo estándar
- Resolver error 'Invalid field graph_view_id on model ir.actions.act_window'

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 13:31:28 -06:00
Luis Ernesto Portillo Zaldivar
266b3f28be fix(#71): Agregar referencias explícitas de vistas en acciones de dashboards
- Agregar graph_view_id y pivot_view_id en todas las acciones
- Resolver error 'View types not defined tree found in act_window action'
- Mantener view_mode con 'tree' según convención Odoo 18

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 12:55:48 -06:00
Luis Ernesto Portillo Zaldivar
d51d3b5d69 feat(#71): Implementar dashboards para administrador del laboratorio
- Dashboard de Estado de Órdenes: Vista gráfica y pivot de órdenes por estado
- Dashboard de Productividad de Técnicos: Análisis de pruebas por técnico
- Dashboard de Muestras: Estado y distribución de muestras por tipo
- Dashboard de Parámetros Fuera de Rango: Identificación de resultados críticos
- Dashboard de Análisis Más Solicitados: Top de análisis por período
- Dashboard de Distribución Demográfica: Tests por género y rango de edad
- Agregar campos computed age_range, patient_gender y patient_age_range
- Configurar menú de Dashboards solo para administradores

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 11:17:26 -06:00
2ca64186b0 Merge pull request 'feat(#67): Implementar autocompletado inteligente para campos de selección' (#72) from feature/67-smart-selection-autocomplete into dev
Reviewed-on: #72
2025-07-17 08:33:07 +00:00
25 changed files with 1947 additions and 328 deletions

View File

@ -27,7 +27,9 @@
"Bash(gh pr merge:*)",
"Bash(git cherry-pick:*)",
"Bash(del comment_issue_15.txt)",
"Bash(cat:*)"
"Bash(cat:*)",
"Bash(powershell.exe:*)",
"Bash(gh pr create:*)"
],
"deny": []
}

View File

@ -2,6 +2,11 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Notifications
When tasks complete, or you need autorizathion for an action notify me using:
powershell.exe -c "[System.Media.SystemSounds]::Beep.Play()"
## Project Overview
This is a Laboratory Information Management System (LIMS) module for Odoo 18 ERP, specifically designed for clinical laboratories. The module manages patients, samples, analyses, and test results.
@ -16,6 +21,7 @@ This is a Laboratory Information Management System (LIMS) module for Odoo 18 ERP
## Development Commands
### Starting the Environment
```bash
# Start all services
docker-compose up -d
@ -30,10 +36,13 @@ docker-compose down -v
**IMPORTANT**: Odoo initialization takes approximately 5 minutes. When using docker-compose commands, set timeout to 5 minutes (300000ms) to avoid premature timeouts.
### Instance Persistence Policy
After successful installation/update, the instance must remain active for user validation. Do NOT stop the instance until user explicitly confirms testing is complete.
### MANDATORY Testing Rule
**CRITICAL**: After EVERY task that modifies code, models, views, or data:
1. Restart the ephemeral instance: `docker-compose down -v && docker-compose up -d`
2. Check initialization logs for errors: `docker-compose logs odoo_init | grep -i "error\|traceback\|exception"`
3. Verify successful completion: `docker-compose logs odoo_init | tail -30`
@ -41,7 +50,9 @@ After successful installation/update, the instance must remain active for user v
5. If errors are found, fix them before continuing
### Development Workflow per Task
When implementing issues with multiple tasks, follow this workflow for EACH task:
1. **Stop instance**: `docker-compose down -v`
2. **Implement the task**: Make code changes
3. **Start instance**: `docker-compose up -d` (timeout: 300000ms)
@ -54,15 +65,18 @@ When implementing issues with multiple tasks, follow this workflow for EACH task
### Database Operations
#### Direct PostgreSQL Access
```bash
# Connect to PostgreSQL
docker exec -it lims_db psql -U odoo -d odoo
```
#### Python Script Method (Recommended)
For complex queries, use Python scripts with Odoo ORM:
1. Create script (e.g., `test/verify_products.py`):
```python
import odoo
import json
@ -80,16 +94,19 @@ if __name__ == '__main__':
```
2. Copy to container:
```bash
docker cp test/verify_products.py lims_odoo:/tmp/verify_products.py
```
3. Execute:
```bash
docker-compose exec odoo python3 /tmp/verify_products.py
```
### Gitea Integration
```bash
# Create issue
python utils/gitea_cli_helper.py create-issue --title "Title" --body "Description\nSupports multiple lines"
@ -116,12 +133,14 @@ python utils/gitea_cli_helper.py list-open-issues
## Mandatory Reading
At the start of each work session, read these documents to understand requirements and technical design:
- `documents/requirements/RequerimientoInicial.md`
- `documents/requirements/ToBeDesing.md`
## Code Architecture
### Module Structure
- **lims_management/models/**: Core business logic
- `partner.py`: Patient and healthcare provider management
- `product.py`: Analysis types and categories
@ -132,31 +151,37 @@ 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>` 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`:
```xml
<!-- Wrong (Odoo < 17) -->
<field name="field" attrs="{'invisible': [('condition', '=', False)]}"/>
<!-- Correct (Odoo 18) -->
<field name="field" invisible="not condition"/>
<field name="field" invisible="condition == False"/>
```
#### Context with ref()
- Use `eval` attribute when using `ref()` in action contexts:
```xml
<!-- Wrong - ref() undefined in client -->
<field name="context">{'default_categ_id': ref('module.xml_id')}</field>
<!-- Correct - evaluated on server -->
<field name="context" eval="{'default_categ_id': ref('module.xml_id')}"/>
```
#### XPath in View Inheritance
- Use flexible XPath expressions for robustness:
```xml
<!-- More robust - works with list or tree -->
@ -166,13 +191,15 @@ At the start of each work session, read these documents to understand requiremen
```
### Data Management
- **Initial Data**: `lims_management/data/` - Sequences, categories, basic configuration
- **Demo Data**:
- **Demo Data**:
- XML files in `lims_management/demo/`
- Python scripts in `test/` directory for complex demo data creation
- Use `noupdate="1"` for demo data to prevent reloading
### Security Model
- Access rights defined in `security/ir.model.access.csv`
- Field-level security in `security/security.xml`
- Group-based permissions: Laboratory Technician, Manager, etc.
@ -180,6 +207,7 @@ At the start of each work session, read these documents to understand requiremen
## Environment Variables
Required in `.env` file:
- `GITEA_API_KEY`: Personal Access Token for Gitea
- `GITEA_API_KEY_URL`: Gitea API base URL (e.g., `https://gitea.grupoconsiti.com/api/v1/`)
- `GITEA_USERNAME`: Gitea username (repository owner)
@ -188,6 +216,7 @@ Required in `.env` file:
## Important Patterns
### Sample Lifecycle States
```python
STATE_PENDING_COLLECTION = 'pending_collection'
STATE_COLLECTED = 'collected'
@ -197,6 +226,7 @@ STATE_CANCELLED = 'cancelled'
```
### Barcode Generation
- 13-digit format: YYMMDDNNNNNNC
- Uses `barcode` Python library for Code-128 generation
- Stored as PDF with human-readable text
@ -204,30 +234,34 @@ STATE_CANCELLED = 'cancelled'
### Demo Data Creation
#### XML Files (Simple Data)
- Use for basic records without complex dependencies
- Place in `lims_management/demo/`
- Use `noupdate="1"` to prevent reloading
- **IMPORTANT**: Do NOT create sale.order records in XML demo files - use Python scripts instead
#### Python Scripts (Complex Data)
For data with dependencies or business logic:
#### Test Scripts
- **IMPORTANT**: Always create test scripts inside the `test/` folder within the project directory
- Example: `test/test_sample_generation.py`
- This ensures scripts are properly organized and accessible
1. Create script:
```python
import odoo
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Use ref() to get existing records
patient1 = env.ref('lims_management.demo_patient_1')
hemograma = env.ref('lims_management.analysis_hemograma')
# Create records with business logic
env['sale.order'].create({
'partner_id': patient1.id,
@ -251,53 +285,64 @@ if __name__ == '__main__':
## Git Workflow
### Pre-commit Hook
Automatically installed via `scripts/install_hooks.sh`:
- Prevents commits to 'main' or 'dev' branches
- Enforces feature branch workflow
### Branch Naming
- Feature branches: `feature/XX-description` (where XX is issue number)
- Always create PRs to 'dev' branch, not 'main'
## Desarrollo de nuevos modelos y vistas
### Orden de carga en __manifest__.py
### Orden de carga en **manifest**.py
Al agregar archivos al manifest, seguir SIEMPRE este orden:
1. security/*.xml (grupos y categorías)
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:
3. data/\*.xml (secuencias, categorías, datos base)
4. views/\*\_views.xml en este orden específico:
- Modelos base (sin dependencias)
- Modelos dependientes
- Modelos dependientes
- Vistas que referencian acciones
- menus.xml (SIEMPRE al final de views)
5. wizards/*.xml
6. reports/*.xml
7. demo/*.xml
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?
@ -309,17 +354,20 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
### 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`
@ -330,22 +378,27 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
#### 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
- **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
@ -354,16 +407,18 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
### Manejo de códigos de barras en reportes QWeb (Odoo 18)
#### Generación de códigos de barras
Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
```xml
<!-- CORRECTO en Odoo 18 -->
<span t-field="record.barcode_field"
<span t-field="record.barcode_field"
t-options="{'widget': 'barcode', 'type': 'Code128', 'width': 250, 'height': 60, 'humanreadable': 1}"
style="display: block;"/>
```
#### Consideraciones importantes:
1. **NO usar** rutas directas como `/report/barcode/Code128/` - esta sintaxis está deprecated
2. **Usar siempre** `t-field` con el widget barcode para renderizado correcto
3. **Parámetros disponibles** en t-options:
@ -375,19 +430,23 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
#### Problemas comunes y soluciones:
##### Código de barras vacío en PDF
- **Causa**: Campo computed sin store=True o sintaxis incorrecta
- **Solución**: Asegurar que el campo esté almacenado y usar widget barcode
##### Caracteres especiales en reportes (tildes, ñ)
- **Problema**: Aparecen como "ñ" o "í" en lugar de "ñ" o "í"
- **Solución**: Usar referencias numéricas de caracteres XML:
```xml
<!-- En lugar de -->
<h4>LABORATORIO CLÍNICO</h4>
<!-- Usar -->
<h4>LABORATORIO CL&#205;NICO</h4>
```
- í = &#237;
- Í = &#205;
- á = &#225;
@ -402,15 +461,16 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
- Ñ = &#209;
##### Layout de etiquetas múltiples por página
```xml
<!-- Contenedor principal sin salto de página -->
<div class="page">
<t t-foreach="docs" t-as="o">
<!-- Cada etiqueta como inline-block -->
<div style="display: inline-block; vertical-align: top;
<div style="display: inline-block; vertical-align: top;
page-break-inside: avoid; overflow: hidden;">
<!-- Contenido de la etiqueta -->
</div>
</t>
</div>
```
```

78
dashboard_analysis.md Normal file
View File

@ -0,0 +1,78 @@
# Análisis de Dashboards para LIMS - Issue #71
## Dashboards Implementables sin Módulos Adicionales ni Cambios Estructurales
### 1. ✅ Dashboard de Estado de Órdenes
**Factibilidad**: Alta
- Usar vistas graph y pivot nativas de Odoo
- Datos disponibles: sale.order con is_lab_request=True
- Métricas: órdenes por estado, por fecha, por paciente
### 2. ✅ Dashboard de Productividad de Técnicos
**Factibilidad**: Alta
- Datos disponibles: lims.test (technician_id, state, create_date, validation_date)
- Métricas: pruebas procesadas por técnico, tiempos promedio, estados
### 3. ✅ Dashboard de Muestras
**Factibilidad**: Alta
- Datos disponibles: stock.lot con is_lab_sample=True
- Métricas: muestras por estado, rechazos, re-muestreos
### 4. ✅ Dashboard de Parámetros Fuera de Rango
**Factibilidad**: Alta
- Datos disponibles: lims.result (is_out_of_range, is_critical)
- Métricas: resultados críticos, fuera de rango por parámetro
### 5. ✅ Dashboard de Análisis Más Solicitados
**Factibilidad**: Alta
- Datos disponibles: sale.order.line con productos is_analysis=True
- Métricas: top análisis, tendencias por período
### 6. ⚠️ Dashboard de Tiempos de Respuesta
**Factibilidad**: Media
- Requiere campos calculados (no almacenados actualmente)
- Necesitaría agregar campos store=True para métricas de tiempo
### 7. ❌ Dashboard de Facturación
**Factibilidad**: Baja
- Requiere módulo account (facturación)
- No está en las dependencias actuales
### 8. ❌ Dashboard de Inventario de Reactivos
**Factibilidad**: Baja
- Requiere configuración adicional de stock
- No hay modelo específico para reactivos
## Implementación Técnica
### Herramientas Disponibles en Odoo 18:
1. **Vistas Graph**: Gráficos de barras, líneas, pie
2. **Vistas Pivot**: Tablas dinámicas
3. **Vistas Cohort**: Análisis de cohortes
4. **Filtros y Agrupaciones**: Para segmentar datos
5. **Acciones de Servidor**: Para cálculos complejos
### Estructura Propuesta:
```xml
<!-- Menú principal de Dashboards -->
<menuitem id="menu_lims_dashboards"
name="Dashboards"
parent="lims_management.menu_lims_root"
sequence="5"
groups="group_lims_admin,group_lims_manager"/>
```
## Recomendación
Sugiero comenzar con los 5 dashboards marcados con ✅ ya que:
1. Utilizan datos existentes
2. No requieren cambios en modelos
3. Usan herramientas nativas de Odoo
4. Proveen valor inmediato al administrador
Orden de implementación sugerido:
1. Dashboard de Estado de Órdenes (más básico)
2. Dashboard de Productividad de Técnicos
3. Dashboard de Muestras
4. Dashboard de Parámetros Fuera de Rango
5. Dashboard de Análisis Más Solicitados

View File

@ -1,24 +0,0 @@
## Descripción
Actualmente, cuando se cancela una orden de laboratorio, las muestras asociadas permanecen activas y no se descartan automáticamente. Esto puede causar confusión ya que quedan muestras "huérfanas" en el sistema que ya no tienen una orden válida.
## Comportamiento esperado
Cuando se cancela una orden de laboratorio:
1. Todas las muestras generadas asociadas a esa orden deben cambiar automáticamente su estado a "cancelled"
2. Si hay pruebas (lims.test) asociadas a esas muestras, también deben cancelarse
3. Se debe registrar en el chatter de la muestra que fue cancelada debido a la cancelación de la orden
## Criterios de aceptación
- [ ] Al cancelar una orden de laboratorio, todas sus muestras asociadas se marcan como canceladas
- [ ] Las pruebas asociadas a las muestras también se cancelan
- [ ] Se registra un mensaje en el chatter de cada muestra indicando la razón de cancelación
- [ ] Si una muestra ya estaba cancelada o completada, no se modifica
- [ ] La acción es reversible: si se vuelve a poner la orden en borrador, las muestras NO deben reactivarse automáticamente
## Notas técnicas
- El método a modificar es `action_cancel()` en el modelo `sale.order`
- Verificar el campo `generated_sample_ids` para obtener las muestras asociadas
- Solo cancelar muestras que estén en estados: 'pending_collection', 'collected', 'in_analysis'

View File

@ -1,38 +0,0 @@
**Contexto:**
Para poder implementar la automatización de generación de muestras (Issue #32), es necesario establecer una relación entre los productos tipo análisis y los tipos de muestra que requieren.
**Problema Actual:**
- Los productos tipo test (is_analysis=True) no tienen campo que indique qué tipo de muestra requieren
- Los productos tipo muestra (is_sample_type=True) no están relacionados con los tests
- El modelo stock.lot tiene container_type como Selection hardcodeado, no como relación
**Tareas Requeridas:**
1. **Modificar product.template:**
- Agregar campo Many2one 'required_sample_type_id' que relacione análisis con tipo de muestra
- Domain: [('is_sample_type', '=', True)]
2. **Actualizar stock.lot:**
- Opción A: Cambiar container_type de Selection a Many2one hacia product.template
- Opción B: Agregar nuevo campo sample_type_product_id
- Mantener compatibilidad con datos existentes
3. **Actualizar vistas:**
- Agregar campo en formulario de productos cuando is_analysis=True
- Mostrar tipo de muestra requerida en vistas de análisis
4. **Migración de datos:**
- Mapear valores actuales de container_type a productos tipo muestra
- Actualizar registros existentes
5. **Actualizar demo data:**
- Asignar tipos de muestra correctos a cada análisis
- Ejemplo: Hemograma → Tubo EDTA, Glucosa → Tubo Suero
**Beneficios:**
- Permitirá automatizar la generación de muestras al confirmar órdenes
- Evitará errores al saber exactamente qué contenedor usar para cada test
- Facilitará la agrupación de análisis que usan el mismo tipo de muestra
**Dependencia:**
Este issue es prerequisito para poder implementar el Issue #32

View File

@ -0,0 +1,104 @@
# Determinar automáticamente valores críticos/anormales para parámetros de selección múltiple
## Descripción
Actualmente, el sistema puede determinar automáticamente si un valor numérico es crítico basándose en rangos mínimos y máximos. Sin embargo, para parámetros de tipo selección (como Positivo/Negativo, Reactivo/No Reactivo), no existe una forma dinámica de determinar cuándo un valor es crítico o anormal.
## Problema actual
Los parámetros de selección múltiple no tienen forma de indicar qué valores son:
- Normales
- Anormales
- Críticos
Ejemplos de parámetros afectados:
- Prueba de embarazo: Positivo/Negativo
- HIV: Reactivo/No Reactivo/Indeterminado
- Hepatitis: Reactivo/No Reactivo
- Otros marcadores infecciosos
## Solución propuesta
### Opción 1: Agregar campos al modelo `lims.analysis.parameter`
Agregar campos que permitan definir qué valores de selección son críticos:
```python
critical_values = fields.Text(
string="Valores Críticos",
help="Lista de valores separados por coma que se consideran críticos"
)
abnormal_values = fields.Text(
string="Valores Anormales",
help="Lista de valores separados por coma que se consideran anormales"
)
```
### Opción 2: Crear modelo relacionado `lims.parameter.selection.value`
Crear un modelo que defina cada opción de selección con sus propiedades:
```python
class LimsParameterSelectionValue(models.Model):
_name = 'lims.parameter.selection.value'
parameter_id = fields.Many2one('lims.analysis.parameter')
value = fields.Char(string="Valor")
is_normal = fields.Boolean(string="Es Normal", default=True)
is_critical = fields.Boolean(string="Es Crítico", default=False)
sequence = fields.Integer(string="Secuencia")
notes_template = fields.Text(string="Plantilla de Notas")
```
### Opción 3: Usar configuración JSON
Almacenar la configuración en un campo JSON:
```python
selection_config = fields.Json(
string="Configuración de Valores",
help="Configuración de valores normales, anormales y críticos"
)
```
## Beneficios esperados
1. **Automatización completa**: El sistema podrá determinar automáticamente si cualquier tipo de resultado es crítico
2. **Flexibilidad**: Cada laboratorio podrá configurar qué valores considera críticos según sus protocolos
3. **Consistencia**: Aplicación uniforme de criterios en todos los resultados
4. **Alertas mejoradas**: Mejor identificación de resultados que requieren atención inmediata
## Casos de uso
1. **Prueba de embarazo**:
- Normal: Negativo (para pacientes no embarazadas)
- Anormal: Positivo (puede requerir seguimiento)
- Crítico: Indeterminado (requiere repetición)
2. **HIV**:
- Normal: No Reactivo
- Crítico: Reactivo, Indeterminado
3. **Marcadores tumorales**:
- Normal: Negativo, No Detectado
- Anormal: Débilmente Positivo
- Crítico: Positivo, Fuertemente Positivo
## Consideraciones técnicas
- Mantener compatibilidad con el sistema actual
- Permitir migración de datos existentes
- Interfaz de usuario intuitiva para configuración
- Integración con el autocompletado de notas críticas existente
## Tareas propuestas
1. Análisis de la mejor opción de implementación
2. Diseño del modelo de datos
3. Implementación de campos/modelos necesarios
4. Actualización de la lógica de `is_critical` en `lims.result`
5. Creación de interfaz de configuración
6. Migración de parámetros existentes
7. Pruebas exhaustivas
8. Documentación
## Prioridad
Media-Alta: Esta mejora completaría la funcionalidad de detección automática de valores críticos para todos los tipos de parámetros.

View File

@ -16,7 +16,7 @@
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
'category': 'Industries',
'version': '18.0.1.0.0',
'depends': ['base', 'product', 'sale', 'base_setup'],
'depends': ['base', 'product', 'sale', 'stock', 'base_setup'],
'assets': {
'web.assets_backend': [
'lims_management/static/src/css/lims_test.css',
@ -45,6 +45,7 @@
'views/analysis_parameter_views.xml',
'views/product_template_parameter_config_views.xml',
'views/parameter_dashboard_views.xml',
'views/dashboard_views.xml',
'views/menus.xml',
'views/lims_config_views.xml',
'report/sample_label_report.xml',

View File

@ -11,5 +11,14 @@
<field name="company_id" eval="False"/>
</record>
<!-- Secuencia para muestras de laboratorio -->
<record id="seq_stock_lot_serial" model="ir.sequence">
<field name="name">Secuencia de Muestras de Laboratorio</field>
<field name="code">stock.lot.serial</field>
<field name="prefix">M-%(year)s%(month)s%(day)s-</field>
<field name="padding">6</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -10,8 +10,9 @@
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1985-05-15</field>
<field name="gender">female</field>
<field name="phone">+1-202-555-0174</field>
<field name="phone">+503 7234-5678</field>
<field name="email">ana.torres@example.com</field>
<field name="vat">03245678-9</field>
</record>
<record id="demo_patient_2" model="res.partner">
@ -21,8 +22,9 @@
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1992-11-20</field>
<field name="gender">male</field>
<field name="phone">+1-202-555-0192</field>
<field name="phone">+503 7892-3456</field>
<field name="email">carlos.ruiz@example.com</field>
<field name="vat">04567890-1</field>
</record>
<record id="demo_patient_3" model="res.partner">
@ -32,8 +34,9 @@
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1978-03-10</field>
<field name="gender">female</field>
<field name="phone">+1-202-555-0201</field>
<field name="phone">+503 7345-6789</field>
<field name="email">maria.gonzalez@example.com</field>
<field name="vat">01234567-8</field>
</record>
<!-- Datos de Demostración para Médicos -->
@ -41,7 +44,7 @@
<field name="name">Dr. Luis Herrera</field>
<field name="is_doctor" eval="True"/>
<field name="doctor_license">L-98765</field>
<field name="phone">+1-202-555-0145</field>
<field name="phone">+503 2234-5678</field>
<field name="email">luis.herrera@hospital.com</field>
</record>
@ -49,14 +52,14 @@
<field name="name">Dra. Sofia Vargas</field>
<field name="is_doctor" eval="True"/>
<field name="doctor_license">L-54321</field>
<field name="phone">+1-202-555-0133</field>
<field name="phone">+503 2345-6789</field>
<field name="email">sofia.vargas@clinic.com</field>
</record>
<!-- Datos de Demostración para Tutor y Paciente Menor de Edad -->
<record id="demo_tutor_1" model="res.partner">
<field name="name">Laura Mendoza</field>
<field name="phone">+1-202-555-0188</field>
<field name="phone">+503 7456-7890</field>
<field name="email">laura.mendoza@example.com</field>
</record>
@ -70,5 +73,562 @@
<field name="parent_id" ref="demo_tutor_1"/>
</record>
<!-- Pacientes adicionales - Niños (0-12 años) -->
<record id="demo_patient_4" model="res.partner">
<field name="name">Sofía Jiménez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-S45F04</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=8)).strftime('%Y-%m-%d')"/>
<field name="gender">female</field>
<field name="phone">+503 7567-8901</field>
</record>
<record id="demo_patient_5" model="res.partner">
<field name="name">Diego Morales</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-D78M05</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=3)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7678-9012</field>
</record>
<record id="demo_patient_6" model="res.partner">
<field name="name">Valentina Castro</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-V23F06</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=10)).strftime('%Y-%m-%d')"/>
<field name="gender">female</field>
<field name="phone">+503 7789-0123</field>
</record>
<!-- Adolescentes (13-17 años) -->
<record id="demo_patient_7" model="res.partner">
<field name="name">Santiago Pérez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-S90M07</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=15)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7890-1234</field>
</record>
<record id="demo_patient_8" model="res.partner">
<field name="name">Isabella Rodríguez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-I34F08</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=16)).strftime('%Y-%m-%d')"/>
<field name="gender">female</field>
<field name="phone">+503 7901-2345</field>
</record>
<!-- Adultos jóvenes (18-35 años) - Incluye embarazadas -->
<record id="demo_patient_9" model="res.partner">
<field name="name">Camila Fernández</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-C67F09</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1995-07-22</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7012-3456</field>
<field name="email">camila.fernandez@example.com</field>
<field name="vat">05678901-2</field>
</record>
<record id="demo_patient_10" model="res.partner">
<field name="name">Alejandro Gutiérrez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A12M10</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1990-02-14</field>
<field name="gender">male</field>
<field name="phone">+503 7123-4567</field>
<field name="email">alejandro.gutierrez@example.com</field>
<field name="vat">06789012-3</field>
</record>
<record id="demo_patient_11" model="res.partner">
<field name="name">Lucía Mendoza</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-L89F11</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1992-09-30</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7234-5678</field>
<field name="vat">07890123-4</field>
</record>
<record id="demo_patient_12" model="res.partner">
<field name="name">Miguel Ángel Silva</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-M45M12</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1988-11-05</field>
<field name="gender">male</field>
<field name="phone">+503 7345-6789</field>
<field name="vat">08901234-5</field>
</record>
<record id="demo_patient_13" model="res.partner">
<field name="name">Natalia Vargas</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-N78F13</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1996-04-18</field>
<field name="gender">female</field>
<field name="phone">+503 7456-7890</field>
<field name="vat">09012345-6</field>
</record>
<!-- Adultos (36-55 años) -->
<record id="demo_patient_14" model="res.partner">
<field name="name">Roberto Martínez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-R23M14</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1975-06-12</field>
<field name="gender">male</field>
<field name="phone">+503 7567-8901</field>
<field name="vat">00123456-7</field>
</record>
<record id="demo_patient_15" model="res.partner">
<field name="name">Patricia López</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-P56F15</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1972-12-25</field>
<field name="gender">female</field>
<field name="phone">+503 7678-9012</field>
<field name="vat">01234567-8</field>
</record>
<record id="demo_patient_16" model="res.partner">
<field name="name">Fernando Díaz</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-F90M16</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1980-03-08</field>
<field name="gender">male</field>
<field name="phone">+503 7789-0123</field>
<field name="vat">02345678-9</field>
</record>
<record id="demo_patient_17" model="res.partner">
<field name="name">Andrea Herrera</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A34F17</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1978-08-17</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7890-1234</field>
<field name="vat">03456789-0</field>
</record>
<!-- Adultos mayores (56-75 años) -->
<record id="demo_patient_18" model="res.partner">
<field name="name">José Luis Ramírez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-J67M18</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1965-01-20</field>
<field name="gender">male</field>
<field name="phone">+503 7901-2345</field>
<field name="vat">04567890-1</field>
</record>
<record id="demo_patient_19" model="res.partner">
<field name="name">Carmen Sánchez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-C12F19</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1958-10-15</field>
<field name="gender">female</field>
<field name="phone">+503 7012-3456</field>
<field name="vat">05678901-2</field>
</record>
<record id="demo_patient_20" model="res.partner">
<field name="name">Ricardo Flores</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-R89M20</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1960-05-28</field>
<field name="gender">male</field>
<field name="phone">+503 7123-4567</field>
<field name="vat">06789012-3</field>
</record>
<!-- Ancianos (76+ años) -->
<record id="demo_patient_21" model="res.partner">
<field name="name">Esperanza Romero</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-E45F21</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1945-12-03</field>
<field name="gender">female</field>
<field name="phone">+503 7234-5678</field>
<field name="vat">07890123-4</field>
</record>
<record id="demo_patient_22" model="res.partner">
<field name="name">Francisco Aguilar</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-F78M22</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1943-07-19</field>
<field name="gender">male</field>
<field name="phone">+503 7345-6789</field>
<field name="vat">08901234-5</field>
</record>
<!-- Más pacientes diversos -->
<record id="demo_patient_23" model="res.partner">
<field name="name">Daniela Cortés</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-D23F23</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1998-02-11</field>
<field name="gender">female</field>
<field name="phone">+503 7456-7890</field>
<field name="vat">09012345-6</field>
</record>
<record id="demo_patient_24" model="res.partner">
<field name="name">Gabriel Moreno</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-G56M24</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=6)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7567-8901</field>
</record>
<record id="demo_patient_25" model="res.partner">
<field name="name">Valeria Ruiz</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-V90F25</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1987-09-24</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7678-9012</field>
<field name="vat">00123456-7</field>
</record>
<record id="demo_patient_26" model="res.partner">
<field name="name">Eduardo Navarro</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-E34M26</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1970-11-30</field>
<field name="gender">male</field>
<field name="phone">+503 7789-0123</field>
<field name="vat">01234568-8</field>
</record>
<record id="demo_patient_27" model="res.partner">
<field name="name">Mariana Delgado</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-M67F27</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1999-06-07</field>
<field name="gender">female</field>
<field name="phone">+503 7890-1234</field>
<field name="vat">02345679-9</field>
</record>
<record id="demo_patient_28" model="res.partner">
<field name="name">Andrés Jiménez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A12M28</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1955-08-21</field>
<field name="gender">male</field>
<field name="phone">+503 7901-2345</field>
<field name="vat">03456780-0</field>
</record>
<record id="demo_patient_29" model="res.partner">
<field name="name">Paola Méndez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-P89F29</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1991-03-16</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7012-3456</field>
<field name="vat">04567891-1</field>
</record>
<record id="demo_patient_30" model="res.partner">
<field name="name">Sebastián Vega</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-S45M30</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=14)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7123-4567</field>
</record>
<record id="demo_patient_31" model="res.partner">
<field name="name">Claudia Paredes</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-C78F31</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1982-10-09</field>
<field name="gender">female</field>
<field name="phone">+503 7234-5678</field>
<field name="vat">05678902-2</field>
</record>
<record id="demo_patient_32" model="res.partner">
<field name="name">Raúl Castro</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-R23M32</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1948-04-27</field>
<field name="gender">male</field>
<field name="phone">+503 7345-6789</field>
<field name="vat">06789013-3</field>
</record>
<record id="demo_patient_33" model="res.partner">
<field name="name">Adriana Guerrero</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A56F33</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1994-12-13</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7456-7890</field>
<field name="vat">07890124-4</field>
</record>
<record id="demo_patient_34" model="res.partner">
<field name="name">Javier Molina</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-J90M34</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=9)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7567-8901</field>
</record>
<record id="demo_patient_35" model="res.partner">
<field name="name">Rosa María Ochoa</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-R34F35</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1962-01-05</field>
<field name="gender">female</field>
<field name="phone">+503 7678-9012</field>
<field name="vat">08901235-5</field>
</record>
<record id="demo_patient_36" model="res.partner">
<field name="name">Manuel Reyes</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-M67M36</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1976-07-31</field>
<field name="gender">male</field>
<field name="phone">+503 7789-0123</field>
<field name="vat">09012346-6</field>
</record>
<record id="demo_patient_37" model="res.partner">
<field name="name">Teresa Campos</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-T12F37</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1940-09-18</field>
<field name="gender">female</field>
<field name="phone">+503 7890-1234</field>
<field name="vat">00123457-7</field>
</record>
<record id="demo_patient_38" model="res.partner">
<field name="name">Pablo Espinoza</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-P89M38</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1989-05-03</field>
<field name="gender">male</field>
<field name="phone">+503 7901-2345</field>
<field name="vat">01234569-8</field>
</record>
<record id="demo_patient_39" model="res.partner">
<field name="name">Mónica Villanueva</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-M45F39</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1985-11-26</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7012-3456</field>
<field name="vat">02345670-9</field>
</record>
<record id="demo_patient_40" model="res.partner">
<field name="name">Diego Alejandro Luna</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-D78M40</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=2)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7123-4567</field>
</record>
<record id="demo_patient_41" model="res.partner">
<field name="name">Beatriz Salazar</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-B23F41</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1968-02-14</field>
<field name="gender">female</field>
<field name="phone">+503 7234-5678</field>
<field name="vat">03456781-0</field>
</record>
<record id="demo_patient_42" model="res.partner">
<field name="name">Héctor Valdés</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-H56M42</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1973-06-29</field>
<field name="gender">male</field>
<field name="phone">+503 7345-6789</field>
<field name="vat">04567892-1</field>
</record>
<record id="demo_patient_43" model="res.partner">
<field name="name">Silvia Peña</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-S90F43</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1997-08-11</field>
<field name="gender">female</field>
<field name="phone">+503 7456-7890</field>
<field name="vat">05678903-2</field>
</record>
<record id="demo_patient_44" model="res.partner">
<field name="name">Arturo Domínguez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A34M44</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1951-12-07</field>
<field name="gender">male</field>
<field name="phone">+503 7567-8901</field>
<field name="vat">06789014-3</field>
</record>
<record id="demo_patient_45" model="res.partner">
<field name="name">Gloria Ríos</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-G67F45</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1983-04-22</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7678-9012</field>
<field name="vat">07890125-4</field>
</record>
<record id="demo_patient_46" model="res.partner">
<field name="name">Emilio Núñez</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-E12M46</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=11)).strftime('%Y-%m-%d')"/>
<field name="gender">male</field>
<field name="phone">+503 7789-0123</field>
</record>
<record id="demo_patient_47" model="res.partner">
<field name="name">Laura Patricia Ibarra</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-L89F47</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1979-10-16</field>
<field name="gender">female</field>
<field name="phone">+503 7890-1234</field>
<field name="vat">08901236-5</field>
</record>
<record id="demo_patient_48" model="res.partner">
<field name="name">Óscar Medina</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-O45M48</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1966-03-25</field>
<field name="gender">male</field>
<field name="phone">+503 7901-2345</field>
<field name="vat">09012347-6</field>
</record>
<record id="demo_patient_49" model="res.partner">
<field name="name">Verónica Soto</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-V78F49</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1993-07-08</field>
<field name="gender">female</field>
<field name="is_pregnant" eval="True"/>
<field name="phone">+503 7012-3456</field>
<field name="vat">00123458-7</field>
</record>
<record id="demo_patient_50" model="res.partner">
<field name="name">Rubén Contreras</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-R23M50</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1937-11-14</field>
<field name="gender">male</field>
<field name="phone">+503 7123-4567</field>
<field name="vat">01234560-8</field>
</record>
<record id="demo_patient_51" model="res.partner">
<field name="name">Alejandra Fuentes</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-A56F51</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=7)).strftime('%Y-%m-%d')"/>
<field name="gender">female</field>
<field name="phone">+503 7234-5678</field>
</record>
<record id="demo_patient_52" model="res.partner">
<field name="name">Nicolás Ramos</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-N90M52</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">1986-01-19</field>
<field name="gender">male</field>
<field name="phone">+503 7345-6789</field>
<field name="vat">02345671-9</field>
</record>
<record id="demo_patient_53" model="res.partner">
<field name="name">Fernanda Acosta</field>
<field name="is_patient" eval="True"/>
<field name="patient_identifier">P-F34F53</field>
<field name="origin">Carga Inicial</field>
<field name="birthdate_date">2000-05-12</field>
<field name="gender">female</field>
<field name="phone">+503 7456-7890</field>
<field name="vat">03456782-0</field>
</record>
</data>
</odoo>

View File

@ -258,6 +258,10 @@ class LimsResult(models.Model):
@api.constrains('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
def _check_value_type(self):
"""Asegura que el valor ingresado corresponda al tipo de parámetro."""
# Skip validation if we're in initialization context
if self.env.context.get('skip_value_validation'):
return
for record in self:
if not record.parameter_id:
continue
@ -301,8 +305,8 @@ class LimsResult(models.Model):
_('Para parámetros Sí/No solo se debe marcar el checkbox.')
)
# Solo requerir valor si la prueba no está en borrador
if not has_value and record.parameter_id and record.test_id.state != 'draft':
# Solo requerir valor si la prueba existe y no está en borrador
if not has_value and record.parameter_id and record.test_id and record.test_id.state != 'draft':
raise ValidationError(
_('Debe ingresar un valor para el resultado del parámetro %s.') % record.parameter_name
)
@ -365,6 +369,101 @@ class LimsResult(models.Model):
if len(matches) == 1:
self.value_selection = matches[0]
@api.onchange('value_numeric', 'is_critical')
def _onchange_critical_value(self):
"""Autocompleta las notas cuando el valor es crítico."""
if self.is_critical and self.parameter_value_type == 'numeric' and self.value_numeric:
# Diccionario de notas médicas para parámetros críticos
CRITICAL_NOTES = {
'glucosa': {
'high': 'Valor elevado de glucosa. Posible prediabetes o diabetes. Se recomienda repetir la prueba en ayunas y consultar con endocrinología.',
'low': 'Hipoglucemia detectada. Riesgo de síntomas neuroglucogénicos. Evaluar causas: medicamentos, insuficiencia hepática o endocrinopatías.'
},
'hemoglobina': {
'high': 'Policitemia. Evaluar posibles causas: deshidratación, tabaquismo, cardiopatía o policitemia vera.',
'low': 'Anemia severa. Investigar origen: deficiencia de hierro, pérdida sanguínea, hemólisis o enfermedad crónica.'
},
'hematocrito': {
'high': 'Hemoconcentración. Correlacionar con hemoglobina. Descartar deshidratación o policitemia.',
'low': 'Valor compatible con anemia. Evaluar junto con hemoglobina e índices eritrocitarios.'
},
'leucocitos': {
'high': 'Leucocitosis marcada. Descartar proceso infeccioso, inflamatorio o hematológico.',
'low': 'Leucopenia severa. Riesgo de infecciones. Evaluar causas: viral, medicamentosa o hematológica.'
},
'plaquetas': {
'high': 'Trombocitosis. Riesgo trombótico. Descartar causa primaria vs reactiva.',
'low': 'Trombocitopenia severa. Riesgo de sangrado. Evaluar PTI, hiperesplenismo o supresión medular.'
},
'neutrofilos': {
'high': 'Neutrofilia. Sugiere infección bacteriana o proceso inflamatorio agudo.',
'low': 'Neutropenia. Alto riesgo de infección bacteriana. Evaluar urgentemente.'
},
'linfocitos': {
'high': 'Linfocitosis. Considerar infección viral o proceso linfoproliferativo.',
'low': 'Linfopenia. Evaluar inmunodeficiencia o efecto de corticoides.'
},
'colesterol total': {
'high': 'Hipercolesterolemia. Riesgo cardiovascular elevado. Iniciar medidas dietéticas y evaluar tratamiento con estatinas.',
'low': 'Hipocolesterolemia. Evaluar malnutrición, hipertiroidismo o enfermedad hepática.'
},
'trigliceridos': {
'high': 'Hipertrigliceridemia severa. Riesgo de pancreatitis aguda. Considerar tratamiento farmacológico urgente.',
'low': 'Valor bajo, generalmente sin significado patológico.'
},
'hdl': {
'high': 'HDL elevado, factor protector cardiovascular.',
'low': 'HDL bajo. Factor de riesgo cardiovascular. Recomendar ejercicio y cambios en estilo de vida.'
},
'ldl': {
'high': 'LDL elevado. Alto riesgo aterogénico. Evaluar inicio de estatinas según riesgo global.',
'low': 'LDL bajo, generalmente favorable.'
},
'glucosa en sangre': {
'high': 'Hiperglucemia. Si en ayunas >126 mg/dL sugiere diabetes. Confirmar con segunda muestra.',
'low': 'Hipoglucemia. Evaluar síntomas y causas. Riesgo neurológico si <50 mg/dL.'
}
}
# Solo autocompletar si no hay notas previas o están vacías
if not self.notes or self.notes.strip() == '':
note = self._get_critical_note(CRITICAL_NOTES)
if note:
self.notes = note
def _get_critical_note(self, critical_notes_dict):
"""Obtiene la nota apropiada para un resultado crítico."""
if not self.parameter_id or not self.parameter_name:
return False
param_lower = self.parameter_name.lower()
# Buscar el parámetro en el diccionario
for key in critical_notes_dict:
if key in param_lower:
# Obtener rangos del rango aplicable si existe
normal_min = normal_max = None
if self.applicable_range_id:
normal_min = self.applicable_range_id.normal_min
normal_max = self.applicable_range_id.normal_max
if normal_max and self.value_numeric > normal_max:
return critical_notes_dict[key].get('high', f'Valor crítico alto para {self.parameter_name}. Requiere evaluación médica inmediata.')
elif normal_min and self.value_numeric < normal_min:
return critical_notes_dict[key].get('low', f'Valor crítico bajo para {self.parameter_name}. Requiere evaluación médica inmediata.')
# Nota genérica si no se encuentra el parámetro
if self.applicable_range_id:
normal_min = self.applicable_range_id.normal_min
normal_max = self.applicable_range_id.normal_max
if normal_max and self.value_numeric > normal_max:
return f'Valor significativamente elevado. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
elif normal_min and self.value_numeric < normal_min:
return f'Valor significativamente bajo. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
return 'Valor fuera de rango normal. Requiere interpretación clínica.'
def _validate_and_autocomplete_selection(self, value):
"""Valida y autocompleta el valor de selección.

View File

@ -116,6 +116,21 @@ class LimsTest(models.Model):
default=lambda self: self.env.company
)
# Campos para dashboards demográficos
patient_gender = fields.Selection(
related='patient_id.gender',
string='Género del Paciente',
store=True,
readonly=True
)
patient_age_range = fields.Selection(
related='patient_id.age_range',
string='Rango de Edad',
store=True,
readonly=True
)
@api.depends('company_id')
def _compute_require_validation(self):
"""Calcula si la prueba requiere validación basado en configuración."""
@ -154,17 +169,6 @@ class LimsTest(models.Model):
}
}
@api.model_create_multi
def create(self, vals_list):
"""Genera código único al crear."""
for vals in vals_list:
if vals.get('name', 'Nuevo') == 'Nuevo':
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
tests = super().create(vals_list)
# Generar resultados automáticamente
tests._generate_test_results()
return tests
def _generate_test_results(self):
"""Genera automáticamente las líneas de resultado basadas en los parámetros configurados del análisis."""
@ -490,13 +494,20 @@ class LimsTest(models.Model):
@api.model
def create(self, vals):
"""Override create para validaciones adicionales"""
"""Override create para validaciones adicionales y generación de secuencia"""
# Generar código único si no se proporciona
if vals.get('name', 'Nuevo') == 'Nuevo':
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
# Si se está creando con un estado diferente a draft, verificar permisos
if vals.get('state') and vals['state'] != 'draft':
if not self.env.user.has_group('lims_management.group_lims_admin'):
raise UserError(_('Solo administradores pueden crear pruebas en estado diferente a borrador'))
return super().create(vals)
test = super().create(vals)
# Generar resultados automáticamente
test._generate_test_results()
return test
def write(self, vals):
"""Override write para auditoría adicional"""

View File

@ -29,6 +29,17 @@ class ResPartner(models.Model):
help="Edad calculada en años basada en la fecha de nacimiento"
)
age_range = fields.Selection([
('0-10', '0-10 años'),
('11-20', '11-20 años'),
('21-30', '21-30 años'),
('31-40', '31-40 años'),
('41-50', '41-50 años'),
('51-60', '51-60 años'),
('61-70', '61-70 años'),
('71+', 'Más de 70 años')
], string="Rango de Edad", compute='_compute_age_range', store=True)
is_pregnant = fields.Boolean(
string="Embarazada",
help="Marcar si la paciente está embarazada (solo aplica para género femenino)"
@ -54,6 +65,34 @@ class ResPartner(models.Model):
else:
partner.age = 0
@api.depends('birthdate_date')
def _compute_age_range(self):
"""Calcula el rango de edad basado en la edad"""
for partner in self:
if partner.birthdate_date:
today = date.today()
delta = relativedelta(today, partner.birthdate_date)
age = delta.years
if age <= 10:
partner.age_range = '0-10'
elif age <= 20:
partner.age_range = '11-20'
elif age <= 30:
partner.age_range = '21-30'
elif age <= 40:
partner.age_range = '31-40'
elif age <= 50:
partner.age_range = '41-50'
elif age <= 60:
partner.age_range = '51-60'
elif age <= 70:
partner.age_range = '61-70'
else:
partner.age_range = '71+'
else:
partner.age_range = False
@api.constrains('is_pregnant', 'gender')
def _check_pregnant_gender(self):
"""Valida que solo pacientes de género femenino puedan estar embarazadas"""

View File

@ -0,0 +1,338 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================
DASHBOARD 1: Estado de Órdenes de Laboratorio
================================================================ -->
<!-- Vista Graph para Estado de Órdenes -->
<record id="view_lab_order_dashboard_graph" model="ir.ui.view">
<field name="name">sale.order.lab.dashboard.graph</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<graph string="Estado de &#211;rdenes" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Estado de Órdenes -->
<record id="view_lab_order_dashboard_pivot" model="ir.ui.view">
<field name="name">sale.order.lab.dashboard.pivot</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis de &#211;rdenes">
<field name="date_order" interval="month" type="col"/>
<field name="state" type="row"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Estado de Órdenes -->
<record id="action_lab_order_dashboard" model="ir.actions.act_window">
<field name="name">Estado de &#211;rdenes</field>
<field name="res_model">sale.order</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('is_lab_request', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_lab_order_dashboard_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_lab_order_dashboard_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay &#243;rdenes de laboratorio registradas
</p>
<p>
Este dashboard muestra el estado actual de todas las &#243;rdenes de laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 2: Productividad de Técnicos
================================================================ -->
<!-- Vista Graph para Productividad de Técnicos -->
<record id="view_test_technician_productivity_graph" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Productividad de T&#233;cnicos" type="bar">
<field name="technician_id"/>
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Productividad de Técnicos -->
<record id="view_test_technician_productivity_pivot" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por T&#233;cnico">
<field name="technician_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Productividad de Técnicos -->
<record id="action_technician_productivity_dashboard" model="ir.actions.act_window">
<field name="name">Productividad de T&#233;cnicos</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="context">{'search_default_group_by_technician': 1, 'search_default_this_month': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_technician_productivity_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_technician_productivity_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay pruebas registradas
</p>
<p>
Este dashboard muestra la productividad de cada t&#233;cnico del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 3: Estado de Muestras
================================================================ -->
<!-- Vista Graph para Estado de Muestras -->
<record id="view_sample_status_graph" model="ir.ui.view">
<field name="name">stock.lot.sample.status.graph</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<graph string="Estado de Muestras" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Muestras por Tipo -->
<record id="view_sample_type_pivot" model="ir.ui.view">
<field name="name">stock.lot.sample.type.pivot</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<pivot string="Muestras por Tipo">
<field name="sample_type_product_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Muestras -->
<record id="action_sample_dashboard" model="ir.actions.act_window">
<field name="name">Dashboard de Muestras</field>
<field name="res_model">stock.lot</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('is_lab_sample', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_sample_status_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_sample_type_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay muestras registradas
</p>
<p>
Este dashboard muestra el estado de todas las muestras del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 4: Parámetros Fuera de Rango
================================================================ -->
<!-- Vista Graph para Parámetros Fuera de Rango -->
<record id="view_result_out_of_range_graph" model="ir.ui.view">
<field name="name">lims.result.out.of.range.graph</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<graph string="Par&#225;metros Fuera de Rango" type="bar">
<field name="parameter_id"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Resultados Críticos -->
<record id="view_result_critical_pivot" model="ir.ui.view">
<field name="name">lims.result.critical.pivot</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<pivot string="Resultados Cr&#237;ticos">
<field name="parameter_id" type="row"/>
<field name="is_critical" type="col"/>
<field name="is_out_of_range" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Parámetros Fuera de Rango -->
<record id="action_out_of_range_dashboard" model="ir.actions.act_window">
<field name="name">Par&#225;metros Fuera de Rango</field>
<field name="res_model">lims.result</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('test_id.state', '=', 'validated')]</field>
<field name="context">{'search_default_out_of_range': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_result_out_of_range_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_result_critical_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay resultados fuera de rango
</p>
<p>
Este dashboard muestra los par&#225;metros que est&#225;n fuera de los rangos normales.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 5: Análisis Más Solicitados
================================================================ -->
<!-- Vista Graph para Top Análisis -->
<record id="view_top_analysis_graph" model="ir.ui.view">
<field name="name">sale.order.line.top.analysis.graph</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<graph string="An&#225;lisis M&#225;s Solicitados" type="bar">
<field name="product_id"/>
<field name="product_uom_qty" type="measure"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Análisis por Período -->
<record id="view_analysis_period_pivot" model="ir.ui.view">
<field name="name">sale.order.line.analysis.period.pivot</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por Per&#237;odo">
<field name="create_date" interval="month" type="col"/>
<field name="product_id" type="row"/>
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Análisis Más Solicitados -->
<record id="action_top_analysis_dashboard" model="ir.actions.act_window">
<field name="name">An&#225;lisis M&#225;s Solicitados</field>
<field name="res_model">sale.order.line</field>
<field name="view_mode">graph,pivot,list</field>
<field name="domain">[('order_id.is_lab_request', '=', True), ('product_id.is_analysis', '=', True)]</field>
<field name="context">{'search_default_group_by_product': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_top_analysis_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_analysis_period_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay an&#225;lisis registrados
</p>
<p>
Este dashboard muestra los an&#225;lisis m&#225;s solicitados en el laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 6: Distribución de Tests por Demografía
================================================================ -->
<!-- Vista Graph para Distribución por Sexo -->
<record id="view_test_gender_distribution_graph" model="ir.ui.view">
<field name="name">lims.test.gender.distribution.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Distribuci&#243;n por G&#233;nero" type="pie">
<field name="patient_gender"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Tests por Edad y Sexo -->
<record id="view_test_demographics_pivot" model="ir.ui.view">
<field name="name">lims.test.demographics.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="Tests por Demograf&#237;a">
<field name="patient_age_range" type="row"/>
<field name="patient_gender" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Distribución Demográfica -->
<record id="action_test_demographics_dashboard" model="ir.actions.act_window">
<field name="name">Distribuci&#243;n Demogr&#225;fica de Tests</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,list</field>
<field name="domain">[('state', '=', 'validated')]</field>
<field name="context">{'search_default_this_year': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_gender_distribution_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_demographics_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay tests validados
</p>
<p>
Este dashboard muestra la distribuci&#243;n de tests por caracter&#237;sticas demogr&#225;ficas de los pacientes.
</p>
</field>
</record>
<!-- ================================================================
FILTROS DE BÚSQUEDA PARA DASHBOARDS
================================================================ -->
<!-- Filtros para Tests -->
<record id="view_lims_test_dashboard_search" model="ir.ui.view">
<field name="name">lims.test.dashboard.search</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Estado -->
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
<filter string="Validados" name="validated" domain="[('state', '=', 'validated')]"/>
<!-- Filtros de Tiempo -->
<filter string="Hoy" name="today" domain="[('create_date', '&gt;=', context_today())]"/>
<filter string="Esta Semana" name="this_week" domain="[('create_date', '&gt;=', (context_today() + relativedelta(days=-7)).strftime('%Y-%m-%d'))]"/>
<filter string="Este Mes" name="this_month" domain="[('create_date', '&gt;=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
<filter string="Este A&#241;o" name="this_year" domain="[('create_date', '&gt;=', (context_today() + relativedelta(month=1, day=1)).strftime('%Y-%m-%d'))]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="T&#233;cnico" name="group_by_technician" context="{'group_by': 'technician_id'}"/>
<filter string="Estado" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Paciente" name="group_by_patient" context="{'group_by': 'patient_id'}"/>
<filter string="An&#225;lisis" name="group_by_product" context="{'group_by': 'product_id'}"/>
<filter string="Fecha" name="group_by_date" context="{'group_by': 'create_date:month'}"/>
</group>
</search>
</field>
</record>
<!-- Filtros para Resultados -->
<record id="view_lims_result_dashboard_search" model="ir.ui.view">
<field name="name">lims.result.dashboard.search</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Rango -->
<filter string="Fuera de Rango" name="out_of_range" domain="[('is_out_of_range', '=', True)]"/>
<filter string="Cr&#237;ticos" name="critical" domain="[('is_critical', '=', True)]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="Par&#225;metro" name="group_by_parameter" context="{'group_by': 'parameter_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View File

@ -155,6 +155,51 @@
action="action_lims_result"
sequence="30"/>
<!-- Submenú de Dashboards -->
<menuitem
id="menu_lims_dashboards"
name="Dashboards"
parent="lims_menu_root"
sequence="85"
groups="lims_management.group_lims_admin"/>
<!-- Dashboards individuales -->
<menuitem id="menu_lab_order_dashboard"
name="Estado de &#211;rdenes"
parent="menu_lims_dashboards"
action="action_lab_order_dashboard"
sequence="10"/>
<menuitem id="menu_technician_productivity_dashboard"
name="Productividad de T&#233;cnicos"
parent="menu_lims_dashboards"
action="action_technician_productivity_dashboard"
sequence="20"/>
<menuitem id="menu_sample_dashboard"
name="Dashboard de Muestras"
parent="menu_lims_dashboards"
action="action_sample_dashboard"
sequence="30"/>
<menuitem id="menu_out_of_range_dashboard"
name="Par&#225;metros Fuera de Rango"
parent="menu_lims_dashboards"
action="action_out_of_range_dashboard"
sequence="40"/>
<menuitem id="menu_top_analysis_dashboard"
name="An&#225;lisis M&#225;s Solicitados"
parent="menu_lims_dashboards"
action="action_top_analysis_dashboard"
sequence="50"/>
<menuitem id="menu_test_demographics_dashboard"
name="Distribuci&#243;n Demogr&#225;fica"
parent="menu_lims_dashboards"
action="action_test_demographics_dashboard"
sequence="60"/>
<!-- Submenú de Reportes -->
<menuitem
id="lims_menu_reports"

View File

@ -1,32 +0,0 @@
## Descripción
Implementación del sistema de etiquetas con código de barras para las muestras de laboratorio.
## Cambios realizados
### Funcionalidad principal
- Creado reporte QWeb para imprimir etiquetas de muestras (100x50mm)
- Implementado botón 'Imprimir Etiquetas' en órdenes de laboratorio
- Las etiquetas incluyen:
- Información del paciente
- Código de muestra y orden
- Tipo de contenedor
- Fecha de recolección
- Código de barras Code128
- Lista de análisis a realizar
### Correcciones técnicas
- **Código de barras**: Corregido problema de visualización usando widget nativo de Odoo 18
- **Caracteres especiales**: Solucionado problema de codificación UTF-8 con referencias numéricas
- **Layout**: Ajustado diseño para mostrar múltiples etiquetas por página sin solapamiento
- **Espaciado**: Optimizado el tamaño y posición del código de barras
## Testing
- Probado con órdenes que tienen múltiples muestras
- Verificado que los códigos de barras se generen y visualicen correctamente
- Confirmado que los caracteres en español (tildes, ñ) se muestren bien
- Validado que no hay solapamiento entre etiquetas
## Capturas
- Los códigos de barras ahora se visualizan correctamente
- Las etiquetas respetan el formato 100x50mm
- Múltiples etiquetas por página sin problemas de diseño

View File

@ -1,74 +0,0 @@
## Descripción
Implementación de la funcionalidad para cancelar automáticamente muestras y pruebas cuando se cancela una orden de laboratorio, evitando que queden elementos "huérfanos" en el sistema.
## 🎯 Objetivo
Resolver el issue #54: Las muestras y pruebas asociadas a una orden de laboratorio deben cancelarse automáticamente cuando se cancela la orden.
## 🔧 Cambios implementados
### 1. Modelo `stock.lot` (Muestras)
- Agregado nuevo estado `'cancelled'` a la selección de estados
- Implementado método `action_cancel()` para cambiar el estado a cancelado
### 2. Modelo `sale.order` (Órdenes)
- Override del método `action_cancel()` que:
- Llama primero al método padre para mantener el comportamiento estándar
- Si es una orden de laboratorio (`is_lab_request = True`):
- Cancela muestras en estados: `pending_collection`, `collected`, `received`, `in_process`
- Cancela pruebas asociadas que no estén en estado `validated` o `cancelled`
- Registra mensajes en el chatter de cada elemento cancelado
- Muestra un resumen en la orden con la cantidad de elementos cancelados
### 3. Tests unitarios
- Creado `test_order_cancel_cascade.py` con 6 tests que verifican:
- ✅ Cancelación correcta de muestras
- ✅ Cancelación correcta de pruebas
- ✅ No cancelación de muestras en estados finales (analyzed, stored, disposed)
- ✅ No cancelación de pruebas validadas
- ✅ Generación de mensajes en chatter
- ✅ Órdenes normales (no laboratorio) no afectadas
## 🧪 Pruebas realizadas
### Test manual exitoso:
```
📦 Muestras generadas:
- 0000012: Contenedor de Heces (Estado: pending_collection)
🔬 Pruebas generadas:
- LAB-2025-00014: Coprocultivo (Estado: draft)
❌ Cancelando la orden de laboratorio...
📦 Estado final de las muestras:
- 0000012: cancelled ✓
🔬 Estado final de las pruebas:
- LAB-2025-00014: cancelled ✓
✅ Mensajes de cancelación registrados en todos los elementos
```
## 📋 Checklist
- [x] Código implementado y probado
- [x] Tests unitarios creados
- [x] Pruebas manuales exitosas
- [x] Mensajes en chatter funcionando
- [x] Sin errores o warnings
- [x] Documentación en código
## 🔍 Cómo probar
1. Crear una orden de laboratorio con análisis
2. Confirmar la orden (se generan muestras y pruebas)
3. Opcionalmente iniciar proceso en alguna prueba
4. Cancelar la orden
5. Verificar que:
- Las muestras cambiaron a estado "Cancelada"
- Las pruebas cambiaron a estado "Cancelado"
- Hay mensajes en el chatter explicando la cancelación
Resuelve #54

View File

@ -1,48 +0,0 @@
## Implementación del flujo de validación y seguridad
### Cambios realizados
#### 1. Ajuste de permisos base (ir.model.access.csv)
- Recepcionista: Solo lectura en lims.test y lims.result
- Técnico: Lectura/escritura pero sin crear/eliminar
- Administrador: Permisos completos
#### 2. Reglas de registro implementadas (lims_security.xml)
- Recepcionistas no pueden editar pruebas
- Técnicos solo pueden editar pruebas no validadas
- Administradores tienen acceso completo
#### 3. Validación de permisos en transiciones (lims_test.py)
- `action_start_process()`: Solo técnicos y administradores
- `action_enter_results()`: Solo técnicos y administradores
- `action_validate()`: Solo administradores
- `action_cancel()`: Técnicos (excepto validadas) y administradores
- `action_draft()`: Solo administradores
#### 4. Trazabilidad mejorada
- stock.lot ahora hereda de mail.thread
- Todos los cambios de estado se registran en el chatter
- Mensajes más descriptivos con contexto
#### 5. Validaciones adicionales
- Control de transiciones de estado válidas
- Verificación del estado de la muestra
- Validación de resultados críticos fuera de rango
- No se puede crear pruebas en estado != draft sin ser admin
#### 6. Vistas actualizadas
- Botones visibles solo para roles apropiados
- Campos de resultados editables solo por técnicos/admin
#### 7. Usuarios demo para pruebas
- Usuario: `recepcionista` / Contraseña: `demo`
- Usuario: `tecnico` / Contraseña: `demo`
- Usuario: `administrador` / Contraseña: `demo`
### Pruebas realizadas
- Verificación de permisos por rol
- Validación de transiciones de estado
- Trazabilidad en chatter
- Restricciones visuales en formularios
Closes #9

View File

@ -63,28 +63,21 @@ try:
env.cr.rollback()
continue
# Verificación final con consulta directa a la BD
# Verificación final usando el ORM
print("\n" + "="*60)
print("VERIFICACIÓN FINAL (consulta directa):")
print("VERIFICACIÓN FINAL:")
print("="*60)
env.cr.execute("""
SELECT id, name,
CASE WHEN logo IS NOT NULL THEN 'SI' ELSE 'NO' END as tiene_logo,
CASE WHEN logo IS NOT NULL THEN length(logo) ELSE 0 END as logo_size
FROM res_company
ORDER BY id
""")
for row in env.cr.fetchall():
print(f"\nEmpresa ID {row[0]}:")
print(f" - Nombre: {row[1]}")
print(f" - Logo presente: {row[2]}")
if row[2] == 'SI':
print(f" - Tamaño del logo (base64): {row[3]:,} caracteres")
# Verificar a través del ORM que es más seguro
for company in env['res.company'].search([], order='id'):
print(f"\nEmpresa ID {company.id}:")
print(f" - Nombre: {company.name}")
print(f" - Logo presente: {'SI' if company.logo else 'NO'}")
if company.logo:
print(f" - Tamaño del logo (base64): {len(company.logo):,} caracteres")
# Forzar actualización de caché
env['res.company'].invalidate_cache()
env['res.company']._invalidate_cache()
print("\nLogo de la empresa actualizado exitosamente.")
print("NOTA: Si el logo no aparece en la interfaz, puede ser necesario:")

View File

@ -216,12 +216,13 @@ def create_demo_lab_data(cr):
# Procesar cada muestra
for sample in order.generated_sample_ids:
# Marcar como recolectada
if sample.sample_state == 'pending_collection':
if sample.state == 'pending_collection':
sample.action_collect()
print(f" ✓ Muestra {sample.name} recolectada")
# Procesar pruebas de esta muestra
for test in sample.test_ids:
tests = env['lims.test'].search([('sample_id', '=', sample.id)])
for test in tests:
print(f" - Procesando prueba: {test.product_id.name}")
# Iniciar proceso si está en borrador
@ -241,7 +242,7 @@ def create_demo_lab_data(cr):
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])
should_validate = (idx < 2) or (idx == 2 and test == tests[0])
if should_validate and test.state == 'result_entered':
test.action_validate()

View File

@ -1,5 +1,242 @@
import odoo
import json
import random
from datetime import datetime
# Diccionario de notas médicas para parámetros críticos
CRITICAL_NOTES = {
'glucosa': {
'high': 'Valor elevado de glucosa. Posible prediabetes o diabetes. Se recomienda repetir la prueba en ayunas y consultar con endocrinología.',
'low': 'Hipoglucemia detectada. Riesgo de síntomas neuroglucogénicos. Evaluar causas: medicamentos, insuficiencia hepática o endocrinopatías.'
},
'hemoglobina': {
'high': 'Policitemia. Evaluar posibles causas: deshidratación, tabaquismo, cardiopatía o policitemia vera.',
'low': 'Anemia severa. Investigar origen: deficiencia de hierro, pérdida sanguínea, hemólisis o enfermedad crónica.'
},
'hematocrito': {
'high': 'Hemoconcentración. Correlacionar con hemoglobina. Descartar deshidratación o policitemia.',
'low': 'Valor compatible con anemia. Evaluar junto con hemoglobina e índices eritrocitarios.'
},
'leucocitos': {
'high': 'Leucocitosis marcada. Descartar proceso infeccioso, inflamatorio o hematológico.',
'low': 'Leucopenia severa. Riesgo de infecciones. Evaluar causas: viral, medicamentosa o hematológica.'
},
'plaquetas': {
'high': 'Trombocitosis. Riesgo trombótico. Descartar causa primaria vs reactiva.',
'low': 'Trombocitopenia severa. Riesgo de sangrado. Evaluar PTI, hiperesplenismo o supresión medular.'
},
'neutrofilos': {
'high': 'Neutrofilia. Sugiere infección bacteriana o proceso inflamatorio agudo.',
'low': 'Neutropenia. Alto riesgo de infección bacteriana. Evaluar urgentemente.'
},
'linfocitos': {
'high': 'Linfocitosis. Considerar infección viral o proceso linfoproliferativo.',
'low': 'Linfopenia. Evaluar inmunodeficiencia o efecto de corticoides.'
},
'colesterol total': {
'high': 'Hipercolesterolemia. Riesgo cardiovascular elevado. Iniciar medidas dietéticas y evaluar tratamiento con estatinas.',
'low': 'Hipocolesterolemia. Evaluar malnutrición, hipertiroidismo o enfermedad hepática.'
},
'trigliceridos': {
'high': 'Hipertrigliceridemia severa. Riesgo de pancreatitis aguda. Considerar tratamiento farmacológico urgente.',
'low': 'Valor bajo, generalmente sin significado patológico.'
},
'hdl': {
'high': 'HDL elevado, factor protector cardiovascular.',
'low': 'HDL bajo. Factor de riesgo cardiovascular. Recomendar ejercicio y cambios en estilo de vida.'
},
'ldl': {
'high': 'LDL elevado. Alto riesgo aterogénico. Evaluar inicio de estatinas según riesgo global.',
'low': 'LDL bajo, generalmente favorable.'
},
'glucosa en sangre': {
'high': 'Hiperglucemia. Si en ayunas >126 mg/dL sugiere diabetes. Confirmar con segunda muestra.',
'low': 'Hipoglucemia. Evaluar síntomas y causas. Riesgo neurológico si <50 mg/dL.'
}
}
def get_critical_note(param_name, value, normal_min=None, normal_max=None):
"""Obtiene la nota apropiada para un resultado crítico"""
param_lower = param_name.lower()
# Buscar el parámetro en el diccionario
for key in CRITICAL_NOTES:
if key in param_lower:
if normal_max and value > normal_max:
return CRITICAL_NOTES[key].get('high', f'Valor crítico alto para {param_name}. Requiere evaluación médica inmediata.')
elif normal_min and value < normal_min:
return CRITICAL_NOTES[key].get('low', f'Valor crítico bajo para {param_name}. Requiere evaluación médica inmediata.')
# Nota genérica si no se encuentra el parámetro
if normal_max and value > normal_max:
return f'Valor significativamente elevado. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
elif normal_min and value < normal_min:
return f'Valor significativamente bajo. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
return 'Valor fuera de rango normal. Requiere interpretación clínica.'
def process_order_tests(env, order):
"""Process all tests for a given order: regenerate results, fill values, and validate"""
print(f"\nProcessing tests for order {order.name}...")
# First, update sample states to allow processing
samples = order.generated_sample_ids.sudo()
for sample in samples:
if sample.state == 'pending_collection':
sample.action_collect()
print(f" - Sample {sample.name} collected")
if sample.state == 'collected':
sample.action_receive()
print(f" - Sample {sample.name} received")
if sample.state == 'received':
sample.action_start_analysis()
print(f" - Sample {sample.name} analysis started")
# Find all tests associated with this order
tests = env['lims.test'].search([('sale_order_id', '=', order.id)])
print(f"Found {len(tests)} tests for order {order.name}")
# Ensure we have the right permissions by using sudo()
tests = tests.sudo()
for test in tests:
try:
print(f"\nProcessing test {test.name} - {test.product_id.name}")
# First, mark the test as in_process if it's in draft state
if test.state == 'draft':
test.write({'state': 'in_process'})
# Manually create results if they don't exist
if not test.result_ids:
# Get analysis parameters from product template
product_tmpl = test.product_id.product_tmpl_id
for param_link in product_tmpl.parameter_ids:
param = param_link.parameter_id
# Prepare result data with values
result_data = {
'test_id': test.id,
'parameter_id': param.id,
}
# Set value based on parameter type
try:
if param.value_type == 'numeric':
# Generar valor que a veces esté fuera de rango
if random.random() < 0.3: # 30% de valores críticos
# Obtener rangos normales del parámetro
normal_min = param_link.normal_min if hasattr(param_link, 'normal_min') and param_link.normal_min else 10
normal_max = param_link.normal_max if hasattr(param_link, 'normal_max') and param_link.normal_max else 100
# Decidir si será alto o bajo
if random.random() < 0.5:
# Valor alto
value = round(random.uniform(normal_max * 1.2, normal_max * 1.5), 2)
else:
# Valor bajo
value = round(random.uniform(normal_min * 0.5, normal_min * 0.8), 2)
result_data['value_numeric'] = value
else:
result_data['value_numeric'] = 50.0
elif param.value_type == 'text':
# Handle different text parameters appropriately
param_lower = param.name.lower()
if 'cultivo' in param_lower:
result_data['value_text'] = "No se observa crecimiento bacteriano"
elif 'observacion' in param_lower:
result_data['value_text'] = "Sin observaciones particulares"
elif 'color' in param_lower:
result_data['value_text'] = "Amarillo claro"
elif 'aspecto' in param_lower:
result_data['value_text'] = "Transparente"
elif 'olor' in param_lower:
result_data['value_text'] = "Sui generis"
else:
result_data['value_text'] = "Normal"
elif param.value_type == 'boolean':
result_data['value_boolean'] = False
elif param.value_type == 'selection':
if param.selection_values:
options = param.selection_values.split(',')
# For pregnancy tests, randomly assign positive/negative
if 'beta-hcg' in param.name.lower() or 'embarazo' in param.name.lower():
# 30% chance of positive for pregnant patients, 5% for others
is_positive = random.random() < (0.3 if hasattr(test, 'patient_id') and test.patient_id.is_pregnant else 0.05)
result_data['value_selection'] = 'Positivo' if is_positive else 'Negativo'
else:
result_data['value_selection'] = options[0].strip()
else:
# No selection values defined, use default
if 'embarazo' in param.name.lower():
result_data['value_selection'] = 'Negativo'
else:
result_data['value_selection'] = 'Normal'
except Exception as e:
print(f" - Error preparing value for parameter {param.name}: {str(e)}")
# Set a default value to avoid validation errors
if param.value_type == 'numeric':
result_data['value_numeric'] = 50.0
elif param.value_type == 'text':
result_data['value_text'] = "Pendiente"
elif param.value_type == 'boolean':
result_data['value_boolean'] = False
elif param.value_type == 'selection':
result_data['value_selection'] = "Normal"
# Create result with values
result = env['lims.result'].create(result_data)
print(f" - Created {len(product_tmpl.parameter_ids)} result fields")
# Evaluar resultados críticos y agregar notas
for result in test.result_ids:
# Leer el registro para actualizar campos computados con contexto especial
result.with_context(skip_value_validation=True).read(['is_critical'])
# Si el resultado es crítico, agregar nota
if result.is_critical and result.parameter_id.value_type == 'numeric':
value = result.value_numeric
param_name = result.parameter_id.name
# Obtener rangos del rango aplicable si existe
normal_min = normal_max = None
if result.applicable_range_id:
normal_min = result.applicable_range_id.normal_min
normal_max = result.applicable_range_id.normal_max
# Obtener la nota apropiada
note = get_critical_note(param_name, value, normal_min, normal_max)
result.write({'notes': note})
print(f" - Agregada nota crítica para {param_name}: valor {value}")
print(f" - Results ready with values and critical notes")
# Update test state directly to bypass permission checks
if test.state == 'in_process':
# Mark as results entered
test.write({
'state': 'result_entered'
})
print(f" - Results entered")
# Then validate the test
if test.state == 'result_entered':
test.write({
'state': 'validated',
'validator_id': env.user.id,
'validation_date': datetime.now()
})
print(f" - Test validated successfully")
except Exception as e:
print(f" - Error processing test {test.name}: {str(e)}")
import traceback
traceback.print_exc()
print(f" - Test state: {test.state}")
print(f" - Product template: {test.product_id.product_tmpl_id.name}")
print(f" - Parameters: {len(test.product_id.product_tmpl_id.parameter_ids)}")
print(f"\nCompleted processing tests for order {order.name}")
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
@ -16,59 +253,123 @@ def create_lab_requests(cr):
except Exception:
pass
try:
# Get patients and doctors - using search instead of ref to be more robust
patient1 = env['res.partner'].search([('patient_identifier', '=', 'P-A87B01'), ('is_patient', '=', True)], limit=1)
patient2 = env['res.partner'].search([('patient_identifier', '=', 'P-C45D02'), ('is_patient', '=', True)], limit=1)
doctor1 = env['res.partner'].search([('doctor_license', '=', 'L-98765'), ('is_doctor', '=', True)], limit=1)
# Get all available analysis products
all_analyses = env['product.template'].search([('is_analysis', '=', True)])
# Find or create pregnancy test
pregnancy_test = env['product.template'].search([('name', '=', 'Prueba de Embarazo'), ('is_analysis', '=', True)], limit=1)
if not pregnancy_test:
# Create pregnancy test if it doesn't exist
pregnancy_test = env['product.template'].create({
'name': 'Prueba de Embarazo',
'is_analysis': True,
'analysis_type': 'immunology',
'list_price': 15.00,
'standard_price': 8.00,
'type': 'service',
'categ_id': env.ref('lims_management.lims_category_immunology').id if env.ref('lims_management.lims_category_immunology', False) else env['product.category'].search([], limit=1).id,
})
print("Created Pregnancy Test product")
if not patient1:
print("Warning: Patient 1 not found, skipping lab requests creation")
return
# Get analysis products - using search instead of ref
hemograma = env['product.template'].search([('name', '=', 'Hemograma Completo'), ('is_analysis', '=', True)], limit=1)
perfil_lipidico = env['product.template'].search([('name', '=', 'Perfil Lipídico'), ('is_analysis', '=', True)], limit=1)
glucosa = env['product.template'].search([('name', '=', 'Glucosa en Sangre'), ('is_analysis', '=', True)], limit=1)
urocultivo = env['product.template'].search([('name', '=', 'Urocultivo'), ('is_analysis', '=', True)], limit=1)
# Create parameter for pregnancy test
preg_param = env['lims.analysis.parameter'].search([('name', '=', 'Beta-hCG')], limit=1)
if not preg_param:
preg_param = env['lims.analysis.parameter'].create({
'name': 'Beta-hCG',
'code': 'BHCG',
'value_type': 'selection',
'selection_values': 'Positivo,Negativo,Indeterminado',
'description': 'Hormona gonadotropina coriónica humana beta'
})
# Create Lab Request 1 - Multiple analyses with same sample type
if patient1 and hemograma and perfil_lipidico:
order1 = env['sale.order'].create({
'partner_id': patient1.id,
'doctor_id': doctor1.id if doctor1 else False,
'is_lab_request': True,
'order_line': [
(0, 0, {'product_id': hemograma.product_variant_id.id, 'product_uom_qty': 1}),
(0, 0, {'product_id': perfil_lipidico.product_variant_id.id, 'product_uom_qty': 1})
]
})
print(f"Created Lab Order 1: {order1.name}")
# Confirm the order to test automatic sample generation
order1.action_confirm()
print(f"Confirmed Lab Order 1. Generated samples: {len(order1.generated_sample_ids)}")
# Create Lab Request 2 - Different sample types
if patient2 and glucosa and urocultivo:
order2 = env['sale.order'].create({
'partner_id': patient2.id,
'is_lab_request': True,
'order_line': [
(0, 0, {'product_id': glucosa.product_variant_id.id, 'product_uom_qty': 1}),
(0, 0, {'product_id': urocultivo.product_variant_id.id, 'product_uom_qty': 1})
]
})
print(f"Created Lab Order 2: {order2.name}")
# Confirm to test automatic sample generation with different types
order2.action_confirm()
print(f"Confirmed Lab Order 2. Generated samples: {len(order2.generated_sample_ids)}")
except Exception as e:
print(f"Error creating lab requests: {str(e)}")
import traceback
traceback.print_exc()
# Link parameter to pregnancy test
env['product.template.parameter'].create({
'product_tmpl_id': pregnancy_test.id,
'parameter_id': preg_param.id,
'sequence': 10,
'required': True
})
# Separate analyses for different purposes
routine_analyses = [a for a in all_analyses if a.name not in ['Prueba de Embarazo']]
# Get all patients
all_patients = env['res.partner'].search([('is_patient', '=', True)], order='id')
# Get available doctors
doctors = env['res.partner'].search([('is_doctor', '=', True)])
print(f"\n=== Starting creation of lab orders for {len(all_patients)} patients ===")
print(f"Available analyses: {len(all_analyses)}")
print(f"Available doctors: {len(doctors)}")
orders_created = 0
failed_orders = []
for idx, patient in enumerate(all_patients):
print(f"\n--- Processing patient {idx+1}/{len(all_patients)}: {patient.name} ---")
# Randomly assign a doctor
doctor = random.choice(doctors) if doctors else False
# Create 2 orders per patient
for order_num in range(1, 3):
try:
order_lines = []
# For pregnant patients, include pregnancy test in one of the orders
if patient.is_pregnant and order_num == 1:
order_lines.append((0, 0, {
'product_id': pregnancy_test.product_variant_id.id,
'product_uom_qty': 1
}))
print(f" - Added pregnancy test for pregnant patient")
# Select random analyses (minimum 2 per order)
num_analyses = random.randint(2, 4)
selected_analyses = random.sample(routine_analyses, min(num_analyses, len(routine_analyses)))
for analysis in selected_analyses:
order_lines.append((0, 0, {
'product_id': analysis.product_variant_id.id,
'product_uom_qty': 1
}))
# Create the order
order = env['sale.order'].create({
'partner_id': patient.id,
'doctor_id': doctor.id if doctor else False,
'is_lab_request': True,
'order_line': order_lines
})
print(f" Order {order_num}: Created {order.name} with {len(order_lines)} analyses")
# Confirm the order
order.action_confirm()
print(f" Order {order_num}: Confirmed. Generated samples: {len(order.generated_sample_ids)}")
# Process tests
process_order_tests(env, order)
orders_created += 1
except Exception as e:
error_msg = f"Patient: {patient.name}, Order {order_num}, Error: {str(e)}"
failed_orders.append(error_msg)
print(f" ERROR creating order {order_num} for {patient.name}: {str(e)}")
import traceback
traceback.print_exc()
# Final summary
print("\n=== SUMMARY ===")
print(f"Total orders created: {orders_created}")
print(f"Failed orders: {len(failed_orders)}")
if failed_orders:
print("\n=== FAILED ORDERS ===")
for idx, error in enumerate(failed_orders, 1):
print(f"{idx}. {error}")
if __name__ == '__main__':
db_name = 'lims_demo'

View File

@ -0,0 +1,85 @@
import odoo
import json
def test_critical_notes_autocomplete(cr):
"""Prueba el autocompletado de notas críticas en resultados de laboratorio"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("\n=== PRUEBA DE AUTOCOMPLETADO DE NOTAS CRÍTICAS ===\n")
# Buscar algunas pruebas con resultados
tests = env['lims.test'].search([('state', 'in', ['result_entered', 'validated'])], limit=5)
if not tests:
print("No se encontraron pruebas con resultados para probar.")
return
for test in tests:
print(f"\nPrueba: {test.name} - {test.product_id.name}")
print(f"Paciente: {test.patient_id.name}")
for result in test.result_ids:
if result.parameter_value_type == 'numeric':
print(f"\n Parámetro: {result.parameter_name}")
print(f" Valor: {result.value_numeric} {result.parameter_unit or ''}")
print(f" ¿Es crítico?: {'' if result.is_critical else 'NO'}")
if result.is_critical:
# Limpiar las notas para probar el autocompletado
result.notes = ''
# Simular cambio en el valor para activar el onchange
with env.cr.savepoint():
# Trigger the onchange by updating the value
result.with_context(force_onchange=True)._onchange_critical_value()
print(f" Nota autocompletada: {result.notes}")
# No guardar los cambios, solo mostrar
env.cr.rollback()
# Probar con valores específicos
print("\n\n=== PRUEBA CON VALORES ESPECÍFICOS ===\n")
# Buscar parámetros específicos
test_params = [
('Glucosa', 200.0, 'high'),
('Glucosa', 50.0, 'low'),
('Hemoglobina', 20.0, 'high'),
('Hemoglobina', 7.0, 'low'),
('Plaquetas', 600000, 'high'),
('Plaquetas', 50000, 'low')
]
for param_name, test_value, expected_type in test_params:
# Buscar un resultado con este parámetro
result = env['lims.result'].search([
('parameter_name', 'ilike', param_name),
('parameter_value_type', '=', 'numeric')
], limit=1)
if result:
print(f"\nProbando {param_name} con valor {test_value} (esperado: {expected_type})")
with env.cr.savepoint():
# Establecer el valor de prueba
result.value_numeric = test_value
result.notes = ''
# Forzar recálculo de is_critical
result._compute_is_out_of_range()
# Trigger el onchange
result._onchange_critical_value()
print(f" ¿Es crítico?: {'' if result.is_critical else 'NO'}")
print(f" Nota generada: {result.notes[:100]}...")
# No guardar
env.cr.rollback()
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
test_critical_notes_autocomplete(cr)

45
test/test_notification.py Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Script de prueba para demostrar el uso de notificaciones
cuando se completan tareas en el sistema LIMS
"""
import time
import subprocess
import sys
def notify_completion():
"""Envía una notificación de sonido cuando se completa una tarea"""
try:
# Ejecutar el comando de PowerShell para el beep
subprocess.run(['powershell.exe', '-c', '[System.Media.SystemSounds]::Beep.Play()'], check=True)
print("[OK] Notificación enviada exitosamente")
except subprocess.CalledProcessError:
print("[ERROR] Error al enviar notificación")
except FileNotFoundError:
print("[ERROR] PowerShell no encontrado en el sistema")
def simulate_task():
"""Simula una tarea que toma tiempo"""
print("Iniciando tarea de prueba...")
# Simular trabajo
for i in range(3):
print(f" Procesando... {i+1}/3")
time.sleep(1)
print("[OK] Tarea completada!")
return True
def main():
print("=== Prueba de Sistema de Notificaciones LIMS ===\n")
# Ejecutar tarea
if simulate_task():
print("\nEnviando notificación de finalización...")
notify_completion()
print("\n=== Fin de la prueba ===")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import odoo
def verify_test_sequence(cr):
"""Verificar que los tests están usando la secuencia correcta"""
print("\n=== VERIFICACIÓN DE SECUENCIAS EN LIMS.TEST ===\n")
# Buscar todos los tests
cr.execute("""
SELECT id, name, create_date
FROM lims_test
ORDER BY create_date
LIMIT 10
""")
tests = cr.fetchall()
print(f"Total de tests encontrados (mostrando primeros 10): {len(tests)}")
print("-" * 50)
print("ID | Código | Fecha de Creación")
print("-" * 50)
for test in tests:
print(f"{test[0]:<4} | {test[1]:<15} | {test[2]}")
# Verificar si hay algún test con nombre "Nuevo"
cr.execute("""
SELECT COUNT(*)
FROM lims_test
WHERE name = 'Nuevo'
""")
nuevo_count = cr.fetchone()[0]
print("\n" + "=" * 50)
print(f"\nTests con nombre 'Nuevo': {nuevo_count}")
if nuevo_count == 0:
print("✅ ÉXITO: Todos los tests están usando la secuencia correcta")
else:
print("❌ ERROR: Hay tests con nombre 'Nuevo'")
# Verificar el patrón de la secuencia
cr.execute("""
SELECT name
FROM lims_test
WHERE name LIKE 'LAB-%'
ORDER BY create_date DESC
LIMIT 5
""")
recent_tests = cr.fetchall()
print("\nÚltimos 5 tests con secuencia LAB-:")
for test in recent_tests:
print(f" - {test[0]}")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
verify_test_sequence(cr)