Compare commits
1 Commits
dev
...
feature/10
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c4cfc0b804 |
|
@ -24,12 +24,7 @@
|
|||
"Bash(true)",
|
||||
"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(gh pr merge:*)"
|
||||
],
|
||||
"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: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
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
|
34
init_odoo.py
34
init_odoo.py
|
@ -36,6 +36,7 @@ odoo_command = [
|
|||
"-d", DB_NAME,
|
||||
"-i", MODULES_TO_INSTALL,
|
||||
"--load-language", "es_ES",
|
||||
"--without-demo=", # Forzar carga de datos demo
|
||||
"--stop-after-init"
|
||||
]
|
||||
|
||||
|
@ -189,39 +190,6 @@ EOF
|
|||
else:
|
||||
print(f"Advertencia: Fallo al actualizar logo de empresa (código {result.returncode})")
|
||||
|
||||
# --- Asignar admin al grupo de Administrador de Laboratorio ---
|
||||
print("\nAsignando usuario admin al grupo de Administrador de Laboratorio...")
|
||||
sys.stdout.flush()
|
||||
|
||||
if os.path.exists("/app/scripts/assign_admin_to_lab_group.py"):
|
||||
with open("/app/scripts/assign_admin_to_lab_group.py", "r") as f:
|
||||
admin_group_script = f.read()
|
||||
|
||||
assign_admin_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{admin_group_script}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
assign_admin_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Assign Admin to Lab Group stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Assign Admin to Lab Group stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Usuario admin asignado exitosamente al grupo de Administrador de Laboratorio.")
|
||||
else:
|
||||
print(f"Advertencia: Fallo al asignar admin al grupo (código {result.returncode})")
|
||||
|
||||
# --- Validación final del logo ---
|
||||
print("\nValidando estado final del logo y nombre...")
|
||||
sys.stdout.flush()
|
||||
|
|
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.
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
|
@ -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',
|
||||
|
@ -29,12 +29,9 @@
|
|||
'data/product_category.xml',
|
||||
'data/sample_types.xml',
|
||||
'data/lims_sequence.xml',
|
||||
'data/rejection_reason_data.xml',
|
||||
'views/partner_views.xml',
|
||||
'views/analysis_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/rejection_reason_views.xml',
|
||||
'wizards/sample_rejection_wizard_views.xml',
|
||||
'views/stock_lot_views.xml',
|
||||
'views/lims_test_views.xml',
|
||||
'views/lims_result_views.xml',
|
||||
|
@ -45,15 +42,10 @@
|
|||
'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',
|
||||
'demo/z_lims_demo.xml',
|
||||
'demo/z_analysis_demo.xml',
|
||||
'demo/z_sample_demo.xml',
|
||||
|
|
Binary file not shown.
|
@ -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>
|
|
@ -1,95 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Rejection Reasons -->
|
||||
<record id="rejection_reason_insufficient" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Insuficiente</field>
|
||||
<field name="code">INSUF</field>
|
||||
<field name="description">El volumen de muestra recibido es insuficiente para realizar los análisis solicitados</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_hemolyzed" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Hemolizada</field>
|
||||
<field name="code">HEMO</field>
|
||||
<field name="description">La muestra presenta hemólisis que interfiere con los análisis</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_coagulated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Coagulada</field>
|
||||
<field name="code">COAG</field>
|
||||
<field name="description">La muestra presenta coágulos que impiden su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_lipemic" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Lipémica</field>
|
||||
<field name="code">LIP</field>
|
||||
<field name="description">La muestra presenta lipemia excesiva que interfiere con los análisis</field>
|
||||
<field name="severity">medium</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_container" model="lims.rejection.reason">
|
||||
<field name="name">Recipiente Inadecuado</field>
|
||||
<field name="code">RECIP</field>
|
||||
<field name="description">El tipo de recipiente utilizado no es apropiado para el análisis solicitado</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_id" model="lims.rejection.reason">
|
||||
<field name="name">Identificación Incorrecta</field>
|
||||
<field name="code">ID</field>
|
||||
<field name="description">La identificación de la muestra no coincide con la solicitud o es ilegible</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">60</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_no_label" model="lims.rejection.reason">
|
||||
<field name="name">Muestra sin Rotular</field>
|
||||
<field name="code">NOLAB</field>
|
||||
<field name="description">La muestra no tiene etiqueta de identificación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">70</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_transport" model="lims.rejection.reason">
|
||||
<field name="name">Condiciones de Transporte Inadecuadas</field>
|
||||
<field name="code">TRANS</field>
|
||||
<field name="description">La muestra no fue transportada en las condiciones requeridas (temperatura, tiempo, etc.)</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">80</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_contaminated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Contaminada</field>
|
||||
<field name="code">CONT</field>
|
||||
<field name="description">La muestra presenta signos evidentes de contaminación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">90</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_expired" model="lims.rejection.reason">
|
||||
<field name="name">Tiempo de Entrega Excedido</field>
|
||||
<field name="code">TIME</field>
|
||||
<field name="description">La muestra fue recibida fuera del tiempo límite establecido para su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">100</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,61 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Usuario Recepcionista -->
|
||||
<record id="demo_user_receptionist" model="res.users">
|
||||
<field name="name">Recepcionista Demo</field>
|
||||
<field name="login">recepcionista</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">recepcionista@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_receptionist'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Usuario Técnico -->
|
||||
<record id="demo_user_technician" model="res.users">
|
||||
<field name="name">Técnico Demo</field>
|
||||
<field name="login">tecnico</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">tecnico@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_technician'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Usuario Administrador de Laboratorio -->
|
||||
<record id="demo_user_lab_admin" model="res.users">
|
||||
<field name="name">Administrador Lab Demo</field>
|
||||
<field name="login">administrador</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">administrador@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_admin'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Partner (empleado) para cada usuario -->
|
||||
<record id="demo_user_receptionist_partner" model="res.partner">
|
||||
<field name="name">Recepcionista Demo</field>
|
||||
<field name="email">recepcionista@example.com</field>
|
||||
<field name="user_id" ref="demo_user_receptionist"/>
|
||||
<field name="is_company" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_user_technician_partner" model="res.partner">
|
||||
<field name="name">Técnico Demo</field>
|
||||
<field name="email">tecnico@example.com</field>
|
||||
<field name="user_id" ref="demo_user_technician"/>
|
||||
<field name="is_company" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_user_lab_admin_partner" model="res.partner">
|
||||
<field name="name">Administrador Lab Demo</field>
|
||||
<field name="email">administrador@example.com</field>
|
||||
<field name="user_id" ref="demo_user_lab_admin"/>
|
||||
<field name="is_company" 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>
|
|
@ -6,8 +6,6 @@ from . import product
|
|||
from . import partner
|
||||
from . import sale_order
|
||||
from . import stock_lot
|
||||
from . import rejection_reason
|
||||
from . import lims_test
|
||||
from . import lims_result
|
||||
from . import res_config_settings
|
||||
from . import lims_config
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class LimsConfig(models.TransientModel):
|
||||
_name = 'lims.config.settings'
|
||||
_inherit = 'res.config.settings'
|
||||
_description = 'Configuración del Laboratorio'
|
||||
|
||||
auto_resample_on_rejection = fields.Boolean(
|
||||
string='Re-muestreo Automático al Rechazar',
|
||||
help='Si está activo, se generará automáticamente una nueva muestra cuando se rechace una existente',
|
||||
config_parameter='lims_management.auto_resample_on_rejection',
|
||||
default=True
|
||||
)
|
||||
|
||||
resample_state = fields.Selection([
|
||||
('pending_collection', 'Pendiente de Recolección'),
|
||||
('collected', 'Recolectada'),
|
||||
], string='Estado Inicial para Re-muestras',
|
||||
help='Estado en el que se crearán las nuevas muestras generadas por re-muestreo',
|
||||
config_parameter='lims_management.resample_state',
|
||||
default='pending_collection'
|
||||
)
|
||||
|
||||
auto_notify_resample = fields.Boolean(
|
||||
string='Notificar Re-muestreo Automático',
|
||||
help='Enviar notificación al recepcionista cuando se genera una nueva muestra por re-muestreo',
|
||||
config_parameter='lims_management.auto_notify_resample',
|
||||
default=True
|
||||
)
|
||||
|
||||
resample_prefix = fields.Char(
|
||||
string='Prefijo para Re-muestras',
|
||||
help='Prefijo que se añadirá al código de las muestras generadas por re-muestreo (ej: RE-)',
|
||||
config_parameter='lims_management.resample_prefix',
|
||||
default='RE-'
|
||||
)
|
||||
|
||||
max_resample_attempts = fields.Integer(
|
||||
string='Máximo de Re-muestreos',
|
||||
help='Número máximo de veces que se puede re-muestrear una muestra (0 = sin límite)',
|
||||
config_parameter='lims_management.max_resample_attempts',
|
||||
default=3
|
||||
)
|
|
@ -25,22 +25,6 @@ class LimsResult(models.Model):
|
|||
ondelete='cascade'
|
||||
)
|
||||
|
||||
# Campo relacionado para acceder a la muestra sin duplicar datos
|
||||
test_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra',
|
||||
related='test_id.sample_id',
|
||||
readonly=True,
|
||||
store=True # Para poder buscar y filtrar
|
||||
)
|
||||
|
||||
# Campo relacionado para mostrar el estado sin duplicar
|
||||
test_sample_state = fields.Selection(
|
||||
string='Estado de Muestra',
|
||||
related='test_sample_id.state',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Cambio de parameter_name a parameter_id
|
||||
parameter_id = fields.Many2one(
|
||||
'lims.analysis.parameter',
|
||||
|
@ -93,15 +77,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 +234,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 +259,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 +266,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 +285,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
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
@ -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',
|
||||
|
@ -59,12 +51,6 @@ class LimsTest(models.Model):
|
|||
tracking=True
|
||||
)
|
||||
|
||||
sample_state = fields.Selection(
|
||||
related='sample_id.state',
|
||||
string='Estado de Muestra',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Borrador'),
|
||||
('in_process', 'En Proceso'),
|
||||
|
@ -116,21 +102,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 +140,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 +169,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
|
||||
|
@ -237,12 +190,6 @@ class LimsTest(models.Model):
|
|||
def action_start_process(self):
|
||||
"""Inicia el proceso de análisis."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para iniciar el proceso de análisis. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'draft':
|
||||
raise UserError(_('Solo se pueden procesar pruebas en estado borrador.'))
|
||||
if not self.sample_id:
|
||||
|
@ -256,44 +203,20 @@ class LimsTest(models.Model):
|
|||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Prueba iniciada por %s') % self.env.user.name,
|
||||
subject=_('Proceso Iniciado'),
|
||||
message_type='notification'
|
||||
subject=_('Proceso Iniciado')
|
||||
)
|
||||
|
||||
# Actualizar estado de la muestra si es necesario
|
||||
if self.sample_id and self.sample_id.state == 'collected':
|
||||
self.sample_id.write({'state': 'in_process'})
|
||||
self.sample_id.message_post(
|
||||
body=_('Muestra en análisis para la prueba %s') % self.name,
|
||||
subject=_('Estado actualizado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_enter_results(self):
|
||||
"""Marca como resultados ingresados."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para ingresar resultados. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'in_process':
|
||||
raise UserError(_('Solo se pueden ingresar resultados en pruebas en proceso.'))
|
||||
|
||||
if not self.result_ids:
|
||||
raise UserError(_('Debe ingresar al menos un resultado.'))
|
||||
|
||||
# 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'
|
||||
)
|
||||
if empty_results:
|
||||
params = ', '.join(empty_results.mapped('parameter_id.name'))
|
||||
raise UserError(_('Los siguientes parámetros no tienen resultados ingresados: %s') % params)
|
||||
|
||||
# Si no requiere validación, pasar directamente a validado
|
||||
if not self.require_validation:
|
||||
self.write({
|
||||
|
@ -303,15 +226,13 @@ class LimsTest(models.Model):
|
|||
})
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados y auto-validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados'),
|
||||
message_type='notification'
|
||||
subject=_('Resultados Validados')
|
||||
)
|
||||
else:
|
||||
self.state = 'result_entered'
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Ingresados'),
|
||||
message_type='notification'
|
||||
subject=_('Resultados Ingresados')
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -319,23 +240,10 @@ class LimsTest(models.Model):
|
|||
def action_validate(self):
|
||||
"""Valida los resultados (solo administradores)."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo administradores
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No tiene permisos para validar resultados. Solo administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'result_entered':
|
||||
raise UserError(_('Solo se pueden validar pruebas con resultados ingresados.'))
|
||||
|
||||
# Verificar que todos los resultados críticos tengan observaciones si están fuera de rango
|
||||
critical_results = []
|
||||
for result in self.result_ids:
|
||||
if result.is_critical: # Usar el campo is_critical del resultado, no del parámetro
|
||||
if not result.notes:
|
||||
critical_results.append(result.parameter_id.name)
|
||||
|
||||
if critical_results:
|
||||
raise UserError(_('Los siguientes parámetros críticos están fuera de rango y requieren observaciones: %s') % ', '.join(critical_results))
|
||||
# TODO: Verificar permisos cuando se implemente seguridad
|
||||
|
||||
self.write({
|
||||
'state': 'validated',
|
||||
|
@ -343,56 +251,26 @@ class LimsTest(models.Model):
|
|||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Log en el chatter con más detalles
|
||||
out_of_range_count = len(self.result_ids.filtered('is_out_of_range'))
|
||||
body = _('Resultados validados por %s') % self.env.user.name
|
||||
if out_of_range_count:
|
||||
body += _('<br/>%d parámetros fuera de rango') % out_of_range_count
|
||||
|
||||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=body,
|
||||
subject=_('Resultados Validados'),
|
||||
message_type='notification'
|
||||
body=_('Resultados validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados')
|
||||
)
|
||||
|
||||
# Actualizar estado de la muestra si todas las pruebas están validadas
|
||||
if self.sample_id:
|
||||
all_tests = self.env['lims.test'].search([
|
||||
('sample_id', '=', self.sample_id.id),
|
||||
('state', '!=', 'cancelled')
|
||||
])
|
||||
if all(test.state == 'validated' for test in all_tests):
|
||||
self.sample_id.write({'state': 'analyzed'})
|
||||
self.sample_id.message_post(
|
||||
body=_('Todas las pruebas de la muestra han sido validadas'),
|
||||
subject=_('Análisis completado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancela la prueba."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: técnicos y administradores pueden cancelar
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para cancelar pruebas. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state == 'validated':
|
||||
# Solo administradores pueden cancelar pruebas validadas
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No se pueden cancelar pruebas validadas. Solo administradores pueden realizar esta acción.'))
|
||||
raise UserError(_('No se pueden cancelar pruebas validadas.'))
|
||||
|
||||
old_state = self.state
|
||||
self.state = 'cancelled'
|
||||
|
||||
# Log en el chatter con el estado anterior
|
||||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Prueba cancelada por %s (estado anterior: %s)') % (self.env.user.name, dict(self._fields['state'].selection).get(old_state)),
|
||||
subject=_('Prueba Cancelada'),
|
||||
message_type='notification'
|
||||
body=_('Prueba cancelada por %s') % self.env.user.name,
|
||||
subject=_('Prueba Cancelada')
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -400,12 +278,6 @@ class LimsTest(models.Model):
|
|||
def action_regenerate_results(self):
|
||||
"""Regenera los resultados basados en la configuración actual del análisis."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para regenerar resultados. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state not in ['draft', 'in_process']:
|
||||
raise UserError(_('Solo se pueden regenerar resultados en pruebas en borrador o en proceso.'))
|
||||
|
||||
|
@ -420,8 +292,7 @@ class LimsTest(models.Model):
|
|||
|
||||
self.message_post(
|
||||
body=_('Resultados regenerados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Regenerados'),
|
||||
message_type='notification'
|
||||
subject=_('Resultados Regenerados')
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -429,105 +300,9 @@ class LimsTest(models.Model):
|
|||
def action_draft(self):
|
||||
"""Regresa a borrador."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo administradores pueden regresar a borrador
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No tiene permisos para regresar pruebas a borrador. Solo administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state not in ['cancelled']:
|
||||
raise UserError(_('Solo se pueden regresar a borrador pruebas canceladas.'))
|
||||
|
||||
self.state = 'draft'
|
||||
|
||||
self.message_post(
|
||||
body=_('Prueba regresada a borrador por %s') % self.env.user.name,
|
||||
subject=_('Estado Restaurado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@api.constrains('state')
|
||||
def _check_state_transition(self):
|
||||
"""Valida que las transiciones de estado sean válidas"""
|
||||
for record in self:
|
||||
# Definir transiciones válidas
|
||||
valid_transitions = {
|
||||
'draft': ['in_process', 'cancelled'],
|
||||
'in_process': ['result_entered', 'cancelled'],
|
||||
'result_entered': ['validated', 'cancelled'],
|
||||
'validated': ['cancelled'], # Solo admin puede cancelar validados
|
||||
'cancelled': ['draft'] # Solo admin puede regresar a draft
|
||||
}
|
||||
|
||||
# Si es un registro nuevo, no hay transición que validar
|
||||
if not record._origin.id:
|
||||
continue
|
||||
|
||||
old_state = record._origin.state
|
||||
new_state = record.state
|
||||
|
||||
# Si el estado no cambió, no hay nada que validar
|
||||
if old_state == new_state:
|
||||
continue
|
||||
|
||||
# Verificar si la transición es válida
|
||||
if old_state in valid_transitions:
|
||||
if new_state not in valid_transitions[old_state]:
|
||||
raise ValidationError(
|
||||
_('Transición de estado no válida: No se puede cambiar de "%s" a "%s"') %
|
||||
(dict(self._fields['state'].selection).get(old_state),
|
||||
dict(self._fields['state'].selection).get(new_state))
|
||||
)
|
||||
|
||||
@api.constrains('sample_id', 'state')
|
||||
def _check_sample_state(self):
|
||||
"""Valida que la muestra esté en un estado apropiado para la prueba"""
|
||||
for record in self:
|
||||
if record.sample_id and record.state in ['in_process', 'result_entered']:
|
||||
# La muestra debe estar al menos recolectada
|
||||
if record.sample_id.state in ['pending_collection', 'cancelled']:
|
||||
raise ValidationError(
|
||||
_('No se puede procesar una prueba con una muestra en estado "%s"') %
|
||||
dict(record.sample_id._fields['state'].selection).get(record.sample_id.state)
|
||||
)
|
||||
|
||||
@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'
|
||||
|
||||
# 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
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para auditoría adicional"""
|
||||
# Si se está cambiando el estado, registrar más detalles
|
||||
if 'state' in vals:
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
# El write real se hace en el super()
|
||||
|
||||
result = super().write(vals)
|
||||
|
||||
# Registrar cambios importantes después del write
|
||||
if 'sample_id' in vals:
|
||||
for record in self:
|
||||
if vals.get('sample_id'):
|
||||
sample = self.env['stock.lot'].browse(vals['sample_id'])
|
||||
record.message_post(
|
||||
body=_('Muestra asignada: %s') % sample.name,
|
||||
subject=_('Muestra Asignada'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return result
|
||||
return True
|
|
@ -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"""
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class LimsRejectionReason(models.Model):
|
||||
_name = 'lims.rejection.reason'
|
||||
_description = 'Motivo de Rechazo de Muestra'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Motivo',
|
||||
required=True
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Código',
|
||||
required=True,
|
||||
help="Código único para identificar el motivo"
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Descripción',
|
||||
help="Descripción detallada del motivo de rechazo"
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Activo',
|
||||
default=True
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Secuencia',
|
||||
default=10,
|
||||
help="Orden de aparición en las listas"
|
||||
)
|
||||
requires_new_sample = fields.Boolean(
|
||||
string='Requiere Nueva Muestra',
|
||||
default=True,
|
||||
help="Indica si este tipo de rechazo requiere solicitar una nueva muestra"
|
||||
)
|
||||
severity = fields.Selection([
|
||||
('low', 'Baja'),
|
||||
('medium', 'Media'),
|
||||
('high', 'Alta'),
|
||||
('critical', 'Crítica')
|
||||
], string='Severidad', default='medium',
|
||||
help="Severidad del problema que causa el rechazo")
|
||||
|
||||
# Statistics
|
||||
rejection_count = fields.Integer(
|
||||
string='Cantidad de Rechazos',
|
||||
compute='_compute_rejection_count',
|
||||
help="Número de muestras rechazadas con este motivo"
|
||||
)
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_rejection_count(self):
|
||||
for record in self:
|
||||
record.rejection_count = self.env['stock.lot'].search_count([
|
||||
('rejection_reason_id', '=', record.id),
|
||||
('state', '=', 'rejected')
|
||||
])
|
||||
|
||||
_sql_constraints = [
|
||||
('code_uniq', 'unique (code)', 'El código del motivo de rechazo debe ser único!'),
|
||||
]
|
|
@ -33,31 +33,6 @@ class SaleOrder(models.Model):
|
|||
help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden"
|
||||
)
|
||||
|
||||
all_sample_ids = fields.Many2many(
|
||||
'stock.lot',
|
||||
string='Todas las Muestras (inc. Re-muestras)',
|
||||
compute='_compute_all_samples',
|
||||
help="Todas las muestras relacionadas con esta orden, incluyendo re-muestras"
|
||||
)
|
||||
|
||||
@api.depends('generated_sample_ids', 'generated_sample_ids.child_sample_ids')
|
||||
def _compute_all_samples(self):
|
||||
"""Compute all samples including resamples"""
|
||||
for order in self:
|
||||
all_samples = order.generated_sample_ids
|
||||
# Add all resamples recursively
|
||||
resamples = self.env['stock.lot']
|
||||
for sample in order.generated_sample_ids:
|
||||
resamples |= self._get_all_resamples(sample)
|
||||
order.all_sample_ids = all_samples | resamples
|
||||
|
||||
def _get_all_resamples(self, sample):
|
||||
"""Recursively get all resamples of a sample"""
|
||||
resamples = sample.child_sample_ids
|
||||
for resample in sample.child_sample_ids:
|
||||
resamples |= self._get_all_resamples(resample)
|
||||
return resamples
|
||||
|
||||
def action_confirm(self):
|
||||
"""Override to generate laboratory samples and tests automatically"""
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
|
@ -320,75 +295,17 @@ class SaleOrder(models.Model):
|
|||
return res
|
||||
|
||||
def action_print_sample_labels(self):
|
||||
"""Imprimir etiquetas de todas las muestras activas (incluyendo re-muestras)"""
|
||||
"""Imprimir etiquetas de todas las muestras generadas para esta orden"""
|
||||
self.ensure_one()
|
||||
|
||||
# Obtener todas las muestras activas (no rechazadas ni canceladas)
|
||||
active_samples = self.all_sample_ids.filtered(
|
||||
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||
)
|
||||
|
||||
if not active_samples:
|
||||
raise UserError(_('No hay muestras activas para imprimir. Todas las muestras están rechazadas, canceladas o desechadas.'))
|
||||
if not self.generated_sample_ids:
|
||||
raise UserError(_('No hay muestras generadas para esta orden. Por favor, confirme la orden primero.'))
|
||||
|
||||
# Asegurar que todas las muestras tengan código de barras
|
||||
active_samples._ensure_barcode()
|
||||
self.generated_sample_ids._ensure_barcode()
|
||||
|
||||
# Obtener el reporte
|
||||
report = self.env.ref('lims_management.action_report_sample_label')
|
||||
|
||||
# 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)
|
||||
# Retornar la acción de imprimir el reporte para todas las muestras
|
||||
return report.report_action(self.generated_sample_ids)
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import models, fields, api
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
class StockLot(models.Model):
|
||||
_name = 'stock.lot'
|
||||
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio')
|
||||
|
||||
|
@ -83,224 +81,36 @@ class StockLot(models.Model):
|
|||
('analyzed', 'Analizada'),
|
||||
('stored', 'Almacenada'),
|
||||
('disposed', 'Desechada'),
|
||||
('cancelled', 'Cancelada'),
|
||||
('rejected', 'Rechazada')
|
||||
('cancelled', 'Cancelada')
|
||||
], string='Estado', default='collected', tracking=True)
|
||||
|
||||
# Rejection fields
|
||||
rejection_reason_id = fields.Many2one(
|
||||
'lims.rejection.reason',
|
||||
string='Motivo de Rechazo',
|
||||
tracking=True
|
||||
)
|
||||
rejection_notes = fields.Text(
|
||||
string='Notas de Rechazo',
|
||||
help="Información adicional sobre el rechazo"
|
||||
)
|
||||
rejected_by = fields.Many2one(
|
||||
'res.users',
|
||||
string='Rechazado por',
|
||||
readonly=True
|
||||
)
|
||||
rejection_date = fields.Datetime(
|
||||
string='Fecha de Rechazo',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Re-sampling fields
|
||||
parent_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra Original',
|
||||
help='Muestra original de la cual esta es un re-muestreo',
|
||||
domain="[('is_lab_sample', '=', True)]"
|
||||
)
|
||||
child_sample_ids = fields.One2many(
|
||||
'stock.lot',
|
||||
'parent_sample_id',
|
||||
string='Re-muestras',
|
||||
help='Muestras generadas como re-muestreo de esta'
|
||||
)
|
||||
resample_count = fields.Integer(
|
||||
string='Número de Re-muestreo',
|
||||
help='Indica cuántas veces se ha re-muestreado esta muestra',
|
||||
compute='_compute_resample_count',
|
||||
store=True
|
||||
)
|
||||
is_resample = fields.Boolean(
|
||||
string='Es Re-muestra',
|
||||
compute='_compute_is_resample',
|
||||
store=True
|
||||
)
|
||||
root_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra Original (Raíz)',
|
||||
compute='_compute_root_sample',
|
||||
store=True,
|
||||
help='Muestra original de la cadena de re-muestreos'
|
||||
)
|
||||
resample_chain_count = fields.Integer(
|
||||
string='Re-muestreos en Cadena',
|
||||
compute='_compute_resample_chain_count',
|
||||
help='Número total de re-muestreos en toda la cadena'
|
||||
)
|
||||
|
||||
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"""
|
||||
self.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
|
||||
|
||||
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"""
|
||||
self.write({'state': 'received'})
|
||||
|
||||
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'
|
||||
)
|
||||
self.write({'state': 'in_process'})
|
||||
|
||||
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'
|
||||
)
|
||||
self.write({'state': 'analyzed'})
|
||||
|
||||
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"""
|
||||
self.write({'state': 'stored'})
|
||||
|
||||
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"""
|
||||
self.write({'state': 'disposed'})
|
||||
|
||||
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'
|
||||
)
|
||||
|
||||
def action_open_rejection_wizard(self):
|
||||
"""Open the rejection wizard"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Rechazar Muestra',
|
||||
'res_model': 'lims.sample.rejection.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_sample_id': self.id,
|
||||
}
|
||||
}
|
||||
|
||||
def action_reject(self, create_resample=None):
|
||||
"""Reject the sample - to be called from wizard
|
||||
|
||||
Args:
|
||||
create_resample: Boolean to force resample creation. If None, uses system config
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state == 'completed':
|
||||
raise ValueError('No se puede rechazar una muestra ya completada')
|
||||
|
||||
# This method is called from the wizard, so rejection fields should already be set
|
||||
self.write({
|
||||
'state': 'rejected',
|
||||
'rejected_by': self.env.user.id,
|
||||
'rejection_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
reason_name = self.rejection_reason_id.name if self.rejection_reason_id else 'Sin especificar'
|
||||
notes = self.rejection_notes or ''
|
||||
|
||||
body = f'Muestra rechazada por {self.env.user.name}<br/>Motivo: {reason_name}'
|
||||
if notes:
|
||||
body += f'<br/>Notas: {notes}'
|
||||
|
||||
self.message_post(
|
||||
body=body,
|
||||
subject='Estado actualizado: Rechazada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Notify related sale order if exists
|
||||
if self.request_id:
|
||||
self.request_id.message_post(
|
||||
body=f'La muestra {self.name} ha sido rechazada. Motivo: {reason_name}',
|
||||
subject='Muestra Rechazada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Determine if we should create a resample
|
||||
should_create_resample = False
|
||||
|
||||
if create_resample is not None:
|
||||
# Explicit value from wizard
|
||||
should_create_resample = create_resample
|
||||
else:
|
||||
# Check system configuration
|
||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
||||
should_create_resample = auto_resample
|
||||
|
||||
if should_create_resample:
|
||||
try:
|
||||
# Create resample automatically
|
||||
resample_action = self.action_create_resample()
|
||||
self.message_post(
|
||||
body=_('Re-muestra generada automáticamente debido al rechazo'),
|
||||
subject='Re-muestreo Automático',
|
||||
message_type='notification'
|
||||
)
|
||||
except UserError as e:
|
||||
# If resample creation fails (e.g., max attempts reached), log it
|
||||
self.message_post(
|
||||
body=_('No se pudo generar re-muestra automática: %s') % str(e),
|
||||
subject='Error en Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
"""Cancel the sample"""
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
@api.onchange('sample_type_product_id')
|
||||
def _onchange_sample_type_product_id(self):
|
||||
|
@ -426,177 +236,3 @@ class StockLot(models.Model):
|
|||
if record.is_lab_sample and not record.barcode:
|
||||
record.barcode = record._generate_unique_barcode()
|
||||
return True
|
||||
|
||||
@api.depends('parent_sample_id')
|
||||
def _compute_is_resample(self):
|
||||
"""Compute if this sample is a resample"""
|
||||
for record in self:
|
||||
record.is_resample = bool(record.parent_sample_id)
|
||||
|
||||
@api.depends('child_sample_ids')
|
||||
def _compute_resample_count(self):
|
||||
"""Compute the number of times this sample has been resampled"""
|
||||
for record in self:
|
||||
record.resample_count = len(record.child_sample_ids)
|
||||
|
||||
@api.depends('parent_sample_id')
|
||||
def _compute_root_sample(self):
|
||||
"""Compute the root sample of the resample chain"""
|
||||
for record in self:
|
||||
root = record
|
||||
while root.parent_sample_id:
|
||||
root = root.parent_sample_id
|
||||
record.root_sample_id = root if root != record else False
|
||||
|
||||
@api.depends('parent_sample_id', 'child_sample_ids')
|
||||
def _compute_resample_chain_count(self):
|
||||
"""Compute total resamples in the entire chain"""
|
||||
for record in self:
|
||||
# Find root sample
|
||||
root = record
|
||||
while root.parent_sample_id:
|
||||
root = root.parent_sample_id
|
||||
# Count all resamples from root
|
||||
record.resample_chain_count = self._count_all_resamples_in_chain(root)
|
||||
|
||||
def action_create_resample(self):
|
||||
"""Create a new sample as a resample of the current one"""
|
||||
self.ensure_one()
|
||||
|
||||
# Determine the parent sample for the new resample
|
||||
# If current sample is already a resample, use its parent
|
||||
# Otherwise, use the current sample as parent
|
||||
parent_for_resample = self.parent_sample_id if self.parent_sample_id else self
|
||||
|
||||
# Check if there's already an active resample for the parent
|
||||
active_resamples = parent_for_resample.child_sample_ids.filtered(
|
||||
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||
)
|
||||
if active_resamples:
|
||||
raise UserError(_('La muestra %s ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') %
|
||||
(parent_for_resample.name, ', '.join(active_resamples.mapped('name'))))
|
||||
|
||||
# Get configuration
|
||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
||||
initial_state = IrConfig.get_param('lims_management.resample_state', 'pending_collection')
|
||||
prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-')
|
||||
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
|
||||
|
||||
# Find the original sample (root of the resample chain)
|
||||
original_sample = parent_for_resample
|
||||
while original_sample.parent_sample_id:
|
||||
original_sample = original_sample.parent_sample_id
|
||||
|
||||
# Count all resamples in the chain
|
||||
total_resamples = self._count_all_resamples_in_chain(original_sample)
|
||||
|
||||
# Check maximum resample attempts based on the entire chain
|
||||
if max_attempts > 0 and total_resamples >= max_attempts:
|
||||
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta cadena de muestras.') % max_attempts)
|
||||
|
||||
# Calculate resample number for naming (based on parent's resample count)
|
||||
resample_number = len(parent_for_resample.child_sample_ids) + 1
|
||||
|
||||
# Prepare values for new sample
|
||||
vals = {
|
||||
'name': f"{prefix}{parent_for_resample.name}-{resample_number}",
|
||||
'product_id': self.product_id.id,
|
||||
'patient_id': self.patient_id.id,
|
||||
'doctor_id': self.doctor_id.id,
|
||||
'origin': self.origin,
|
||||
'sample_type_product_id': self.sample_type_product_id.id,
|
||||
'volume_ml': self.volume_ml,
|
||||
'is_lab_sample': True,
|
||||
'state': initial_state,
|
||||
'analysis_names': self.analysis_names,
|
||||
'parent_sample_id': parent_for_resample.id, # Always use the determined parent
|
||||
'request_id': self.request_id.id if self.request_id else False,
|
||||
}
|
||||
|
||||
# Create the resample
|
||||
resample = self.create(vals)
|
||||
|
||||
# Post message in all relevant samples
|
||||
self.message_post(
|
||||
body=_('Re-muestra creada: %s') % resample.name,
|
||||
subject='Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
if self != parent_for_resample:
|
||||
# If we're creating from a resample, also notify the parent
|
||||
parent_for_resample.message_post(
|
||||
body=_('Nueva re-muestra creada: %s (debido al rechazo de %s)') % (resample.name, self.name),
|
||||
subject='Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
resample.message_post(
|
||||
body=_('Esta es una re-muestra de: %s<br/>Creada debido al rechazo de: %s<br/>Motivo: %s') %
|
||||
(parent_for_resample.name, self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'),
|
||||
subject='Re-muestra creada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Notify receptionist if configured
|
||||
auto_notify = IrConfig.get_param('lims_management.auto_notify_resample', 'True') == 'True'
|
||||
if auto_notify:
|
||||
self._notify_resample_created(resample)
|
||||
|
||||
# If there's a related order, update it
|
||||
if self.request_id:
|
||||
self.request_id.message_post(
|
||||
body=_('Se ha creado una re-muestra (%s) para la muestra rechazada %s') % (resample.name, self.name),
|
||||
subject='Re-muestra creada',
|
||||
message_type='notification'
|
||||
)
|
||||
# Add the new sample to the order's generated samples
|
||||
self.request_id.generated_sample_ids = [(4, resample.id)]
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Re-muestra Creada',
|
||||
'res_model': 'stock.lot',
|
||||
'res_id': resample.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _count_all_resamples_in_chain(self, root_sample):
|
||||
"""Count all resamples in the entire chain starting from root"""
|
||||
count = 0
|
||||
samples_to_check = [root_sample]
|
||||
|
||||
while samples_to_check:
|
||||
sample = samples_to_check.pop(0)
|
||||
# Add all child samples to the check list
|
||||
for child in sample.child_sample_ids:
|
||||
count += 1
|
||||
samples_to_check.append(child)
|
||||
|
||||
return count
|
||||
|
||||
def _notify_resample_created(self, resample):
|
||||
"""Notify receptionist users about the created resample"""
|
||||
# Find receptionist users
|
||||
receptionist_group = self.env.ref('lims_management.group_lims_receptionist', raise_if_not_found=False)
|
||||
if receptionist_group:
|
||||
receptionist_users = receptionist_group.users
|
||||
|
||||
# Get the model id for stock.lot
|
||||
model_id = self.env['ir.model'].search([('model', '=', 'stock.lot')], limit=1).id
|
||||
|
||||
# Create activities for receptionists
|
||||
for user in receptionist_users:
|
||||
self.env['mail.activity'].create({
|
||||
'res_model': 'stock.lot',
|
||||
'res_model_id': model_id, # Campo obligatorio
|
||||
'res_id': resample.id,
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
||||
'summary': _('Nueva re-muestra pendiente de recolección'),
|
||||
'note': _('Se ha generado una re-muestra (%s) que requiere recolección. Muestra original: %s') %
|
||||
(resample.name, self.name),
|
||||
'user_id': user.id,
|
||||
'date_deadline': fields.Date.today(),
|
||||
})
|
||||
|
|
|
@ -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>
|
|
@ -6,21 +6,6 @@ access_product_template_parameter_manager,product.template.parameter.manager,mod
|
|||
access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_range,base.group_user,1,0,0,0
|
||||
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,1,1,1,1
|
||||
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_line_receptionist,sale.order.line.receptionist,sale.model_sale_order_line,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_technician,sale.order.technician,sale.model_sale_order,group_lims_technician,1,0,0,0
|
||||
access_sale_order_line_technician,sale.order.line.technician,sale.model_sale_order_line,group_lims_technician,1,0,0,0
|
||||
access_sale_order_admin,sale.order.admin,sale.model_sale_order,group_lims_admin,1,1,1,1
|
||||
access_sale_order_line_admin,sale.order.line.admin,sale.model_sale_order_line,group_lims_admin,1,1,1,1
|
||||
access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
|
||||
access_lims_test_receptionist,lims.test.receptionist,model_lims_test,group_lims_receptionist,1,0,0,0
|
||||
access_lims_test_technician,lims.test.technician,model_lims_test,group_lims_technician,1,1,1,0
|
||||
access_lims_test_admin,lims.test.admin,model_lims_test,group_lims_admin,1,1,1,1
|
||||
access_lims_result_receptionist,lims.result.receptionist,model_lims_result,group_lims_receptionist,1,0,0,0
|
||||
access_lims_result_technician,lims.result.technician,model_lims_result,group_lims_technician,1,1,1,0
|
||||
access_lims_result_admin,lims.result.admin,model_lims_result,group_lims_admin,1,1,1,1
|
||||
access_lims_rejection_reason_user,lims.rejection.reason.user,model_lims_rejection_reason,base.group_user,1,0,0,0
|
||||
access_lims_rejection_reason_technician,lims.rejection.reason.technician,model_lims_rejection_reason,group_lims_technician,1,0,0,0
|
||||
access_lims_rejection_reason_admin,lims.rejection.reason.admin,model_lims_rejection_reason,group_lims_admin,1,1,1,1
|
||||
access_lims_sample_rejection_wizard_user,lims.sample.rejection.wizard.user,model_lims_sample_rejection_wizard,base.group_user,1,1,1,1
|
||||
access_lims_sample_rejection_wizard_technician,lims.sample.rejection.wizard.technician,model_lims_sample_rejection_wizard,group_lims_technician,1,1,1,1
|
||||
access_lims_config_settings_admin,lims.config.settings.admin,model_lims_config_settings,group_lims_admin,1,1,1,1
|
||||
access_lims_test_user,lims.test.user,model_lims_test,base.group_user,1,1,1,1
|
||||
access_lims_result_user,lims.result.user,model_lims_result,base.group_user,1,1,1,1
|
||||
|
|
|
|
@ -33,81 +33,5 @@
|
|||
El usuario tiene acceso completo al módulo LIMS, incluyendo la validación de resultados, configuración y reportes.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Reglas de registro para lims.test -->
|
||||
|
||||
<!-- Recepcionistas: Solo pueden ver pruebas, no editarlas -->
|
||||
<record id="lims_test_receptionist_read_rule" model="ir.rule">
|
||||
<field name="name">Recepcionista: Solo lectura en pruebas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_receptionist'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Técnicos: Pueden editar solo pruebas no validadas -->
|
||||
<record id="lims_test_technician_write_rule" model="ir.rule">
|
||||
<field name="name">Técnico: Editar solo pruebas no validadas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_technician'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[('state', '!=', 'validated')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Administradores: Acceso completo (sin restricciones) -->
|
||||
<record id="lims_test_admin_all_rule" model="ir.rule">
|
||||
<field name="name">Administrador: Acceso completo a pruebas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_admin'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Reglas de registro para lims.result -->
|
||||
|
||||
<!-- Recepcionistas: Solo pueden ver resultados -->
|
||||
<record id="lims_result_receptionist_read_rule" model="ir.rule">
|
||||
<field name="name">Recepcionista: Solo lectura en resultados</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_receptionist'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Técnicos: Pueden editar resultados de pruebas no validadas -->
|
||||
<record id="lims_result_technician_write_rule" model="ir.rule">
|
||||
<field name="name">Técnico: Editar resultados de pruebas no validadas</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_technician'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[('test_id.state', '!=', 'validated')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Administradores: Acceso completo a resultados -->
|
||||
<record id="lims_result_admin_all_rule" model="ir.rule">
|
||||
<field name="name">Administrador: Acceso completo a resultados</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_admin'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</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>
|
|
@ -1,55 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Laboratory Configuration Form View -->
|
||||
<record id="view_lims_config_settings_form" model="ir.ui.view">
|
||||
<field name="name">lims.config.settings.form</field>
|
||||
<field name="model">lims.config.settings</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuración del Laboratorio">
|
||||
<header>
|
||||
<button string="Guardar" type="object" name="execute" class="oe_highlight"/>
|
||||
<button string="Cancelar" special="cancel"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="o_form_label">Configuración de Re-muestreo</div>
|
||||
<group>
|
||||
<group name="resample_settings" string="Re-muestreo Automático">
|
||||
<field name="auto_resample_on_rejection"/>
|
||||
<field name="resample_state" invisible="not auto_resample_on_rejection"/>
|
||||
<field name="resample_prefix" invisible="not auto_resample_on_rejection"/>
|
||||
<field name="max_resample_attempts" invisible="not auto_resample_on_rejection"/>
|
||||
</group>
|
||||
<group name="notification_settings" string="Notificaciones">
|
||||
<field name="auto_notify_resample" invisible="not auto_resample_on_rejection"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información">
|
||||
<div class="text-muted">
|
||||
<p>El re-muestreo automático permite generar una nueva muestra cuando se rechaza una existente.</p>
|
||||
<p>Las notificaciones se enviarán a todos los usuarios con rol de Recepcionista.</p>
|
||||
</div>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open laboratory configuration -->
|
||||
<record id="action_lims_config_settings" model="ir.actions.act_window">
|
||||
<field name="name">Configuración del Laboratorio</field>
|
||||
<field name="res_model">lims.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'dialog_size': 'medium'}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu for Laboratory Configuration -->
|
||||
<menuitem id="menu_lims_lab_config"
|
||||
name="Configuración del Laboratorio"
|
||||
parent="lims_management.lims_menu_config"
|
||||
action="action_lims_config_settings"
|
||||
sequence="60"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
</data>
|
||||
</odoo>
|
|
@ -35,9 +35,7 @@
|
|||
|
||||
<group>
|
||||
<group>
|
||||
<field name="sample_id" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="sample_id" readonly="1"/>
|
||||
<field name="technician_id" readonly="state != 'in_process'"/>
|
||||
</group>
|
||||
<group>
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
<group>
|
||||
<group string="Información del Test">
|
||||
<field name="test_id" readonly="1"/>
|
||||
<field name="test_sample_id" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="test_sample_state" widget="badge"/>
|
||||
<field name="patient_id" readonly="1"/>
|
||||
<field name="test_date" readonly="1"/>
|
||||
</group>
|
||||
|
@ -73,13 +69,6 @@
|
|||
<field name="arch" type="xml">
|
||||
<list string="Resultados de Análisis" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="test_sample_id"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"
|
||||
optional="show"/>
|
||||
<field name="test_sample_state"
|
||||
widget="badge"
|
||||
optional="show"/>
|
||||
<field name="parameter_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="parameter_code" optional="show"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
|
@ -112,7 +101,6 @@
|
|||
<field name="arch" type="xml">
|
||||
<search string="Buscar Resultados">
|
||||
<field name="test_id"/>
|
||||
<field name="test_sample_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="patient_id"/>
|
||||
|
@ -130,19 +118,10 @@
|
|||
domain="[('parameter_value_type', '=', 'selection')]"/>
|
||||
<filter string="Sí/No" name="boolean"
|
||||
domain="[('parameter_value_type', '=', 'boolean')]"/>
|
||||
<separator/>
|
||||
<filter string="Muestras Pendientes" name="sample_pending"
|
||||
domain="[('test_sample_state', 'in', ['pending_collection', 'collected'])]"/>
|
||||
<filter string="Muestras en Proceso" name="sample_process"
|
||||
domain="[('test_sample_state', '=', 'in_process')]"/>
|
||||
<filter string="Muestras Completadas" name="sample_completed"
|
||||
domain="[('test_sample_state', '=', 'completed')]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Test" name="group_test" context="{'group_by': 'test_id'}"/>
|
||||
<filter string="Parámetro" name="group_parameter" context="{'group_by': 'parameter_id'}"/>
|
||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
||||
<filter string="Muestra" name="group_sample" context="{'group_by': 'test_sample_id'}"/>
|
||||
<filter string="Estado de Muestra" name="group_sample_state" context="{'group_by': 'test_sample_state'}"/>
|
||||
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'parameter_value_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
|
|
|
@ -11,29 +11,23 @@
|
|||
<header>
|
||||
<button name="action_start_process" string="Iniciar Proceso"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'draft'"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_enter_results" string="Marcar Resultados Ingresados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'in_process'"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
invisible="state != 'in_process'"/>
|
||||
<button name="action_validate" string="Validar Resultados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'result_entered' or not require_validation"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
invisible="state != 'result_entered' or not require_validation"/>
|
||||
<button name="action_cancel" string="Cancelar"
|
||||
type="object"
|
||||
invisible="state in ['validated', 'cancelled']"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
invisible="state in ['validated', 'cancelled']"/>
|
||||
<button name="action_draft" string="Volver a Borrador"
|
||||
type="object"
|
||||
invisible="state != 'cancelled'"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
invisible="state != 'cancelled'"/>
|
||||
<button name="action_regenerate_results" string="Regenerar Resultados"
|
||||
type="object"
|
||||
invisible="state not in ['draft', 'in_process']"
|
||||
confirm="¿Está seguro de regenerar los resultados? Esto eliminará los resultados actuales."
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
confirm="¿Está seguro de regenerar los resultados? Esto eliminará los resultados actuales."/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,in_process,result_entered,validated"/>
|
||||
</header>
|
||||
|
@ -50,9 +44,7 @@
|
|||
<field name="product_id"/>
|
||||
<field name="sample_id"
|
||||
options="{'no_create': True}"
|
||||
domain="[('is_lab_sample', '=', True), ('patient_id', '=', patient_id)]"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
domain="[('is_lab_sample', '=', True), ('patient_id', '=', patient_id)]"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="technician_id" readonly="state != 'draft'"/>
|
||||
|
@ -64,7 +56,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 +82,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 +110,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>
|
||||
|
@ -144,9 +133,7 @@
|
|||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="sample_id"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="sample_id"/>
|
||||
<field name="technician_id" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'validated'"
|
||||
|
|
|
@ -102,14 +102,6 @@
|
|||
action="action_lims_lab_sample"
|
||||
sequence="16"/>
|
||||
|
||||
<!-- Menú para Muestras Rechazadas -->
|
||||
<menuitem
|
||||
id="lims_menu_lab_samples_rejected"
|
||||
name="Muestras Rechazadas"
|
||||
parent="lims_menu_root"
|
||||
action="action_lab_sample_rejected"
|
||||
sequence="17"/>
|
||||
|
||||
<!-- Submenú de Laboratorio -->
|
||||
<menuitem
|
||||
id="lims_menu_laboratory"
|
||||
|
@ -155,51 +147,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"
|
||||
|
@ -318,13 +265,6 @@
|
|||
action="action_lims_parameter_statistics"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- Menú de Motivos de Rechazo -->
|
||||
<menuitem id="menu_lims_rejection_reason"
|
||||
name="Motivos de Rechazo"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_rejection_reason"
|
||||
sequence="50"/>
|
||||
|
||||
<!-- Menú de configuración de ajustes -->
|
||||
<menuitem id="menu_lims_config_settings"
|
||||
name="Ajustes"
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- List View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_list" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.list</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Motivos de Rechazo" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="severity" widget="badge"/>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="rejection_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_form" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.form</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Motivo de Rechazo">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archivado" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Motivo de rechazo..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="severity"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="active"/>
|
||||
<field name="rejection_count"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Descripción">
|
||||
<field name="description" nolabel="1" placeholder="Descripción detallada del motivo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_search" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.search</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Motivos de Rechazo">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<filter string="Activos" name="active" domain="[('active', '=', True)]"/>
|
||||
<filter string="Archivados" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Requiere Nueva Muestra" name="requires_new" domain="[('requires_new_sample', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Severidad Alta/Crítica" name="high_severity" domain="[('severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Severidad" name="group_severity" context="{'group_by': 'severity'}"/>
|
||||
<filter string="Requiere Nueva Muestra" name="group_requires_new" context="{'group_by': 'requires_new_sample'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejection Reasons -->
|
||||
<record id="action_lims_rejection_reason" model="ir.actions.act_window">
|
||||
<field name="name">Motivos de Rechazo</field>
|
||||
<field name="res_model">lims.rejection.reason</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_rejection_reason_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Configure los motivos de rechazo de muestras
|
||||
</p>
|
||||
<p>
|
||||
Los motivos de rechazo permiten categorizar y documentar
|
||||
las razones por las cuales una muestra no puede ser procesada.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -14,14 +14,8 @@
|
|||
string="Imprimir Etiquetas"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="not is_lab_request or state != 'sale' or not all_sample_ids"
|
||||
invisible="not is_lab_request or state != 'sale' or not generated_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"/>
|
||||
|
@ -35,49 +29,26 @@
|
|||
</xpath>
|
||||
<!-- Add Generated Samples tab -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Muestras" name="all_samples" invisible="not is_lab_request">
|
||||
<group string="Todas las Muestras (incluyendo Re-muestras)">
|
||||
<field name="all_sample_ids" nolabel="1" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}">
|
||||
<list string="Todas las Muestras" create="false" edit="false" delete="false">
|
||||
<page string="Muestras Generadas" name="generated_samples" invisible="not is_lab_request">
|
||||
<group>
|
||||
<field name="generated_sample_ids" nolabel="1" readonly="1">
|
||||
<list string="Muestras Generadas" create="false" edit="false" delete="false">
|
||||
<field name="name" string="Código de Muestra"/>
|
||||
<field name="barcode" string="Código de Barras" optional="show"/>
|
||||
<field name="barcode" string="Código de Barras"/>
|
||||
<field name="sample_type_product_id" string="Tipo de Muestra"/>
|
||||
<field name="volume_ml" string="Volumen (ml)" optional="show"/>
|
||||
<field name="analysis_names" string="Análisis" optional="show"/>
|
||||
<field name="is_resample" string="Es Re-muestra" widget="boolean_toggle"/>
|
||||
<field name="parent_sample_id" string="Muestra Original" optional="show"/>
|
||||
<field name="state" string="Estado" widget="badge"
|
||||
decoration-success="state == 'analyzed'"
|
||||
decoration-info="state == 'in_process'"
|
||||
decoration-danger="state == 'rejected'"
|
||||
decoration-warning="state == 'pending_collection'"/>
|
||||
<field name="rejection_reason_id" string="Motivo Rechazo" optional="show"/>
|
||||
<field name="volume_ml" string="Volumen (ml)"/>
|
||||
<field name="analysis_names" string="Análisis"/>
|
||||
<field name="state" string="Estado"/>
|
||||
<button name="action_collect" string="Recolectar" type="object"
|
||||
class="btn-sm btn-primary" invisible="state != 'pending_collection'"/>
|
||||
class="btn-primary" invisible="state != 'pending_collection'"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Resumen" col="4">
|
||||
<field name="generated_sample_ids" invisible="1"/>
|
||||
<group>
|
||||
<label for="generated_sample_ids" string="Muestras Originales:"/>
|
||||
<div>
|
||||
<span class="badge badge-primary"><field name="generated_sample_ids" readonly="1" widget="many2many_tags"/></span>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p><i class="fa fa-info-circle"/> Las muestras han sido generadas automáticamente basándose en los análisis solicitados.</p>
|
||||
<p>Las re-muestras se generan cuando una muestra es rechazada.</p>
|
||||
</div>
|
||||
</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 invisible="not generated_sample_ids">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p>Las muestras han sido generadas automáticamente basándose en los análisis solicitados.
|
||||
Cada muestra agrupa los análisis que requieren el mismo tipo de contenedor.</p>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
|
|
|
@ -15,9 +15,7 @@
|
|||
<field name="collection_date" string="Fecha de Recolección"/>
|
||||
<field name="collector_id" string="Recolectado por"/>
|
||||
<field name="container_type" optional="hide" string="Tipo Contenedor (Obsoleto)"/>
|
||||
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-danger="state == 'rejected'" decoration-muted="state == 'stored' or state == 'disposed' or state == 'cancelled'" widget="badge"/>
|
||||
<field name="is_resample" string="Re-muestra" widget="boolean_toggle" optional="show"/>
|
||||
<field name="resample_count" string="Re-muestreos" optional="show"/>
|
||||
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-muted="state == 'stored' or state == 'disposed'" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -35,19 +33,7 @@
|
|||
<button name="action_complete_analysis" string="Completar Análisis" type="object" class="oe_highlight" invisible="state != 'in_process'"/>
|
||||
<button name="action_store" string="Almacenar" type="object" invisible="state not in ['analyzed', 'in_process', 'received']"/>
|
||||
<button name="action_dispose" string="Desechar" type="object" invisible="state == 'disposed'"/>
|
||||
<button name="action_open_rejection_wizard"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-danger"
|
||||
invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/>
|
||||
<button name="action_cancel" string="Cancelar" type="object" invisible="state in ['cancelled', 'rejected', 'disposed']"/>
|
||||
<button name="action_create_resample"
|
||||
string="Crear Re-muestra"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'rejected' or resample_count >= 3"
|
||||
confirm="¿Está seguro de que desea crear una re-muestra para esta muestra rechazada?"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored,rejected"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
|
@ -83,100 +69,10 @@
|
|||
invisible="sample_type_product_id != False"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información de Rechazo" invisible="state != 'rejected'" col="4">
|
||||
<field name="rejection_reason_id" readonly="1"/>
|
||||
<field name="rejected_by" readonly="1"/>
|
||||
<field name="rejection_date" readonly="1"/>
|
||||
<field name="rejection_notes" readonly="1" colspan="4"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
|
||||
<group col="4">
|
||||
<field name="is_resample" invisible="1"/>
|
||||
<field name="resample_count" invisible="1"/>
|
||||
<field name="parent_sample_id" readonly="1" invisible="not is_resample"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="root_sample_id" readonly="1" invisible="not is_resample"/>
|
||||
<field name="resample_chain_count" readonly="1" invisible="resample_chain_count == 0"/>
|
||||
</group>
|
||||
<group string="Re-muestras Generadas" invisible="resample_count == 0">
|
||||
<field name="child_sample_ids" nolabel="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="collection_date"/>
|
||||
<field name="rejection_reason_id"/>
|
||||
<field name="resample_count" string="Re-muestras propias"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Información de Trazabilidad" invisible="not is_resample">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p><i class="fa fa-info-circle"/> Esta muestra es parte de una cadena de re-muestreo.</p>
|
||||
<p>Total de re-muestreos en la cadena: <field name="resample_chain_count" readonly="1" nolabel="1" class="oe_inline"/></p>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Lab Samples -->
|
||||
<record id="view_lab_sample_search" model="ir.ui.view">
|
||||
<field name="name">lab.sample.search</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Muestras">
|
||||
<field name="name" string="Código"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="barcode"/>
|
||||
<field name="analysis_names"/>
|
||||
<filter string="Pendientes" name="pending" domain="[('state', 'in', ['pending_collection', 'collected', 'received'])]"/>
|
||||
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
||||
<filter string="Analizadas" name="analyzed" domain="[('state', '=', 'analyzed')]"/>
|
||||
<filter string="Rechazadas" name="rejected" domain="[('state', '=', 'rejected')]"/>
|
||||
<filter string="Re-muestras" name="resamples" domain="[('is_resample', '=', True)]"/>
|
||||
<filter string="Con Re-muestras" name="has_resamples" domain="[('resample_count', '>', 0)]"/>
|
||||
<separator/>
|
||||
<filter string="Hoy" name="today" domain="[('collection_date', '>=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')), ('collection_date', '<=', datetime.datetime.now().strftime('%Y-%m-%d 23:59:59'))]"/>
|
||||
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '>=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Rechazadas - Alta Severidad" name="rejected_high"
|
||||
domain="[('state', '=', 'rejected'), ('rejection_reason_id.severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Estado" name="group_state" context="{'group_by': 'state'}"/>
|
||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
||||
<filter string="Fecha de Recolección" name="group_collection" context="{'group_by': 'collection_date:day'}"/>
|
||||
<filter string="Motivo de Rechazo" name="group_rejection" context="{'group_by': 'rejection_reason_id'}"/>
|
||||
<filter string="Es Re-muestra" name="group_resample" context="{'group_by': 'is_resample'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejected Samples -->
|
||||
<record id="action_lab_sample_rejected" model="ir.actions.act_window">
|
||||
<field name="name">Muestras Rechazadas</field>
|
||||
<field name="res_model">stock.lot</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('is_lab_sample', '=', True), ('state', '=', 'rejected')]</field>
|
||||
<field name="context">{'search_default_rejected': 1, 'default_is_lab_sample': True}</field>
|
||||
<field name="search_view_id" ref="view_lab_sample_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay muestras rechazadas
|
||||
</p>
|
||||
<p>
|
||||
Las muestras rechazadas aparecerán aquí con información
|
||||
sobre el motivo del rechazo y las acciones tomadas.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import sample_rejection_wizard
|
|
@ -1,90 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class SampleRejectionWizard(models.TransientModel):
|
||||
_name = 'lims.sample.rejection.wizard'
|
||||
_description = 'Wizard para Rechazo de Muestras'
|
||||
|
||||
sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra',
|
||||
required=True,
|
||||
readonly=True,
|
||||
domain=[('is_lab_sample', '=', True)]
|
||||
)
|
||||
|
||||
rejection_reason_id = fields.Many2one(
|
||||
'lims.rejection.reason',
|
||||
string='Motivo de Rechazo',
|
||||
required=True,
|
||||
domain=[('active', '=', True)]
|
||||
)
|
||||
|
||||
rejection_notes = fields.Text(
|
||||
string='Notas Adicionales',
|
||||
help="Información adicional sobre el rechazo"
|
||||
)
|
||||
|
||||
requires_new_sample = fields.Boolean(
|
||||
string='Requiere Nueva Muestra',
|
||||
related='rejection_reason_id.requires_new_sample',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
create_new_sample = fields.Boolean(
|
||||
string='Crear Nueva Solicitud',
|
||||
help="Crear automáticamente una nueva solicitud de muestra"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SampleRejectionWizard, self).default_get(fields)
|
||||
active_id = self.env.context.get('active_id')
|
||||
if active_id:
|
||||
sample = self.env['stock.lot'].browse(active_id)
|
||||
res['sample_id'] = sample.id
|
||||
return res
|
||||
|
||||
@api.onchange('rejection_reason_id')
|
||||
def _onchange_rejection_reason_id(self):
|
||||
if self.rejection_reason_id and self.rejection_reason_id.requires_new_sample:
|
||||
self.create_new_sample = True
|
||||
|
||||
def action_reject_sample(self):
|
||||
"""Reject the sample with the provided reason"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.sample_id:
|
||||
raise ValidationError('No se ha seleccionado ninguna muestra')
|
||||
|
||||
if self.sample_id.state == 'completed':
|
||||
raise ValidationError('No se puede rechazar una muestra ya completada')
|
||||
|
||||
# Update sample with rejection information
|
||||
self.sample_id.write({
|
||||
'rejection_reason_id': self.rejection_reason_id.id,
|
||||
'rejection_notes': self.rejection_notes
|
||||
})
|
||||
|
||||
# Call the rejection method on the sample with explicit resample creation preference
|
||||
self.sample_id.action_reject(create_resample=self.create_new_sample)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _create_new_sample_request(self):
|
||||
"""Create a new sample request based on the rejected one"""
|
||||
original_order = self.sample_id.request_id
|
||||
|
||||
# Create a note in the original order
|
||||
original_order.message_post(
|
||||
body=f'Se solicitará una nueva muestra debido al rechazo. Motivo: {self.rejection_reason_id.name}',
|
||||
subject='Nueva Muestra Solicitada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Here you could implement logic to create a new sale.order
|
||||
# or a specific request for a new sample
|
||||
# For now, we'll just add a note
|
||||
|
||||
return True
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for Sample Rejection Wizard -->
|
||||
<record id="view_lims_sample_rejection_wizard_form" model="ir.ui.view">
|
||||
<field name="name">lims.sample.rejection.wizard.form</field>
|
||||
<field name="model">lims.sample.rejection.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rechazar Muestra">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="sample_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="rejection_reason_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample" invisible="1"/>
|
||||
<field name="create_new_sample"
|
||||
invisible="not requires_new_sample"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información Adicional">
|
||||
<field name="rejection_notes" placeholder="Agregue cualquier información relevante sobre el rechazo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_reject_sample"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
confirm="¿Está seguro de rechazar esta muestra? Esta acción no se puede deshacer."/>
|
||||
<button string="Cancelar" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Sample Rejection Wizard -->
|
||||
<record id="action_lims_sample_rejection_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Rechazar Muestra</field>
|
||||
<field name="res_model">lims.sample.rejection.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_sample_id': active_id}</field>
|
||||
</record>
|
||||
</odoo>
|
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
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para asignar el usuario admin al grupo de Administrador de Laboratorio
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# Buscar el usuario admin
|
||||
admin_user = env['res.users'].search([('login', '=', 'admin')], limit=1)
|
||||
if not admin_user:
|
||||
_logger.error("No se encontró el usuario admin")
|
||||
exit(1)
|
||||
|
||||
# Buscar el grupo de Administrador de Laboratorio
|
||||
try:
|
||||
lab_admin_group = env.ref('lims_management.group_lims_admin')
|
||||
except ValueError:
|
||||
_logger.error("No se encontró el grupo de Administrador de Laboratorio")
|
||||
exit(1)
|
||||
|
||||
# Verificar si el usuario ya está en el grupo
|
||||
if lab_admin_group in admin_user.groups_id:
|
||||
_logger.info("El usuario admin ya está en el grupo de Administrador de Laboratorio")
|
||||
else:
|
||||
# Agregar el usuario al grupo
|
||||
admin_user.write({
|
||||
'groups_id': [(4, lab_admin_group.id)]
|
||||
})
|
||||
_logger.info("Usuario admin agregado exitosamente al grupo de Administrador de Laboratorio")
|
||||
|
||||
# Confirmar los grupos del usuario
|
||||
group_names = ', '.join(admin_user.groups_id.mapped('name'))
|
||||
_logger.info(f"Grupos del usuario admin: {group_names}")
|
||||
|
||||
env.cr.commit()
|
||||
_logger.info("Cambios guardados exitosamente")
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Error al asignar usuario admin al grupo: {str(e)}")
|
||||
exit(1)
|
|
@ -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,63 +0,0 @@
|
|||
import odoo
|
||||
import json
|
||||
|
||||
def check_demo_users(cr):
|
||||
"""Verificar si los usuarios demo fueron creados"""
|
||||
cr.execute("""
|
||||
SELECT
|
||||
u.id,
|
||||
u.login,
|
||||
u.name,
|
||||
u.active,
|
||||
array_agg(g.name) as groups
|
||||
FROM res_users u
|
||||
LEFT JOIN res_groups_users_rel rel ON rel.uid = u.id
|
||||
LEFT JOIN res_groups g ON g.id = rel.gid
|
||||
WHERE u.login IN ('recepcionista', 'tecnico', 'administrador')
|
||||
GROUP BY u.id, u.login, u.name, u.active
|
||||
ORDER BY u.login
|
||||
""")
|
||||
|
||||
users = cr.fetchall()
|
||||
|
||||
print("\n=== USUARIOS DEMO CREADOS ===")
|
||||
print("-" * 60)
|
||||
|
||||
if not users:
|
||||
print("❌ NO se encontraron usuarios demo")
|
||||
return
|
||||
|
||||
for user in users:
|
||||
user_id, login, name, active, groups = user
|
||||
status = "✓ Activo" if active else "✗ Inactivo"
|
||||
print(f"\nUsuario: {login}")
|
||||
print(f" ID: {user_id}")
|
||||
print(f" Nombre: {name}")
|
||||
print(f" Estado: {status}")
|
||||
print(f" Grupos: {', '.join(groups) if groups[0] else 'Sin grupos'}")
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print(f"Total usuarios demo encontrados: {len(users)}")
|
||||
|
||||
# Verificar contraseñas (solo para confirmar que pueden loguearse)
|
||||
expected_users = {
|
||||
'recepcionista': 'Recepcionista Demo',
|
||||
'tecnico': 'Técnico Demo',
|
||||
'administrador': 'Administrador Lab Demo'
|
||||
}
|
||||
|
||||
missing = []
|
||||
for login, expected_name in expected_users.items():
|
||||
if not any(u[1] == login for u in users):
|
||||
missing.append(login)
|
||||
|
||||
if missing:
|
||||
print(f"\n⚠️ Usuarios faltantes: {', '.join(missing)}")
|
||||
else:
|
||||
print("\n✅ Todos los usuarios demo esperados fueron creados")
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
check_demo_users(cr)
|
|
@ -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