Compare commits
No commits in common. "dev" and "feature/63-sample-view-redirection" have entirely different histories.
dev
...
feature/63
|
@ -25,11 +25,7 @@
|
|||
"Bash(bash:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(gh pr merge:*)",
|
||||
"Bash(git cherry-pick:*)",
|
||||
"Bash(del comment_issue_15.txt)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(gh pr create:*)"
|
||||
"Bash(git cherry-pick:*)"
|
||||
],
|
||||
"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
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB |
|
@ -1,294 +0,0 @@
|
|||
# Plan de Desarrollo - Issue #11: Informe Final de Resultados en PDF
|
||||
|
||||
## Resumen del Issue
|
||||
Crear una plantilla de reporte QWeb compleja y profesional para el informe de resultados de laboratorio, con capacidad de resaltar valores fuera de rango, incluir datos del laboratorio y paciente, y guardarse automáticamente como adjunto.
|
||||
|
||||
## Análisis de Requerimientos
|
||||
|
||||
### Componentes del Reporte
|
||||
1. **Encabezado**
|
||||
- Logo del laboratorio
|
||||
- Datos del laboratorio (nombre, dirección, teléfono)
|
||||
- Datos del paciente (nombre, ID, edad, sexo)
|
||||
- Número de orden y fecha
|
||||
|
||||
2. **Sección de Resultados**
|
||||
- Agrupación por tipo de análisis
|
||||
- Tabla con columnas: Parámetro | Resultado | Unidad | Valor de Referencia
|
||||
- Resaltado visual de valores fuera de rango (color/símbolo)
|
||||
- Indicación especial para valores críticos
|
||||
|
||||
3. **Sección de Comentarios**
|
||||
- Observaciones generales de la orden
|
||||
- Notas específicas por resultado si las hay
|
||||
|
||||
4. **Pie del Informe**
|
||||
- Datos del profesional validador (nombre, título, registro)
|
||||
- Fecha y hora de validación
|
||||
- Firma digital o espacio para firma
|
||||
|
||||
### Requisitos Técnicos
|
||||
- Botón "Imprimir Informe de Resultados" solo activo cuando todas las pruebas estén en estado "validated"
|
||||
- PDF generado se guarda automáticamente como adjunto en la orden
|
||||
- Formato profesional y limpio
|
||||
|
||||
## Estructura de Archivos a Crear/Modificar
|
||||
|
||||
### 1. Reporte QWeb
|
||||
```
|
||||
lims_management/
|
||||
├── reports/
|
||||
│ ├── lab_results_report.xml # Plantilla QWeb del reporte
|
||||
│ └── lab_results_report_data.xml # Definición del reporte y paper format
|
||||
```
|
||||
|
||||
### 2. Modelos a Modificar
|
||||
```
|
||||
lims_management/
|
||||
├── models/
|
||||
│ └── sale_order.py # Agregar método para generar reporte
|
||||
```
|
||||
|
||||
### 3. Vistas a Modificar
|
||||
```
|
||||
lims_management/
|
||||
├── views/
|
||||
│ └── sale_order_views.xml # Agregar botón de impresión
|
||||
```
|
||||
|
||||
### 4. Manifest
|
||||
```
|
||||
lims_management/
|
||||
├── __manifest__.py # Agregar archivos de reportes
|
||||
```
|
||||
|
||||
## Implementación Detallada
|
||||
|
||||
### Fase 1: Estructura Base del Reporte
|
||||
|
||||
#### 1.1 Definir Paper Format Personalizado
|
||||
```xml
|
||||
<!-- lab_results_report_data.xml -->
|
||||
<record id="paperformat_lab_results" model="report.paperformat">
|
||||
<field name="name">Formato Resultados de Laboratorio</field>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">40</field>
|
||||
<field name="margin_bottom">25</field>
|
||||
<field name="margin_left">10</field>
|
||||
<field name="margin_right">10</field>
|
||||
<field name="header_spacing">35</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
#### 1.2 Definir Acción del Reporte
|
||||
```xml
|
||||
<record id="action_report_lab_results" model="ir.actions.report">
|
||||
<field name="name">Informe de Resultados</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">lims_management.report_lab_results</field>
|
||||
<field name="report_file">lims_management.report_lab_results</field>
|
||||
<field name="paperformat_id" ref="paperformat_lab_results"/>
|
||||
<field name="attachment">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="attachment_use">True</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
### Fase 2: Plantilla QWeb del Reporte
|
||||
|
||||
#### 2.1 Estructura Principal
|
||||
```xml
|
||||
<!-- lab_results_report.xml -->
|
||||
<template id="report_lab_results">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="lims_management.report_lab_results_document"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 2.2 Documento Individual
|
||||
```xml
|
||||
<template id="report_lab_results_document">
|
||||
<div class="page">
|
||||
<!-- Encabezado -->
|
||||
<div class="header">
|
||||
<!-- Logo y datos del laboratorio -->
|
||||
<!-- Datos del paciente -->
|
||||
</div>
|
||||
|
||||
<!-- Cuerpo con resultados -->
|
||||
<div class="body">
|
||||
<!-- Iterar por pruebas validadas -->
|
||||
<t t-foreach="o.lab_test_ids.filtered(lambda t: t.state == 'validated')" t-as="test">
|
||||
<!-- Tabla de resultados -->
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Pie con validación -->
|
||||
<div class="footer">
|
||||
<!-- Datos del validador -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Fase 3: Lógica del Modelo
|
||||
|
||||
#### 3.1 Método para Verificar Estado
|
||||
```python
|
||||
# En sale_order.py
|
||||
@api.depends('lab_test_ids.state')
|
||||
def _compute_can_print_results(self):
|
||||
for order in self:
|
||||
tests = order.lab_test_ids
|
||||
order.can_print_results = (
|
||||
tests and
|
||||
all(test.state == 'validated' for test in tests)
|
||||
)
|
||||
|
||||
can_print_results = fields.Boolean(
|
||||
compute='_compute_can_print_results',
|
||||
string="Puede Imprimir Resultados"
|
||||
)
|
||||
```
|
||||
|
||||
#### 3.2 Método para Generar y Adjuntar PDF
|
||||
```python
|
||||
def action_print_lab_results(self):
|
||||
"""Genera el informe de resultados y lo adjunta"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar que todas las pruebas estén validadas
|
||||
if not self.can_print_results:
|
||||
raise ValidationError("No se puede imprimir: hay pruebas sin validar")
|
||||
|
||||
# Generar el reporte
|
||||
return self.env.ref('lims_management.action_report_lab_results').report_action(self)
|
||||
```
|
||||
|
||||
### Fase 4: Botón en la Vista
|
||||
|
||||
```xml
|
||||
<!-- En sale_order_views.xml -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_print_lab_results"
|
||||
string="Imprimir Informe de Resultados"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="not can_print_results or not is_lab_request"/>
|
||||
</xpath>
|
||||
```
|
||||
|
||||
### Fase 5: Estilos CSS para el Reporte
|
||||
|
||||
#### 5.1 Estilos para Resaltado
|
||||
```xml
|
||||
<style>
|
||||
.result-out-of-range {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-critical {
|
||||
background-color: #f2dede;
|
||||
color: #a94442;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-normal {
|
||||
color: #5cb85c;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
#### 5.2 Aplicación Condicional
|
||||
```xml
|
||||
<td t-attf-class="#{result.is_critical and 'result-critical' or result.is_out_of_range and 'result-out-of-range' or 'result-normal'}">
|
||||
<t t-esc="result.value_display"/>
|
||||
</td>
|
||||
```
|
||||
|
||||
### Fase 6: Datos Demo para Pruebas
|
||||
|
||||
Crear script Python que:
|
||||
1. Genere órdenes con múltiples análisis
|
||||
2. Ingrese resultados variados (normales, fuera de rango, críticos)
|
||||
3. Valide las pruebas
|
||||
4. Permita probar la generación del PDF
|
||||
|
||||
## Consideraciones Especiales
|
||||
|
||||
### 1. Manejo de Caracteres Especiales
|
||||
- Usar entidades HTML para tildes y ñ en el reporte
|
||||
- Ejemplo: `Í` para Í, `ñ` para ñ
|
||||
|
||||
### 2. Códigos de Barras
|
||||
- Usar widget nativo de Odoo 18: `t-options="{'widget': 'barcode', 'type': 'Code128'}"`
|
||||
- NO usar rutas deprecated como `/report/barcode/`
|
||||
|
||||
### 3. Agrupación de Resultados
|
||||
- Agrupar por tipo de análisis para mejor legibilidad
|
||||
- Mantener orden por secuencia definida en parámetros
|
||||
|
||||
### 4. Seguridad
|
||||
- Solo usuarios con permisos de lectura en órdenes pueden generar el reporte
|
||||
- El PDF se adjunta con permisos heredados de la orden
|
||||
|
||||
## Secuencia de Implementación
|
||||
|
||||
1. **Crear estructura base de reportes**
|
||||
- Crear carpeta reports/
|
||||
- Definir paper format y acción
|
||||
|
||||
2. **Implementar plantilla QWeb básica**
|
||||
- Estructura HTML con secciones
|
||||
- Iterar sobre pruebas y resultados
|
||||
|
||||
3. **Agregar lógica en modelo**
|
||||
- Campo computado can_print_results
|
||||
- Método action_print_lab_results
|
||||
|
||||
4. **Integrar botón en vista**
|
||||
- Agregar botón con visibilidad condicional
|
||||
|
||||
5. **Implementar estilos y resaltado**
|
||||
- CSS para valores fuera de rango
|
||||
- Clases condicionales en plantilla
|
||||
|
||||
6. **Configurar adjunto automático**
|
||||
- Configurar attachment en ir.actions.report
|
||||
- Verificar guardado en ir.attachment
|
||||
|
||||
7. **Crear datos demo y probar**
|
||||
- Script para generar casos de prueba
|
||||
- Validar formato y contenido del PDF
|
||||
|
||||
## Validación y Pruebas
|
||||
|
||||
### Casos de Prueba
|
||||
1. **Orden sin pruebas validadas**: Botón invisible
|
||||
2. **Orden parcialmente validada**: Botón invisible
|
||||
3. **Orden completamente validada**: Botón visible, genera PDF
|
||||
4. **Valores normales**: Sin resaltado
|
||||
5. **Valores fuera de rango**: Resaltado en color
|
||||
6. **Valores críticos**: Resaltado especial
|
||||
7. **PDF adjunto**: Verificar que se guarda en la orden
|
||||
|
||||
### Criterios de Aceptación
|
||||
- [ ] Reporte muestra todos los datos requeridos
|
||||
- [ ] Valores fuera de rango se resaltan correctamente
|
||||
- [ ] Botón solo visible cuando todas las pruebas están validadas
|
||||
- [ ] PDF se genera con formato profesional
|
||||
- [ ] PDF se adjunta automáticamente a la orden
|
||||
- [ ] Datos del validador aparecen correctamente
|
||||
- [ ] Comentarios y observaciones se muestran si existen
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar Odoo 18 syntax para invisibility: `invisible="not can_print_results"`
|
||||
- Verificar compatibilidad con wkhtmltopdf para renderizado PDF
|
||||
- Considerar tamaño del archivo para órdenes con muchos análisis
|
||||
- El attachment_use=True garantiza que no se regenere si ya existe
|
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,12 +45,9 @@
|
|||
'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',
|
||||
'reports/lab_results_report_data.xml',
|
||||
'reports/lab_results_report.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo_users.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.
Binary file not shown.
Binary file not shown.
|
@ -93,15 +93,7 @@ class LimsResult(models.Model):
|
|||
)
|
||||
|
||||
value_selection = fields.Char(
|
||||
string='Valor de Selección',
|
||||
help='Ingrese el valor o las primeras letras. Ej: P para Positivo, N para Negativo'
|
||||
)
|
||||
|
||||
# Campo para mostrar las opciones disponibles
|
||||
selection_options_display = fields.Char(
|
||||
string='Opciones disponibles',
|
||||
compute='_compute_selection_options_display',
|
||||
help='Opciones válidas para este parámetro'
|
||||
string='Valor de Selección'
|
||||
)
|
||||
|
||||
value_boolean = fields.Boolean(
|
||||
|
@ -258,10 +250,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
|
||||
|
@ -287,17 +275,6 @@ class LimsResult(models.Model):
|
|||
raise ValidationError(
|
||||
_('Para parámetros de selección solo se debe elegir una opción.')
|
||||
)
|
||||
# Validar que el valor seleccionado sea válido
|
||||
if has_value and record.parameter_id:
|
||||
valid_options = record.parameter_id.get_selection_list()
|
||||
if valid_options and record.value_selection not in valid_options:
|
||||
# Intentar autocompletar antes de rechazar
|
||||
autocompleted = record._validate_and_autocomplete_selection(record.value_selection)
|
||||
if autocompleted not in valid_options:
|
||||
raise ValidationError(
|
||||
_('El valor "%s" no es una opción válida. Opciones disponibles: %s') %
|
||||
(record.value_selection, ', '.join(valid_options))
|
||||
)
|
||||
elif value_type == 'boolean':
|
||||
has_value = True # Boolean siempre tiene valor (True o False)
|
||||
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_selection:
|
||||
|
@ -305,8 +282,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
|
||||
)
|
||||
|
@ -324,214 +301,4 @@ class LimsResult(models.Model):
|
|||
# Si es selección, obtener las opciones
|
||||
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
|
||||
# Esto se usará en las vistas para mostrar las opciones dinámicamente
|
||||
pass
|
||||
|
||||
@api.depends('parameter_id', 'parameter_id.selection_values')
|
||||
def _compute_selection_options_display(self):
|
||||
"""Calcula las opciones disponibles para mostrar al usuario."""
|
||||
for record in self:
|
||||
if record.parameter_id and record.parameter_value_type == 'selection':
|
||||
options = record.parameter_id.get_selection_list()
|
||||
if options:
|
||||
record.selection_options_display = ' | '.join(options)
|
||||
else:
|
||||
record.selection_options_display = 'Sin opciones definidas'
|
||||
else:
|
||||
record.selection_options_display = False
|
||||
|
||||
@api.onchange('value_selection')
|
||||
def _onchange_value_selection(self):
|
||||
"""Autocompleta el valor de selección basado en coincidencia parcial."""
|
||||
if self.value_selection and self.parameter_id and self.parameter_value_type == 'selection':
|
||||
# Obtener las opciones disponibles
|
||||
options = self.parameter_id.get_selection_list()
|
||||
if options:
|
||||
# Convertir el valor ingresado a mayúsculas para comparación
|
||||
input_upper = self.value_selection.upper().strip()
|
||||
|
||||
# Buscar coincidencias
|
||||
matches = []
|
||||
for option in options:
|
||||
option_upper = option.upper()
|
||||
if option_upper.startswith(input_upper):
|
||||
matches.append(option)
|
||||
|
||||
# Si hay exactamente una coincidencia, autocompletar
|
||||
if len(matches) == 1:
|
||||
self.value_selection = matches[0]
|
||||
elif len(matches) == 0:
|
||||
# Si no hay coincidencias directas, buscar coincidencias parciales
|
||||
for option in options:
|
||||
if input_upper in option.upper():
|
||||
matches.append(option)
|
||||
|
||||
# Si hay una sola coincidencia parcial, autocompletar
|
||||
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.
|
||||
|
||||
Esta función es llamada antes de guardar para asegurar que el valor
|
||||
sea válido y esté completo.
|
||||
"""
|
||||
if not value or not self.parameter_id or self.parameter_value_type != 'selection':
|
||||
return value
|
||||
|
||||
options = self.parameter_id.get_selection_list()
|
||||
if not options:
|
||||
return value
|
||||
|
||||
# Convertir a mayúsculas para comparación
|
||||
value_upper = value.upper().strip()
|
||||
|
||||
# Buscar coincidencias exactas primero
|
||||
for option in options:
|
||||
if option.upper() == value_upper:
|
||||
return option
|
||||
|
||||
# Buscar coincidencias que empiecen con el valor
|
||||
matches = []
|
||||
for option in options:
|
||||
if option.upper().startswith(value_upper):
|
||||
matches.append(option)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
elif len(matches) > 1:
|
||||
# Si hay múltiples coincidencias, intentar ser más específico
|
||||
# Preferir la coincidencia más corta
|
||||
shortest = min(matches, key=len)
|
||||
return shortest
|
||||
|
||||
# Si no hay coincidencias por inicio, buscar contenido
|
||||
for option in options:
|
||||
if value_upper in option.upper():
|
||||
matches.append(option)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
elif len(matches) > 1:
|
||||
# Retornar la primera coincidencia
|
||||
return matches[0]
|
||||
|
||||
# Si no hay ninguna coincidencia, retornar el valor original
|
||||
# La validación en @api.constrains se encargará de rechazarlo
|
||||
return value
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""Override create para autocompletar valores de selección."""
|
||||
if 'value_selection' in vals and vals.get('value_selection'):
|
||||
# Necesitamos el parameter_id para validar
|
||||
if 'parameter_id' in vals:
|
||||
parameter = self.env['lims.analysis.parameter'].browse(vals['parameter_id'])
|
||||
if parameter.value_type == 'selection':
|
||||
# Crear un registro temporal para usar el método
|
||||
temp_record = self.new({'parameter_id': parameter.id, 'parameter_value_type': 'selection'})
|
||||
vals['value_selection'] = temp_record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||
return super(LimsResult, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para autocompletar valores de selección."""
|
||||
if 'value_selection' in vals and vals.get('value_selection'):
|
||||
for record in self:
|
||||
if record.parameter_value_type == 'selection':
|
||||
vals['value_selection'] = record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||
break # Solo necesitamos procesar una vez
|
||||
return super(LimsResult, self).write(vals)
|
||||
pass
|
|
@ -28,14 +28,6 @@ class LimsTest(models.Model):
|
|||
ondelete='restrict'
|
||||
)
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Orden de Venta',
|
||||
related='sale_order_line_id.order_id',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
patient_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Paciente',
|
||||
|
@ -116,21 +108,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 +146,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."""
|
||||
|
@ -187,40 +175,11 @@ class LimsTest(models.Model):
|
|||
|
||||
# Crear una línea de resultado por cada parámetro
|
||||
for param_config in template_parameters:
|
||||
# Preparar las notas/instrucciones
|
||||
notes = param_config.instructions or ''
|
||||
|
||||
# Si es un parámetro de tipo selection, agregar instrucciones de autocompletado
|
||||
if param_config.parameter_value_type == 'selection':
|
||||
selection_values = param_config.parameter_id.selection_values
|
||||
if selection_values:
|
||||
options = [v.strip() for v in selection_values.split(',')]
|
||||
if options:
|
||||
# Generar instrucciones automáticas
|
||||
auto_instructions = "Opciones: " + ", ".join(options) + ". "
|
||||
auto_instructions += "Puede escribir las iniciales o parte del texto. "
|
||||
|
||||
# Agregar ejemplos específicos
|
||||
examples = []
|
||||
for opt in options[:3]: # Mostrar ejemplos para las primeras 3 opciones
|
||||
if opt:
|
||||
initial = opt[0].upper()
|
||||
examples.append(f"{initial}={opt}")
|
||||
|
||||
if examples:
|
||||
auto_instructions += "Ej: " + ", ".join(examples)
|
||||
|
||||
# Combinar con instrucciones existentes
|
||||
if notes:
|
||||
notes = auto_instructions + "\n" + notes
|
||||
else:
|
||||
notes = auto_instructions
|
||||
|
||||
result_vals = {
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_config.parameter_id.id,
|
||||
'sequence': param_config.sequence,
|
||||
'notes': notes
|
||||
'notes': param_config.instructions or ''
|
||||
}
|
||||
|
||||
# Inicializar valores según el tipo
|
||||
|
@ -288,7 +247,7 @@ class LimsTest(models.Model):
|
|||
|
||||
# Verificar que todos los resultados tengan valores ingresados
|
||||
empty_results = self.result_ids.filtered(
|
||||
lambda r: not r.value_text and not r.value_numeric and not r.value_selection and not r.value_boolean and r.parameter_id.value_type != 'boolean'
|
||||
lambda r: not r.value_text and not r.value_numeric and not r.value_boolean and r.parameter_id.value_type != 'boolean'
|
||||
)
|
||||
if empty_results:
|
||||
params = ', '.join(empty_results.mapped('parameter_id.name'))
|
||||
|
@ -494,20 +453,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"""
|
||||
|
|
|
@ -102,26 +102,6 @@ class LimsParameterRange(models.Model):
|
|||
readonly=True
|
||||
)
|
||||
|
||||
reference_text = fields.Char(
|
||||
string='Texto de Referencia',
|
||||
compute='_compute_reference_text',
|
||||
store=False,
|
||||
help='Texto formateado del rango de referencia'
|
||||
)
|
||||
|
||||
@api.depends('normal_min', 'normal_max', 'parameter_unit')
|
||||
def _compute_reference_text(self):
|
||||
"""Computa el texto de referencia basado en los valores min/max y unidad"""
|
||||
for record in self:
|
||||
if record.normal_min is not False and record.normal_max is not False:
|
||||
unit = record.parameter_unit or ''
|
||||
# Formatear los números para evitar decimales innecesarios
|
||||
min_val = f"{record.normal_min:.2f}".rstrip('0').rstrip('.')
|
||||
max_val = f"{record.normal_max:.2f}".rstrip('0').rstrip('.')
|
||||
record.reference_text = f"{min_val} - {max_val} {unit}".strip()
|
||||
else:
|
||||
record.reference_text = "N/A"
|
||||
|
||||
@api.depends('parameter_id', 'gender', 'age_min', 'age_max', 'pregnant')
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -339,56 +339,3 @@ class SaleOrder(models.Model):
|
|||
|
||||
# Retornar la acción de imprimir el reporte para las muestras activas
|
||||
return report.report_action(active_samples)
|
||||
|
||||
# Fields for lab results report
|
||||
can_print_results = fields.Boolean(
|
||||
string="Puede Imprimir Resultados",
|
||||
compute='_compute_can_print_results',
|
||||
help="Indica si todas las pruebas están validadas y se puede imprimir el informe"
|
||||
)
|
||||
|
||||
lab_test_ids = fields.One2many(
|
||||
'lims.test',
|
||||
'sale_order_id',
|
||||
string="Pruebas de Laboratorio",
|
||||
readonly=True,
|
||||
help="Todas las pruebas de laboratorio asociadas a esta orden"
|
||||
)
|
||||
|
||||
referring_doctor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Médico Solicitante",
|
||||
related='doctor_id',
|
||||
readonly=True,
|
||||
help="Médico que solicitó los análisis"
|
||||
)
|
||||
|
||||
lab_notes = fields.Text(
|
||||
string="Observaciones del Laboratorio",
|
||||
help="Observaciones generales sobre la orden o los resultados"
|
||||
)
|
||||
|
||||
@api.depends('lab_test_ids.state')
|
||||
def _compute_can_print_results(self):
|
||||
"""Compute if results can be printed (all tests validated)"""
|
||||
for order in self:
|
||||
tests = order.lab_test_ids
|
||||
order.can_print_results = (
|
||||
tests and
|
||||
all(test.state == 'validated' for test in tests)
|
||||
)
|
||||
|
||||
def action_print_lab_results(self):
|
||||
"""Generate and print lab results report"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verify all tests are validated
|
||||
if not self.can_print_results:
|
||||
raise UserError(_("No se puede imprimir el informe: hay pruebas sin validar"))
|
||||
|
||||
# Ensure this is a lab request
|
||||
if not self.is_lab_request:
|
||||
raise UserError(_("Esta no es una orden de laboratorio"))
|
||||
|
||||
# Generate the report
|
||||
return self.env.ref('lims_management.action_report_lab_results').report_action(self)
|
||||
|
|
|
@ -145,81 +145,74 @@ class StockLot(models.Model):
|
|||
)
|
||||
|
||||
def action_collect(self):
|
||||
"""Mark sample(s) as collected"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
|
||||
record.message_post(
|
||||
body='Muestra recolectada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recolectada',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Mark sample as collected"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
|
||||
self.message_post(
|
||||
body='Muestra recolectada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recolectada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_receive(self):
|
||||
"""Mark sample(s) as received in laboratory"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'received'})
|
||||
record.message_post(
|
||||
body='Muestra recibida en laboratorio por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recibida',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Mark sample as received in laboratory"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'received'})
|
||||
self.message_post(
|
||||
body='Muestra recibida en laboratorio por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recibida',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_start_analysis(self):
|
||||
"""Start analysis process"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'in_process'})
|
||||
record.message_post(
|
||||
body='Análisis iniciado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: En Proceso',
|
||||
message_type='notification'
|
||||
)
|
||||
old_state = self.state
|
||||
self.write({'state': 'in_process'})
|
||||
self.message_post(
|
||||
body='Análisis iniciado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: En Proceso',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_complete_analysis(self):
|
||||
"""Mark analysis as completed"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'analyzed'})
|
||||
record.message_post(
|
||||
body='Análisis completado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Analizada',
|
||||
message_type='notification'
|
||||
)
|
||||
old_state = self.state
|
||||
self.write({'state': 'analyzed'})
|
||||
self.message_post(
|
||||
body='Análisis completado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Analizada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_store(self):
|
||||
"""Store the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'stored'})
|
||||
record.message_post(
|
||||
body='Muestra almacenada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Almacenada',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Store the sample"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'stored'})
|
||||
self.message_post(
|
||||
body='Muestra almacenada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Almacenada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_dispose(self):
|
||||
"""Dispose of the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'disposed'})
|
||||
record.message_post(
|
||||
body='Muestra desechada por %s. Motivo de disposición registrado.' % self.env.user.name,
|
||||
subject='Estado actualizado: Desechada',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Dispose of the sample"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'disposed'})
|
||||
self.message_post(
|
||||
body='Muestra desechada por %s. Motivo de disposición registrado.' % self.env.user.name,
|
||||
subject='Estado actualizado: Desechada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancel the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'cancelled'})
|
||||
record.message_post(
|
||||
body='Muestra cancelada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Cancelada',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Cancel the sample"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'cancelled'})
|
||||
self.message_post(
|
||||
body='Muestra cancelada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Cancelada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_open_rejection_wizard(self):
|
||||
"""Open the rejection wizard"""
|
||||
|
|
|
@ -1,274 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Template principal del reporte -->
|
||||
<template id="report_lab_results">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-if="o.is_lab_request">
|
||||
<t t-call="lims_management.report_lab_results_document" t-lang="o.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Documento individual -->
|
||||
<template id="report_lab_results_document">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<!-- Estilos CSS -->
|
||||
<style>
|
||||
.lab-header {
|
||||
border-bottom: 2px solid #337ab7;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.patient-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.results-table {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.results-table th {
|
||||
background-color: #e9ecef;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.results-table td {
|
||||
padding: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.result-out-of-range {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.result-critical {
|
||||
background-color: #f2dede;
|
||||
color: #a94442;
|
||||
font-weight: bold;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.result-normal {
|
||||
color: #5cb85c;
|
||||
}
|
||||
.test-header {
|
||||
background-color: #337ab7;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.observations {
|
||||
background-color: #fcf8e3;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
border-left: 4px solid #faebcc;
|
||||
}
|
||||
.validation-info {
|
||||
margin-top: 40px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.signature-line {
|
||||
border-bottom: 1px solid #000;
|
||||
width: 250px;
|
||||
margin-top: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Encabezado del laboratorio -->
|
||||
<div class="lab-header">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<h2>LABORATORIO CLÍNICO</h2>
|
||||
<h3><t t-esc="o.company_id.name"/></h3>
|
||||
<p>
|
||||
<t t-if="o.company_id.street"><t t-esc="o.company_id.street"/><br/></t>
|
||||
<t t-if="o.company_id.city"><t t-esc="o.company_id.city"/>, </t>
|
||||
<t t-if="o.company_id.state_id"><t t-esc="o.company_id.state_id.name"/><br/></t>
|
||||
<t t-if="o.company_id.phone">Tel: <t t-esc="o.company_id.phone"/></t>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-4 text-right">
|
||||
<img t-if="o.company_id.logo" t-att-src="image_data_uri(o.company_id.logo)"
|
||||
style="max-height: 100px; max-width: 200px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información del paciente y orden -->
|
||||
<div class="patient-info">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h4>DATOS DEL PACIENTE</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Nombre:</strong></td>
|
||||
<td><t t-esc="o.partner_id.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Identificación:</strong></td>
|
||||
<td><t t-esc="o.partner_id.vat or 'N/A'"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Edad:</strong></td>
|
||||
<td>
|
||||
<t t-if="o.partner_id.birthdate_date">
|
||||
<t t-esc="o.partner_id.age"/> años
|
||||
</t>
|
||||
<t t-else="">N/A</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sexo:</strong></td>
|
||||
<td>
|
||||
<t t-if="o.partner_id.gender == 'male'">Masculino</t>
|
||||
<t t-elif="o.partner_id.gender == 'female'">Femenino</t>
|
||||
<t t-else="">No especificado</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h4>DATOS DE LA ORDEN</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Número de Orden:</strong></td>
|
||||
<td><t t-esc="o.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Fecha de Solicitud:</strong></td>
|
||||
<td><t t-esc="o.date_order" t-options='{"widget": "date"}'/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Médico Solicitante:</strong></td>
|
||||
<td><t t-esc="o.referring_doctor_id.name or 'N/A'"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Estado:</strong></td>
|
||||
<td>Resultados Validados</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resultados de análisis -->
|
||||
<h3 class="text-center" style="margin: 30px 0;">INFORME DE RESULTADOS</h3>
|
||||
|
||||
<!-- Iterar por cada prueba validada -->
|
||||
<t t-set="validated_tests" t-value="o.lab_test_ids.filtered(lambda t: t.state == 'validated')"/>
|
||||
<t t-foreach="validated_tests" t-as="test">
|
||||
<div class="test-section">
|
||||
<!-- Encabezado del análisis -->
|
||||
<h4 class="test-header">
|
||||
<t t-esc="test.product_id.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Tabla de resultados -->
|
||||
<table class="table results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">PARÁMETRO</th>
|
||||
<th width="20%" class="text-center">RESULTADO</th>
|
||||
<th width="15%" class="text-center">UNIDAD</th>
|
||||
<th width="35%" class="text-center">VALOR DE REFERENCIA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="test.result_ids" t-as="result">
|
||||
<tr>
|
||||
<td><t t-esc="result.parameter_id.name"/></td>
|
||||
<td class="text-center">
|
||||
<span t-attf-class="#{result.is_critical and 'result-critical' or result.is_out_of_range and 'result-out-of-range' or 'result-normal'}">
|
||||
<t t-esc="result.value_display"/>
|
||||
<t t-if="result.is_critical"> **</t>
|
||||
<t t-elif="result.is_out_of_range"> *</t>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="result.parameter_id.unit or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="result.applicable_range_id">
|
||||
<t t-if="result.parameter_id.value_type == 'numeric'">
|
||||
<t t-esc="result.applicable_range_id.normal_min"/> - <t t-esc="result.applicable_range_id.normal_max"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="result.applicable_range_id.reference_text or 'N/A'"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">N/A</t>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Mostrar notas si existen -->
|
||||
<t t-if="result.notes">
|
||||
<tr>
|
||||
<td colspan="4" style="padding-left: 30px; font-style: italic;">
|
||||
<strong>Nota:</strong> <t t-esc="result.notes"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Comentarios de la prueba -->
|
||||
<t t-if="test.notes">
|
||||
<div class="observations">
|
||||
<strong>Observaciones:</strong> <t t-esc="test.notes"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Leyenda de símbolos -->
|
||||
<div style="margin-top: 30px; font-size: 12px;">
|
||||
<p><strong>*</strong> Valor fuera del rango normal</p>
|
||||
<p><strong>**</strong> Valor crítico que requiere atención inmediata</p>
|
||||
</div>
|
||||
|
||||
<!-- Comentarios generales de la orden -->
|
||||
<t t-if="o.lab_notes">
|
||||
<div class="observations" style="margin-top: 30px;">
|
||||
<h5>OBSERVACIONES GENERALES</h5>
|
||||
<p><t t-esc="o.lab_notes"/></p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Información de validación -->
|
||||
<div class="validation-info">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<p><strong>Fecha de Validación:</strong>
|
||||
<t t-if="validated_tests">
|
||||
<t t-esc="validated_tests[0].validation_date" t-options='{"widget": "datetime"}'/>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6 text-center">
|
||||
<t t-if="validated_tests and validated_tests[0].validator_id">
|
||||
<div class="signature-line"></div>
|
||||
<p style="margin-top: 5px;">
|
||||
<strong><t t-esc="validated_tests[0].validator_id.name"/></strong><br/>
|
||||
Responsable del Laboratorio
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nota al pie -->
|
||||
<div style="margin-top: 50px; font-size: 10px; text-align: center; color: #666;">
|
||||
<p>Este informe es confidencial y está dirigido exclusivamente al paciente y/o médico tratante.</p>
|
||||
<p>Los resultados se relacionan únicamente con las muestras analizadas.</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Paper Format para el reporte de resultados -->
|
||||
<record id="paperformat_lab_results" model="report.paperformat">
|
||||
<field name="name">Formato Resultados de Laboratorio</field>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">40</field>
|
||||
<field name="margin_bottom">25</field>
|
||||
<field name="margin_left">10</field>
|
||||
<field name="margin_right">10</field>
|
||||
<field name="header_spacing">35</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- Acción del reporte -->
|
||||
<record id="action_report_lab_results" model="ir.actions.report">
|
||||
<field name="name">Informe de Resultados</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">lims_management.report_lab_results</field>
|
||||
<field name="report_file">lims_management.report_lab_results</field>
|
||||
<field name="print_report_name">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="paperformat_id" ref="paperformat_lab_results"/>
|
||||
<field name="attachment">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="attachment_use">True</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -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>
|
|
@ -64,7 +64,7 @@
|
|||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Resultados" name="results">
|
||||
<page string="Resultados">
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
context="{'default_test_id': id, 'default_patient_id': patient_id, 'default_test_date': create_date}"
|
||||
|
@ -90,7 +90,7 @@
|
|||
class="oe_edit_only"/>
|
||||
<field name="value_selection"
|
||||
invisible="parameter_value_type != 'selection'"
|
||||
placeholder="Ingrese valor o iniciales"
|
||||
widget="selection"
|
||||
class="oe_edit_only"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"
|
||||
|
@ -118,19 +118,16 @@
|
|||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Observaciones" name="observations">
|
||||
<group>
|
||||
<field name="notes" nolabel="1" placeholder="Agregar observaciones generales de la prueba..."/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Actividades" name="activities">
|
||||
<field name="activity_ids"/>
|
||||
</page>
|
||||
<page string="Historial" name="history">
|
||||
<field name="message_ids" options="{'no_create': True}"/>
|
||||
<page string="Observaciones">
|
||||
<field name="notes" placeholder="Agregar observaciones generales de la prueba..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -16,12 +16,6 @@
|
|||
class="btn-primary"
|
||||
invisible="not is_lab_request or state != 'sale' or not all_sample_ids"
|
||||
icon="fa-print"/>
|
||||
<button name="action_print_lab_results"
|
||||
string="Imprimir Informe de Resultados"
|
||||
type="object"
|
||||
class="btn-success"
|
||||
invisible="not can_print_results or not is_lab_request"
|
||||
icon="fa-file-pdf-o"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="doctor_id" invisible="not is_lab_request"/>
|
||||
|
@ -75,11 +69,6 @@
|
|||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Observaciones Lab" name="lab_notes" invisible="not is_lab_request">
|
||||
<group>
|
||||
<field name="lab_notes" nolabel="1" placeholder="Ingrese observaciones generales sobre la orden o los resultados..."/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
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:")
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para agregar resultados a las pruebas ya validadas
|
||||
"""
|
||||
|
||||
import odoo
|
||||
import logging
|
||||
import random
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_results_to_validated_tests(env):
|
||||
"""Agregar resultados a pruebas ya validadas"""
|
||||
|
||||
# Buscar las órdenes S00029 y S00030
|
||||
orders = env['sale.order'].search([('name', 'in', ['S00029', 'S00030'])], order='name')
|
||||
|
||||
if not orders:
|
||||
print("No se encontraron las órdenes S00029 o S00030")
|
||||
return
|
||||
|
||||
for order in orders:
|
||||
print(f"\n=== Procesando orden {order.name} ===")
|
||||
|
||||
for test in order.lab_test_ids:
|
||||
print(f"\nPrueba: {test.product_id.name}")
|
||||
|
||||
# Cambiar temporalmente a estado draft para poder modificar
|
||||
test.sudo().write({'state': 'draft'})
|
||||
|
||||
# Generar resultados si no existen
|
||||
if not test.result_ids:
|
||||
test.sudo()._generate_test_results()
|
||||
print(f" Generados {len(test.result_ids)} resultados")
|
||||
|
||||
# Asignar valores a los resultados
|
||||
for result in test.result_ids:
|
||||
parameter = result.parameter_id
|
||||
vals = {}
|
||||
|
||||
if parameter.value_type == 'numeric':
|
||||
# Valores específicos por código
|
||||
if parameter.code == 'HGB': # Hemoglobina
|
||||
vals['value_numeric'] = random.uniform(12.0, 16.0)
|
||||
elif parameter.code == 'HCT': # Hematocrito
|
||||
vals['value_numeric'] = random.uniform(36.0, 46.0)
|
||||
elif parameter.code == 'WBC': # Leucocitos
|
||||
vals['value_numeric'] = random.uniform(4.5, 10.0)
|
||||
elif parameter.code == 'PLT': # Plaquetas
|
||||
vals['value_numeric'] = random.uniform(150, 400)
|
||||
elif parameter.code == 'RBC': # Eritrocitos
|
||||
vals['value_numeric'] = random.uniform(4.0, 5.5)
|
||||
elif parameter.code == 'GLU': # Glucosa
|
||||
vals['value_numeric'] = random.uniform(70, 110)
|
||||
elif parameter.code == 'CHOL': # Colesterol
|
||||
vals['value_numeric'] = random.uniform(160, 220)
|
||||
elif parameter.code == 'TRIG': # Triglicéridos
|
||||
vals['value_numeric'] = random.uniform(50, 150)
|
||||
elif parameter.code == 'HDL': # HDL
|
||||
vals['value_numeric'] = random.uniform(40, 60)
|
||||
elif parameter.code == 'LDL': # LDL
|
||||
vals['value_numeric'] = random.uniform(80, 130)
|
||||
else:
|
||||
# Valor genérico
|
||||
vals['value_numeric'] = random.uniform(10, 100)
|
||||
|
||||
print(f" - {parameter.name}: {vals['value_numeric']:.2f}")
|
||||
|
||||
elif parameter.value_type == 'text':
|
||||
vals['value_text'] = "Normal"
|
||||
elif parameter.value_type == 'selection':
|
||||
vals['value_selection'] = "normal"
|
||||
elif parameter.value_type == 'boolean':
|
||||
vals['value_boolean'] = False
|
||||
|
||||
# Escribir con sudo para evitar restricciones
|
||||
result.sudo().write(vals)
|
||||
|
||||
# Volver a estado validated
|
||||
test.sudo().write({
|
||||
'state': 'validated',
|
||||
'validator_id': env.ref('base.user_admin').id,
|
||||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Agregar notas a algunas pruebas
|
||||
if 'Hemograma' in test.product_id.name:
|
||||
test.sudo().write({'notes': 'Todos los parámetros dentro de rangos normales.'})
|
||||
elif 'Lipídico' in test.product_id.name:
|
||||
test.sudo().write({'notes': 'Perfil lipídico normal. Se recomienda mantener dieta balanceada.'})
|
||||
|
||||
print("\n✅ Resultados agregados exitosamente a todas las pruebas")
|
||||
return orders
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Importar fields después de configurar Odoo
|
||||
from odoo import fields
|
||||
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Agregar resultados a las pruebas
|
||||
orders = add_results_to_validated_tests(env)
|
||||
|
||||
# Confirmar cambios
|
||||
cr.commit()
|
||||
|
||||
print("\n📋 Ahora puedes probar el botón 'Imprimir Informe de Resultados' en las órdenes S00029 y S00030.")
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
_logger.error(f"Error agregando resultados: {str(e)}", exc_info=True)
|
|
@ -1,67 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para verificar los datos de la orden S00025
|
||||
"""
|
||||
|
||||
import odoo
|
||||
|
||||
def check_order_s00025(env):
|
||||
"""Verificar datos específicos de la orden S00025"""
|
||||
|
||||
# Buscar la orden S00025
|
||||
order = env['sale.order'].search([
|
||||
('name', '=', 'S00025')
|
||||
], limit=1)
|
||||
|
||||
if not order:
|
||||
print("❌ No se encontró la orden S00025")
|
||||
return
|
||||
|
||||
patient = order.partner_id
|
||||
print(f"Orden: {order.name}")
|
||||
print(f" Es orden de laboratorio: {order.is_lab_request}")
|
||||
print(f" Paciente: {patient.name}")
|
||||
print(f" ID Paciente: {patient.id}")
|
||||
print(f" Fecha de nacimiento: {patient.birthdate_date}")
|
||||
print(f" Edad: {patient.age if patient.birthdate_date else 'N/A'}")
|
||||
print(f" Género: {patient.gender or 'No especificado'}")
|
||||
|
||||
# Verificar estado de las pruebas
|
||||
print(f"\nPruebas de laboratorio ({len(order.lab_test_ids)}):")
|
||||
for test in order.lab_test_ids:
|
||||
print(f" - {test.product_id.name}: {test.state}")
|
||||
|
||||
# Verificar si puede imprimir resultados
|
||||
print(f"\n¿Puede imprimir resultados?: {order.can_print_results}")
|
||||
|
||||
# Si el paciente no tiene fecha de nacimiento, actualizarla
|
||||
if not patient.birthdate_date:
|
||||
print("\n⚠️ El paciente no tiene fecha de nacimiento. Actualizando...")
|
||||
patient.write({'birthdate_date': '1985-01-15'})
|
||||
print(f" Fecha de nacimiento actualizada a: {patient.birthdate_date}")
|
||||
print(f" Edad calculada: {patient.age} años")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Verificar datos
|
||||
check_order_s00025(env)
|
||||
cr.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para verificar los datos de pacientes en las órdenes de laboratorio
|
||||
"""
|
||||
|
||||
import odoo
|
||||
|
||||
def check_patient_data(env):
|
||||
"""Verificar datos de pacientes en órdenes recientes"""
|
||||
|
||||
# Buscar órdenes de laboratorio recientes
|
||||
orders = env['sale.order'].search([
|
||||
('is_lab_request', '=', True),
|
||||
('name', 'in', ['S00032', 'S00033'])
|
||||
], order='id desc', limit=5)
|
||||
|
||||
if not orders:
|
||||
orders = env['sale.order'].search([
|
||||
('is_lab_request', '=', True)
|
||||
], order='id desc', limit=5)
|
||||
|
||||
print(f"Verificando {len(orders)} órdenes de laboratorio...")
|
||||
|
||||
for order in orders:
|
||||
patient = order.partner_id
|
||||
print(f"\nOrden: {order.name}")
|
||||
print(f" Paciente: {patient.name}")
|
||||
print(f" ID Paciente: {patient.id}")
|
||||
print(f" Fecha de nacimiento: {patient.birthdate_date}")
|
||||
print(f" Edad: {patient.age if patient.birthdate_date else 'N/A'}")
|
||||
print(f" Género: {patient.gender or 'No especificado'}")
|
||||
print(f" ¿Tiene campo age?: {hasattr(patient, 'age')}")
|
||||
print(f" ¿Tiene campo birthdate_date?: {hasattr(patient, 'birthdate_date')}")
|
||||
|
||||
# Verificar si podemos acceder a los campos
|
||||
try:
|
||||
age_value = patient.age
|
||||
print(f" Valor de age: {age_value}")
|
||||
except Exception as e:
|
||||
print(f" Error al acceder a age: {str(e)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Verificar datos
|
||||
check_patient_data(env)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {str(e)}")
|
|
@ -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,226 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para crear órdenes de laboratorio con resultados validados para probar el reporte PDF
|
||||
"""
|
||||
|
||||
import odoo
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_validated_lab_order(env):
|
||||
"""Crear una orden de laboratorio con resultados completos y validados"""
|
||||
|
||||
# Obtener o crear paciente y doctor demo
|
||||
patient = env['res.partner'].search([('is_patient', '=', True)], limit=1)
|
||||
if not patient:
|
||||
patient = env['res.partner'].create({
|
||||
'name': 'Juan Pérez (Demo)',
|
||||
'is_patient': True,
|
||||
'birthdate_date': '1980-05-15',
|
||||
'gender': 'male',
|
||||
'vat': '12345678',
|
||||
})
|
||||
|
||||
doctor = env['res.partner'].search([('is_doctor', '=', True)], limit=1)
|
||||
if not doctor:
|
||||
doctor = env['res.partner'].create({
|
||||
'name': 'Dr. María García (Demo)',
|
||||
'is_doctor': True,
|
||||
})
|
||||
|
||||
# Usar usuario admin como técnico y validador
|
||||
admin_user = env.ref('base.user_admin')
|
||||
technician = admin_user
|
||||
validator = admin_user
|
||||
|
||||
# Obtener análisis disponibles
|
||||
hemograma = env.ref('lims_management.analysis_hemograma')
|
||||
glucosa = env.ref('lims_management.analysis_glucosa')
|
||||
perfil_lipidico = env.ref('lims_management.analysis_perfil_lipidico')
|
||||
|
||||
# Crear orden de laboratorio
|
||||
order = env['sale.order'].create({
|
||||
'partner_id': patient.id,
|
||||
'doctor_id': doctor.id,
|
||||
'is_lab_request': True,
|
||||
'lab_notes': 'Paciente en ayunas de 12 horas. Control de rutina anual.',
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'product_id': hemograma.product_variant_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': hemograma.list_price,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': glucosa.product_variant_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': glucosa.list_price,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': perfil_lipidico.product_variant_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': perfil_lipidico.list_price,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
# Confirmar orden (genera muestras y pruebas automáticamente)
|
||||
order.action_confirm()
|
||||
|
||||
_logger.info(f"Orden creada: {order.name}")
|
||||
|
||||
# Primero, marcar todas las muestras como recolectadas
|
||||
for sample in order.generated_sample_ids:
|
||||
sample.write({'state': 'collected'})
|
||||
|
||||
# Procesar cada prueba
|
||||
for test in order.lab_test_ids:
|
||||
# Asignar técnico
|
||||
test.write({
|
||||
'technician_id': technician.id
|
||||
})
|
||||
|
||||
# Generar resultados si no existen
|
||||
if not test.result_ids:
|
||||
# Usar sudo para evitar restricciones de permisos y llamar método interno
|
||||
test.sudo()._generate_test_results()
|
||||
_logger.info(f"Generados {len(test.result_ids)} resultados para prueba {test.name}")
|
||||
|
||||
# Cambiar estado a in_process
|
||||
test.write({'state': 'in_process'})
|
||||
|
||||
# Ingresar resultados según el tipo de análisis
|
||||
for result in test.result_ids:
|
||||
parameter = result.parameter_id
|
||||
vals = {}
|
||||
|
||||
# Solo procesar parámetros numéricos
|
||||
if parameter.value_type == 'numeric':
|
||||
# Generar valores basados en el parámetro
|
||||
if parameter.code == 'HGB': # Hemoglobina
|
||||
vals['value_numeric'] = random.uniform(11.0, 16.5) # Algunos fuera de rango
|
||||
elif parameter.code == 'HCT': # Hematocrito
|
||||
vals['value_numeric'] = random.uniform(35.0, 48.0)
|
||||
elif parameter.code == 'WBC': # Leucocitos
|
||||
vals['value_numeric'] = random.uniform(3.5, 11.0) # Algunos fuera de rango
|
||||
elif parameter.code == 'PLT': # Plaquetas
|
||||
vals['value_numeric'] = random.uniform(140, 450)
|
||||
elif parameter.code == 'RBC': # Eritrocitos
|
||||
vals['value_numeric'] = random.uniform(3.8, 5.8)
|
||||
elif parameter.code == 'GLU': # Glucosa
|
||||
vals['value_numeric'] = random.uniform(65, 125) # Algunos elevados
|
||||
elif parameter.code == 'CHOL': # Colesterol
|
||||
vals['value_numeric'] = random.uniform(150, 240) # Algunos elevados
|
||||
elif parameter.code == 'TRIG': # Triglicéridos
|
||||
vals['value_numeric'] = random.uniform(40, 200) # Algunos elevados
|
||||
elif parameter.code == 'HDL': # HDL
|
||||
vals['value_numeric'] = random.uniform(35, 65)
|
||||
elif parameter.code == 'LDL': # LDL
|
||||
vals['value_numeric'] = random.uniform(70, 160)
|
||||
else:
|
||||
# Valor genérico para otros parámetros numéricos
|
||||
if result.applicable_range_id:
|
||||
# Generar valor cercano al rango normal
|
||||
min_val = result.applicable_range_id.normal_min or 0
|
||||
max_val = result.applicable_range_id.normal_max or 100
|
||||
vals['value_numeric'] = random.uniform(min_val * 0.8, max_val * 1.2)
|
||||
else:
|
||||
# Valor por defecto si no hay rango
|
||||
vals['value_numeric'] = random.uniform(1, 100)
|
||||
|
||||
_logger.info(f"Asignando valor numérico {vals.get('value_numeric', 0):.2f} a {parameter.name} ({parameter.code})")
|
||||
elif parameter.value_type == 'text':
|
||||
vals['value_text'] = "Normal"
|
||||
elif parameter.value_type == 'selection':
|
||||
vals['value_selection'] = "normal"
|
||||
elif parameter.value_type == 'boolean':
|
||||
vals['value_boolean'] = False
|
||||
|
||||
# Escribir valores
|
||||
if vals:
|
||||
result.write(vals)
|
||||
|
||||
# Agregar notas si está fuera de rango
|
||||
if result.is_out_of_range:
|
||||
if result.is_critical:
|
||||
result.notes = "Valor crítico. Se recomienda repetir el análisis y consultar con el médico de inmediato."
|
||||
else:
|
||||
result.notes = "Valor ligeramente alterado. Se sugiere control en 3 meses."
|
||||
|
||||
# Marcar resultados como ingresados
|
||||
test.write({'state': 'result_entered'})
|
||||
|
||||
# Agregar comentarios a algunas pruebas
|
||||
if test.product_id == hemograma:
|
||||
test.notes = "Serie roja dentro de parámetros normales. Serie blanca con ligera leucocitosis."
|
||||
elif test.product_id == perfil_lipidico:
|
||||
test.notes = "Se recomienda dieta baja en grasas y control en 3 meses."
|
||||
|
||||
# Validar todas las pruebas
|
||||
for test in order.lab_test_ids:
|
||||
test.write({
|
||||
'state': 'validated',
|
||||
'validator_id': validator.id,
|
||||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
_logger.info(f"Todas las pruebas validadas para orden {order.name}")
|
||||
|
||||
return order
|
||||
|
||||
|
||||
def create_multiple_test_orders(env, count=3):
|
||||
"""Crear múltiples órdenes con diferentes escenarios"""
|
||||
orders = env['sale.order']
|
||||
|
||||
for i in range(count):
|
||||
_logger.info(f"Creando orden {i+1} de {count}")
|
||||
order = create_validated_lab_order(env)
|
||||
orders |= order
|
||||
|
||||
# Variar las observaciones
|
||||
if i == 1:
|
||||
order.lab_notes = "Paciente diabético tipo 2. Control mensual de glucemia."
|
||||
elif i == 2:
|
||||
order.lab_notes = "Control post-operatorio. Paciente con antecedentes de anemia."
|
||||
|
||||
return orders
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Crear órdenes con resultados validados
|
||||
orders = create_multiple_test_orders(env, count=2)
|
||||
|
||||
# Confirmar cambios
|
||||
cr.commit()
|
||||
|
||||
print(f"\n✅ Se crearon {len(orders)} órdenes de laboratorio con resultados validados:")
|
||||
for order in orders:
|
||||
print(f" - {order.name}: {order.partner_id.name}")
|
||||
print(f" Pruebas: {', '.join(test.product_id.name for test in order.lab_test_ids)}")
|
||||
|
||||
print("\n📋 Ahora puedes probar el botón 'Imprimir Informe de Resultados' en estas órdenes.")
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
_logger.error(f"Error creando datos demo: {str(e)}", exc_info=True)
|
|
@ -1,68 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para debuggear el autocompletado de selection
|
||||
"""
|
||||
|
||||
import odoo
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def debug_selection_autocomplete(env):
|
||||
"""Debug del autocompletado"""
|
||||
|
||||
print("=" * 80)
|
||||
print("DEBUG DE AUTOCOMPLETADO DE SELECTION")
|
||||
print("=" * 80)
|
||||
|
||||
# Buscar un resultado con tipo selection
|
||||
result = env['lims.result'].search([
|
||||
('parameter_value_type', '=', 'selection'),
|
||||
('test_id.state', '=', 'in_process')
|
||||
], limit=1)
|
||||
|
||||
if result:
|
||||
print(f"\nResultado encontrado:")
|
||||
print(f" - ID: {result.id}")
|
||||
print(f" - Parámetro: {result.parameter_id.name}")
|
||||
print(f" - Valor actual: '{result.value_selection}'")
|
||||
print(f" - Valores posibles: {result.parameter_id.selection_values}")
|
||||
|
||||
# Probar el autocompletado
|
||||
test_values = ['Negative', 'negative', 'NEG', 'neg', 'N', 'n', 'Positivo', 'P']
|
||||
|
||||
print("\nProbando autocompletado:")
|
||||
for test_val in test_values:
|
||||
autocompleted = result._validate_and_autocomplete_selection(test_val)
|
||||
print(f" '{test_val}' -> '{autocompleted}'")
|
||||
else:
|
||||
print("No se encontraron resultados de tipo selection")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Debug
|
||||
debug_selection_autocomplete(env)
|
||||
|
||||
# No guardar cambios
|
||||
cr.rollback()
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"\n❌ Error: {str(e)}")
|
||||
_logger.error(f"Error: {str(e)}", exc_info=True)
|
|
@ -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,65 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para actualizar la fecha de nacimiento de los pacientes existentes
|
||||
"""
|
||||
|
||||
import odoo
|
||||
from datetime import date, timedelta
|
||||
import random
|
||||
|
||||
def update_patient_birthdates(env):
|
||||
"""Actualizar fechas de nacimiento de pacientes existentes"""
|
||||
|
||||
# Buscar pacientes sin fecha de nacimiento
|
||||
patients = env['res.partner'].search([
|
||||
('is_patient', '=', True),
|
||||
('birthdate_date', '=', False)
|
||||
])
|
||||
|
||||
if patients:
|
||||
print(f"Actualizando {len(patients)} pacientes sin fecha de nacimiento...")
|
||||
|
||||
for patient in patients:
|
||||
# Generar una edad aleatoria entre 20 y 70 años
|
||||
age_years = random.randint(20, 70)
|
||||
birthdate = date.today() - timedelta(days=age_years * 365 + random.randint(0, 364))
|
||||
|
||||
# Actualizar fecha de nacimiento
|
||||
patient.write({
|
||||
'birthdate_date': birthdate.strftime('%Y-%m-%d')
|
||||
})
|
||||
|
||||
print(f" - {patient.name}: {birthdate.strftime('%Y-%m-%d')} ({age_years} años)")
|
||||
else:
|
||||
print("Todos los pacientes ya tienen fecha de nacimiento.")
|
||||
|
||||
return patients
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configuración
|
||||
db_name = 'lims_demo'
|
||||
|
||||
# Conectar a Odoo
|
||||
odoo.tools.config.parse_config(['--database', db_name])
|
||||
|
||||
# Obtener el registro de la base de datos
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
# Crear cursor y environment
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
try:
|
||||
# Actualizar pacientes
|
||||
patients = update_patient_birthdates(env)
|
||||
|
||||
# Confirmar cambios
|
||||
cr.commit()
|
||||
|
||||
if patients:
|
||||
print(f"\n✅ Se actualizaron {len(patients)} pacientes con fecha de nacimiento.")
|
||||
|
||||
except Exception as e:
|
||||
cr.rollback()
|
||||
print(f"\n❌ Error: {str(e)}")
|
|
@ -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