Compare commits

..

1 Commits

Author SHA1 Message Date
Luis Ernesto Portillo Zaldivar
c4cfc0b804 docs: Agregar documentación sobre manejo de códigos de barras en Odoo 18
- Sintaxis correcta para widget barcode en reportes QWeb
- Solución para caracteres especiales en PDFs
- Ejemplo de layout para múltiples etiquetas por página
- Problemas comunes y sus soluciones
2025-07-15 22:07:49 -06:00
65 changed files with 296 additions and 5040 deletions

View File

@ -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": []
}

View File

@ -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&#205;NICO</h4>
```
- í = &#237;
- Í = &#205;
- á = &#225;
@ -461,16 +402,15 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
- Ñ = &#209;
##### Layout de etiquetas múltiples por página
```xml
<!-- Contenedor principal sin salto de página -->
<div class="page">
<t t-foreach="docs" t-as="o">
<!-- Cada etiqueta como inline-block -->
<div style="display: inline-block; vertical-align: top;
<div style="display: inline-block; vertical-align: top;
page-break-inside: avoid; overflow: hidden;">
<!-- Contenido de la etiqueta -->
</div>
</t>
</div>
```
```

View File

@ -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

View File

@ -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: `&#205;` para Í, `&#241;` 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

View File

@ -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
View 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
View 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

View File

@ -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.

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizards

View File

@ -16,7 +16,7 @@
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
'category': 'Industries',
'version': '18.0.1.0.0',
'depends': ['base', 'product', 'sale', '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',

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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"""

View File

@ -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!'),
]

View File

@ -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)

View File

@ -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(),
})

View File

@ -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&#205;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&#243;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&#241;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&#250;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&#233;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&#193;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&#237;tico que requiere atenci&#243;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&#243;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&#225; dirigido exclusivamente al paciente y/o m&#233;dico tratante.</p>
<p>Los resultados se relacionan &#250;nicamente con las muestras analizadas.</p>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -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>

View File

@ -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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
6 access_lims_parameter_range_user lims.parameter.range.user model_lims_parameter_range base.group_user 1 0 0 0
7 access_lims_parameter_range_manager lims.parameter.range.manager model_lims_parameter_range group_lims_admin 1 1 1 1
8 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
9 access_stock_lot_user stock.lot.user stock.model_stock_lot base.group_user 1 1 1 1
10 access_lims_test_receptionist access_lims_test_user lims.test.receptionist lims.test.user model_lims_test group_lims_receptionist base.group_user 1 0 1 0 1 0 1
11 access_lims_test_technician access_lims_result_user lims.test.technician lims.result.user model_lims_test model_lims_result group_lims_technician base.group_user 1 1 1 0 1
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

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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'"

View File

@ -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 &#211;rdenes"
parent="menu_lims_dashboards"
action="action_lab_order_dashboard"
sequence="10"/>
<menuitem id="menu_technician_productivity_dashboard"
name="Productividad de T&#233;cnicos"
parent="menu_lims_dashboards"
action="action_technician_productivity_dashboard"
sequence="20"/>
<menuitem id="menu_sample_dashboard"
name="Dashboard de Muestras"
parent="menu_lims_dashboards"
action="action_sample_dashboard"
sequence="30"/>
<menuitem id="menu_out_of_range_dashboard"
name="Par&#225;metros Fuera de Rango"
parent="menu_lims_dashboards"
action="action_out_of_range_dashboard"
sequence="40"/>
<menuitem id="menu_top_analysis_dashboard"
name="An&#225;lisis M&#225;s Solicitados"
parent="menu_lims_dashboards"
action="action_top_analysis_dashboard"
sequence="50"/>
<menuitem id="menu_test_demographics_dashboard"
name="Distribuci&#243;n Demogr&#225;fica"
parent="menu_lims_dashboards"
action="action_test_demographics_dashboard"
sequence="60"/>
<!-- Submenú de Reportes -->
<menuitem
id="lims_menu_reports"
@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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', '&gt;=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')), ('collection_date', '&lt;=', datetime.datetime.now().strftime('%Y-%m-%d 23:59:59'))]"/>
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '&gt;=', (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>

View File

@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from . import sample_rejection_wizard

View File

@ -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

View File

@ -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
View 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

View File

@ -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)

View File

@ -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:")

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)}")

View File

@ -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()

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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?: {'' if result.is_critical else 'NO'}")
if result.is_critical:
# Limpiar las notas para probar el autocompletado
result.notes = ''
# Simular cambio en el valor para activar el onchange
with env.cr.savepoint():
# Trigger the onchange by updating the value
result.with_context(force_onchange=True)._onchange_critical_value()
print(f" Nota autocompletada: {result.notes}")
# No guardar los cambios, solo mostrar
env.cr.rollback()
# Probar con valores específicos
print("\n\n=== PRUEBA CON VALORES ESPECÍFICOS ===\n")
# Buscar parámetros específicos
test_params = [
('Glucosa', 200.0, 'high'),
('Glucosa', 50.0, 'low'),
('Hemoglobina', 20.0, 'high'),
('Hemoglobina', 7.0, 'low'),
('Plaquetas', 600000, 'high'),
('Plaquetas', 50000, 'low')
]
for param_name, test_value, expected_type in test_params:
# Buscar un resultado con este parámetro
result = env['lims.result'].search([
('parameter_name', 'ilike', param_name),
('parameter_value_type', '=', 'numeric')
], limit=1)
if result:
print(f"\nProbando {param_name} con valor {test_value} (esperado: {expected_type})")
with env.cr.savepoint():
# Establecer el valor de prueba
result.value_numeric = test_value
result.notes = ''
# Forzar recálculo de is_critical
result._compute_is_out_of_range()
# Trigger el onchange
result._onchange_critical_value()
print(f" ¿Es crítico?: {'' if result.is_critical else 'NO'}")
print(f" Nota generada: {result.notes[:100]}...")
# No guardar
env.cr.rollback()
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
test_critical_notes_autocomplete(cr)

View File

@ -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()

View File

@ -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)}")

View File

@ -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)