Compare commits
No commits in common. "dev" and "feature/67-smart-selection-autocomplete" have entirely different histories.
dev
...
feature/67
|
@ -27,9 +27,7 @@
|
|||
"Bash(gh pr merge:*)",
|
||||
"Bash(git cherry-pick:*)",
|
||||
"Bash(del comment_issue_15.txt)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(gh pr create:*)"
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
|
96
CLAUDE.md
96
CLAUDE.md
|
@ -2,11 +2,6 @@
|
|||
|
||||
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.
|
||||
|
@ -21,7 +16,6 @@ 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
|
||||
|
@ -36,13 +30,10 @@ 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`
|
||||
|
@ -50,9 +41,7 @@ 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)
|
||||
|
@ -65,18 +54,15 @@ 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
|
||||
|
@ -94,19 +80,16 @@ 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"
|
||||
|
@ -133,14 +116,12 @@ 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
|
||||
|
@ -151,37 +132,31 @@ 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 -->
|
||||
|
@ -191,15 +166,13 @@ 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.
|
||||
|
@ -207,7 +180,6 @@ 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)
|
||||
|
@ -216,7 +188,6 @@ Required in `.env` file:
|
|||
## Important Patterns
|
||||
|
||||
### Sample Lifecycle States
|
||||
|
||||
```python
|
||||
STATE_PENDING_COLLECTION = 'pending_collection'
|
||||
STATE_COLLECTED = 'collected'
|
||||
|
@ -226,7 +197,6 @@ STATE_CANCELLED = 'cancelled'
|
|||
```
|
||||
|
||||
### Barcode Generation
|
||||
|
||||
- 13-digit format: YYMMDDNNNNNNC
|
||||
- Uses `barcode` Python library for Code-128 generation
|
||||
- Stored as PDF with human-readable text
|
||||
|
@ -234,34 +204,30 @@ 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,
|
||||
|
@ -285,64 +251,53 @@ 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?
|
||||
|
@ -354,20 +309,17 @@ 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`
|
||||
|
@ -378,27 +330,22 @@ 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
|
||||
|
@ -407,18 +354,16 @@ 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:
|
||||
|
@ -430,23 +375,19 @@ 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ÍNICO</h4>
|
||||
```
|
||||
|
||||
- í = í
|
||||
- Í = Í
|
||||
- á = á
|
||||
|
@ -461,16 +402,15 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
|
|||
- Ñ = Ñ
|
||||
|
||||
##### 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>
|
||||
```
|
||||
```
|
|
@ -1,78 +0,0 @@
|
|||
# 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
|
24
issue_body.txt
Normal file
24
issue_body.txt
Normal file
|
@ -0,0 +1,24 @@
|
|||
## 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'
|
38
issue_content.txt
Normal file
38
issue_content.txt
Normal file
|
@ -0,0 +1,38 @@
|
|||
**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
|
|
@ -1,104 +0,0 @@
|
|||
# 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.
|
|
@ -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', 'stock', 'base_setup'],
|
||||
'depends': ['base', 'product', 'sale', 'base_setup'],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'lims_management/static/src/css/lims_test.css',
|
||||
|
@ -45,7 +45,6 @@
|
|||
'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',
|
||||
|
|
|
@ -11,14 +11,5 @@
|
|||
<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>
|
|
@ -10,9 +10,8 @@
|
|||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1985-05-15</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="phone">+1-202-555-0174</field>
|
||||
<field name="email">ana.torres@example.com</field>
|
||||
<field name="vat">03245678-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_2" model="res.partner">
|
||||
|
@ -22,9 +21,8 @@
|
|||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1992-11-20</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7892-3456</field>
|
||||
<field name="phone">+1-202-555-0192</field>
|
||||
<field name="email">carlos.ruiz@example.com</field>
|
||||
<field name="vat">04567890-1</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_3" model="res.partner">
|
||||
|
@ -34,9 +32,8 @@
|
|||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1978-03-10</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="phone">+1-202-555-0201</field>
|
||||
<field name="email">maria.gonzalez@example.com</field>
|
||||
<field name="vat">01234567-8</field>
|
||||
</record>
|
||||
|
||||
<!-- Datos de Demostración para Médicos -->
|
||||
|
@ -44,7 +41,7 @@
|
|||
<field name="name">Dr. Luis Herrera</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-98765</field>
|
||||
<field name="phone">+503 2234-5678</field>
|
||||
<field name="phone">+1-202-555-0145</field>
|
||||
<field name="email">luis.herrera@hospital.com</field>
|
||||
</record>
|
||||
|
||||
|
@ -52,14 +49,14 @@
|
|||
<field name="name">Dra. Sofia Vargas</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-54321</field>
|
||||
<field name="phone">+503 2345-6789</field>
|
||||
<field name="phone">+1-202-555-0133</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">+503 7456-7890</field>
|
||||
<field name="phone">+1-202-555-0188</field>
|
||||
<field name="email">laura.mendoza@example.com</field>
|
||||
</record>
|
||||
|
||||
|
@ -73,562 +70,5 @@
|
|||
<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>
|
Binary file not shown.
|
@ -258,10 +258,6 @@ 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
|
||||
|
@ -305,8 +301,8 @@ class LimsResult(models.Model):
|
|||
_('Para parámetros Sí/No solo se debe marcar el checkbox.')
|
||||
)
|
||||
|
||||
# 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':
|
||||
# Solo requerir valor si la prueba no está en borrador
|
||||
if not has_value and record.parameter_id and record.test_id.state != 'draft':
|
||||
raise ValidationError(
|
||||
_('Debe ingresar un valor para el resultado del parámetro %s.') % record.parameter_name
|
||||
)
|
||||
|
@ -369,101 +365,6 @@ 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.
|
||||
|
||||
|
|
|
@ -116,21 +116,6 @@ 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."""
|
||||
|
@ -169,6 +154,17 @@ 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."""
|
||||
|
@ -494,20 +490,13 @@ class LimsTest(models.Model):
|
|||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""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'
|
||||
|
||||
"""Override create para validaciones adicionales"""
|
||||
# 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'))
|
||||
|
||||
test = super().create(vals)
|
||||
# Generar resultados automáticamente
|
||||
test._generate_test_results()
|
||||
return test
|
||||
return super().create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para auditoría adicional"""
|
||||
|
|
|
@ -29,17 +29,6 @@ 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)"
|
||||
|
@ -65,34 +54,6 @@ 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"""
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
<?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 Ó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álisis de Ó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 Ó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 órdenes de laboratorio registradas
|
||||
</p>
|
||||
<p>
|
||||
Este dashboard muestra el estado actual de todas las ó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é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álisis por Té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é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é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á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í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á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ámetros que está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álisis Má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álisis por Perí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álisis Má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álisis registrados
|
||||
</p>
|
||||
<p>
|
||||
Este dashboard muestra los análisis má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ón por Gé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í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ón Demográ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ón de tests por características demográ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', '>=', context_today())]"/>
|
||||
<filter string="Esta Semana" name="this_week" domain="[('create_date', '>=', (context_today() + relativedelta(days=-7)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Este Mes" name="this_month" domain="[('create_date', '>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Este Año" name="this_year" domain="[('create_date', '>=', (context_today() + relativedelta(month=1, day=1)).strftime('%Y-%m-%d'))]"/>
|
||||
|
||||
<!-- Agrupaciones -->
|
||||
<group expand="0" string="Agrupar Por">
|
||||
<filter string="Té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á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íticos" name="critical" domain="[('is_critical', '=', True)]"/>
|
||||
|
||||
<!-- Agrupaciones -->
|
||||
<group expand="0" string="Agrupar Por">
|
||||
<filter string="Parámetro" name="group_by_parameter" context="{'group_by': 'parameter_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -155,51 +155,6 @@
|
|||
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 Órdenes"
|
||||
parent="menu_lims_dashboards"
|
||||
action="action_lab_order_dashboard"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_technician_productivity_dashboard"
|
||||
name="Productividad de Té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ámetros Fuera de Rango"
|
||||
parent="menu_lims_dashboards"
|
||||
action="action_out_of_range_dashboard"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_top_analysis_dashboard"
|
||||
name="Análisis Más Solicitados"
|
||||
parent="menu_lims_dashboards"
|
||||
action="action_top_analysis_dashboard"
|
||||
sequence="50"/>
|
||||
|
||||
<menuitem id="menu_test_demographics_dashboard"
|
||||
name="Distribución Demográfica"
|
||||
parent="menu_lims_dashboards"
|
||||
action="action_test_demographics_dashboard"
|
||||
sequence="60"/>
|
||||
|
||||
<!-- Submenú de Reportes -->
|
||||
<menuitem
|
||||
id="lims_menu_reports"
|
||||
|
|
32
pr_body_10.txt
Normal file
32
pr_body_10.txt
Normal file
|
@ -0,0 +1,32 @@
|
|||
## 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
|
74
pr_body_54.txt
Normal file
74
pr_body_54.txt
Normal file
|
@ -0,0 +1,74 @@
|
|||
## 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
|
48
pr_body_9.txt
Normal file
48
pr_body_9.txt
Normal file
|
@ -0,0 +1,48 @@
|
|||
## 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
|
|
@ -63,21 +63,28 @@ try:
|
|||
env.cr.rollback()
|
||||
continue
|
||||
|
||||
# Verificación final usando el ORM
|
||||
# Verificación final con consulta directa a la BD
|
||||
print("\n" + "="*60)
|
||||
print("VERIFICACIÓN FINAL:")
|
||||
print("VERIFICACIÓN FINAL (consulta directa):")
|
||||
print("="*60)
|
||||
|
||||
# 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")
|
||||
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")
|
||||
|
||||
# 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:")
|
||||
|
|
|
@ -216,13 +216,12 @@ def create_demo_lab_data(cr):
|
|||
# Procesar cada muestra
|
||||
for sample in order.generated_sample_ids:
|
||||
# Marcar como recolectada
|
||||
if sample.state == 'pending_collection':
|
||||
if sample.sample_state == 'pending_collection':
|
||||
sample.action_collect()
|
||||
print(f" ✓ Muestra {sample.name} recolectada")
|
||||
|
||||
# Procesar pruebas de esta muestra
|
||||
tests = env['lims.test'].search([('sample_id', '=', sample.id)])
|
||||
for test in tests:
|
||||
for test in sample.test_ids:
|
||||
print(f" - Procesando prueba: {test.product_id.name}")
|
||||
|
||||
# Iniciar proceso si está en borrador
|
||||
|
@ -242,7 +241,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 == tests[0])
|
||||
should_validate = (idx < 2) or (idx == 2 and test == sample.test_ids[0])
|
||||
|
||||
if should_validate and test.state == 'result_entered':
|
||||
test.action_validate()
|
||||
|
|
|
@ -1,242 +1,5 @@
|
|||
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, {})
|
||||
|
@ -253,123 +16,59 @@ def create_lab_requests(cr):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# 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")
|
||||
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)
|
||||
|
||||
# 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'
|
||||
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 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})
|
||||
]
|
||||
})
|
||||
|
||||
# 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}")
|
||||
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()
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import odoo
|
||||
import json
|
||||
|
||||
def test_critical_notes_autocomplete(cr):
|
||||
"""Prueba el autocompletado de notas críticas en resultados de laboratorio"""
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
print("\n=== PRUEBA DE AUTOCOMPLETADO DE NOTAS CRÍTICAS ===\n")
|
||||
|
||||
# Buscar algunas pruebas con resultados
|
||||
tests = env['lims.test'].search([('state', 'in', ['result_entered', 'validated'])], limit=5)
|
||||
|
||||
if not tests:
|
||||
print("No se encontraron pruebas con resultados para probar.")
|
||||
return
|
||||
|
||||
for test in tests:
|
||||
print(f"\nPrueba: {test.name} - {test.product_id.name}")
|
||||
print(f"Paciente: {test.patient_id.name}")
|
||||
|
||||
for result in test.result_ids:
|
||||
if result.parameter_value_type == 'numeric':
|
||||
print(f"\n Parámetro: {result.parameter_name}")
|
||||
print(f" Valor: {result.value_numeric} {result.parameter_unit or ''}")
|
||||
print(f" ¿Es crítico?: {'SÍ' if result.is_critical else 'NO'}")
|
||||
|
||||
if result.is_critical:
|
||||
# Limpiar las notas para probar el autocompletado
|
||||
result.notes = ''
|
||||
|
||||
# Simular cambio en el valor para activar el onchange
|
||||
with env.cr.savepoint():
|
||||
# Trigger the onchange by updating the value
|
||||
result.with_context(force_onchange=True)._onchange_critical_value()
|
||||
|
||||
print(f" Nota autocompletada: {result.notes}")
|
||||
|
||||
# No guardar los cambios, solo mostrar
|
||||
env.cr.rollback()
|
||||
|
||||
# Probar con valores específicos
|
||||
print("\n\n=== PRUEBA CON VALORES ESPECÍFICOS ===\n")
|
||||
|
||||
# Buscar parámetros específicos
|
||||
test_params = [
|
||||
('Glucosa', 200.0, 'high'),
|
||||
('Glucosa', 50.0, 'low'),
|
||||
('Hemoglobina', 20.0, 'high'),
|
||||
('Hemoglobina', 7.0, 'low'),
|
||||
('Plaquetas', 600000, 'high'),
|
||||
('Plaquetas', 50000, 'low')
|
||||
]
|
||||
|
||||
for param_name, test_value, expected_type in test_params:
|
||||
# Buscar un resultado con este parámetro
|
||||
result = env['lims.result'].search([
|
||||
('parameter_name', 'ilike', param_name),
|
||||
('parameter_value_type', '=', 'numeric')
|
||||
], limit=1)
|
||||
|
||||
if result:
|
||||
print(f"\nProbando {param_name} con valor {test_value} (esperado: {expected_type})")
|
||||
|
||||
with env.cr.savepoint():
|
||||
# Establecer el valor de prueba
|
||||
result.value_numeric = test_value
|
||||
result.notes = ''
|
||||
|
||||
# Forzar recálculo de is_critical
|
||||
result._compute_is_out_of_range()
|
||||
|
||||
# Trigger el onchange
|
||||
result._onchange_critical_value()
|
||||
|
||||
print(f" ¿Es crítico?: {'SÍ' if result.is_critical else 'NO'}")
|
||||
print(f" Nota generada: {result.notes[:100]}...")
|
||||
|
||||
# No guardar
|
||||
env.cr.rollback()
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
test_critical_notes_autocomplete(cr)
|
|
@ -1,45 +0,0 @@
|
|||
#!/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()
|
|
@ -1,64 +0,0 @@
|
|||
#!/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)
|
Loading…
Reference in New Issue
Block a user