Merge pull request 'feat(#71): Dashboards para administrador del laboratorio' (#73) from feature/71-laboratory-dashboards into dev

Reviewed-on: #73
This commit is contained in:
luis_portillo 2025-07-18 18:13:41 +00:00
commit cf1e40726f
12 changed files with 655 additions and 39 deletions

View File

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

View File

@ -2,6 +2,11 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Notifications
When tasks complete, or you need autorizathion for an action notify me using:
powershell.exe -c "[System.Media.SystemSounds]::Beep.Play()"
## Project Overview
This is a Laboratory Information Management System (LIMS) module for Odoo 18 ERP, specifically designed for clinical laboratories. The module manages patients, samples, analyses, and test results.
@ -16,6 +21,7 @@ This is a Laboratory Information Management System (LIMS) module for Odoo 18 ERP
## Development Commands
### Starting the Environment
```bash
# Start all services
docker-compose up -d
@ -30,10 +36,13 @@ docker-compose down -v
**IMPORTANT**: Odoo initialization takes approximately 5 minutes. When using docker-compose commands, set timeout to 5 minutes (300000ms) to avoid premature timeouts.
### Instance Persistence Policy
After successful installation/update, the instance must remain active for user validation. Do NOT stop the instance until user explicitly confirms testing is complete.
### MANDATORY Testing Rule
**CRITICAL**: After EVERY task that modifies code, models, views, or data:
1. Restart the ephemeral instance: `docker-compose down -v && docker-compose up -d`
2. Check initialization logs for errors: `docker-compose logs odoo_init | grep -i "error\|traceback\|exception"`
3. Verify successful completion: `docker-compose logs odoo_init | tail -30`
@ -41,7 +50,9 @@ After successful installation/update, the instance must remain active for user v
5. If errors are found, fix them before continuing
### Development Workflow per Task
When implementing issues with multiple tasks, follow this workflow for EACH task:
1. **Stop instance**: `docker-compose down -v`
2. **Implement the task**: Make code changes
3. **Start instance**: `docker-compose up -d` (timeout: 300000ms)
@ -54,15 +65,18 @@ When implementing issues with multiple tasks, follow this workflow for EACH task
### Database Operations
#### Direct PostgreSQL Access
```bash
# Connect to PostgreSQL
docker exec -it lims_db psql -U odoo -d odoo
```
#### Python Script Method (Recommended)
For complex queries, use Python scripts with Odoo ORM:
1. Create script (e.g., `test/verify_products.py`):
```python
import odoo
import json
@ -80,16 +94,19 @@ if __name__ == '__main__':
```
2. Copy to container:
```bash
docker cp test/verify_products.py lims_odoo:/tmp/verify_products.py
```
3. Execute:
```bash
docker-compose exec odoo python3 /tmp/verify_products.py
```
### Gitea Integration
```bash
# Create issue
python utils/gitea_cli_helper.py create-issue --title "Title" --body "Description\nSupports multiple lines"
@ -116,12 +133,14 @@ python utils/gitea_cli_helper.py list-open-issues
## Mandatory Reading
At the start of each work session, read these documents to understand requirements and technical design:
- `documents/requirements/RequerimientoInicial.md`
- `documents/requirements/ToBeDesing.md`
## Code Architecture
### Module Structure
- **lims_management/models/**: Core business logic
- `partner.py`: Patient and healthcare provider management
- `product.py`: Analysis types and categories
@ -132,31 +151,37 @@ At the start of each work session, read these documents to understand requiremen
### Odoo 18 Specific Conventions
#### View Definitions
- **CRITICAL**: Use `<list>` instead of `<tree>` in view XML - using `<tree>` causes error "El nodo raíz de una vista list debe ser <list>, no <tree>"
- View mode in actions must be `tree,form` not `list,form` (paradójicamente, el modo se llama "tree" pero el XML debe usar `<list>`)
#### Visibility Attributes
- Use `invisible` attribute directly instead of `attrs`:
```xml
<!-- Wrong (Odoo < 17) -->
<field name="field" attrs="{'invisible': [('condition', '=', False)]}"/>
<!-- Correct (Odoo 18) -->
<field name="field" invisible="not condition"/>
<field name="field" invisible="condition == False"/>
```
#### Context with ref()
- Use `eval` attribute when using `ref()` in action contexts:
```xml
<!-- Wrong - ref() undefined in client -->
<field name="context">{'default_categ_id': ref('module.xml_id')}</field>
<!-- Correct - evaluated on server -->
<field name="context" eval="{'default_categ_id': ref('module.xml_id')}"/>
```
#### XPath in View Inheritance
- Use flexible XPath expressions for robustness:
```xml
<!-- More robust - works with list or tree -->
@ -166,13 +191,15 @@ At the start of each work session, read these documents to understand requiremen
```
### Data Management
- **Initial Data**: `lims_management/data/` - Sequences, categories, basic configuration
- **Demo Data**:
- **Demo Data**:
- XML files in `lims_management/demo/`
- Python scripts in `test/` directory for complex demo data creation
- Use `noupdate="1"` for demo data to prevent reloading
### Security Model
- Access rights defined in `security/ir.model.access.csv`
- Field-level security in `security/security.xml`
- Group-based permissions: Laboratory Technician, Manager, etc.
@ -180,6 +207,7 @@ At the start of each work session, read these documents to understand requiremen
## Environment Variables
Required in `.env` file:
- `GITEA_API_KEY`: Personal Access Token for Gitea
- `GITEA_API_KEY_URL`: Gitea API base URL (e.g., `https://gitea.grupoconsiti.com/api/v1/`)
- `GITEA_USERNAME`: Gitea username (repository owner)
@ -188,6 +216,7 @@ Required in `.env` file:
## Important Patterns
### Sample Lifecycle States
```python
STATE_PENDING_COLLECTION = 'pending_collection'
STATE_COLLECTED = 'collected'
@ -197,6 +226,7 @@ STATE_CANCELLED = 'cancelled'
```
### Barcode Generation
- 13-digit format: YYMMDDNNNNNNC
- Uses `barcode` Python library for Code-128 generation
- Stored as PDF with human-readable text
@ -204,30 +234,34 @@ STATE_CANCELLED = 'cancelled'
### Demo Data Creation
#### XML Files (Simple Data)
- Use for basic records without complex dependencies
- Place in `lims_management/demo/`
- Use `noupdate="1"` to prevent reloading
- **IMPORTANT**: Do NOT create sale.order records in XML demo files - use Python scripts instead
#### Python Scripts (Complex Data)
For data with dependencies or business logic:
#### Test Scripts
- **IMPORTANT**: Always create test scripts inside the `test/` folder within the project directory
- Example: `test/test_sample_generation.py`
- This ensures scripts are properly organized and accessible
1. Create script:
```python
import odoo
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Use ref() to get existing records
patient1 = env.ref('lims_management.demo_patient_1')
hemograma = env.ref('lims_management.analysis_hemograma')
# Create records with business logic
env['sale.order'].create({
'partner_id': patient1.id,
@ -251,53 +285,64 @@ if __name__ == '__main__':
## Git Workflow
### Pre-commit Hook
Automatically installed via `scripts/install_hooks.sh`:
- Prevents commits to 'main' or 'dev' branches
- Enforces feature branch workflow
### Branch Naming
- Feature branches: `feature/XX-description` (where XX is issue number)
- Always create PRs to 'dev' branch, not 'main'
## Desarrollo de nuevos modelos y vistas
### Orden de carga en __manifest__.py
### Orden de carga en **manifest**.py
Al agregar archivos al manifest, seguir SIEMPRE este orden:
1. security/*.xml (grupos y categorías)
1. security/\*.xml (grupos y categorías)
2. security/ir.model.access.csv
3. data/*.xml (secuencias, categorías, datos base)
4. views/*_views.xml en este orden específico:
3. data/\*.xml (secuencias, categorías, datos base)
4. views/\*\_views.xml en este orden específico:
- Modelos base (sin dependencias)
- Modelos dependientes
- Modelos dependientes
- Vistas que referencian acciones
- menus.xml (SIEMPRE al final de views)
5. wizards/*.xml
6. reports/*.xml
7. demo/*.xml
5. wizards/\*.xml
6. reports/\*.xml
7. demo/\*.xml
### Desarrollo de modelos relacionados
Cuando crees modelos que se relacionan entre sí en el mismo issue:
#### Fase 1: Modelos base
1. Crear modelos SIN campos One2many
2. Solo incluir campos básicos y Many2one si el modelo referenciado ya existe
3. Probar que la instancia levante
#### Fase 2: Relaciones
1. Agregar campos One2many en los modelos padre
2. Verificar que todos los inverse_name existan
3. Probar nuevamente
#### Fase 3: Vistas complejas
1. Agregar vistas con referencias a acciones
2. Verificar que las acciones referenciadas ya estén definidas
### Contextos en vistas XML
- En formularios: usar `id` (NO `active_id`)
- En acciones de ventana: usar `active_id`
- En campos One2many: usar `parent` para referenciar el registro padre
### Checklist antes de reiniciar instancia
- [ ] ¿Los modelos referenciados en relaciones ya existen?
- [ ] ¿Las acciones/vistas referenciadas se cargan ANTES?
- [ ] ¿Los grupos en ir.model.access.csv coinciden con los de security.xml?
@ -309,17 +354,20 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
### Desarrollo de vistas - Mejores prácticas
#### Antes de crear vistas:
1. **Verificar campos del modelo**: SIEMPRE revisar qué campos existen con `grep "fields\." models/archivo.py`
2. **Verificar métodos disponibles**: Buscar métodos con `grep "def action_" models/archivo.py`
3. **Verificar campos relacionados**: Confirmar que los campos related tienen la ruta correcta
#### Orden de creación de vistas:
1. **Primero**: Definir todas las acciones (ir.actions.act_window) en un solo lugar
2. **Segundo**: Crear las vistas (form, list, search, etc.)
3. **Tercero**: Crear los menús que referencian las acciones
4. **Cuarto**: Si hay referencias cruzadas entre archivos, considerar consolidar en un solo archivo
#### Widgets válidos en Odoo 18:
- Numéricos: `float`, `integer`, `monetary` (NO `float_time` para datos generales)
- Texto: `text`, `char`, `html` (NO `text_emojis`)
- Booleanos: `boolean`, `boolean_toggle`, `boolean_button`
@ -330,22 +378,27 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
#### Errores comunes y soluciones:
##### Error: "External ID not found"
- **Causa**: Referencia a un ID que aún no fue cargado
- **Solución**: Reorganizar orden en __manifest__.py o mover definición al mismo archivo
- **Solución**: Reorganizar orden en **manifest**.py o mover definición al mismo archivo
##### Error: "Field 'X' does not exist"
- **Causa**: Vista referencia campo inexistente en el modelo
- **Solución**: Verificar modelo y agregar campo o corregir nombre en vista
##### Error: "action_X is not a valid action"
- **Causa**: Nombre de método incorrecto en botón
- **Solución**: Verificar nombre exacto del método en el modelo Python
##### Error: "Invalid widget"
- **Causa**: Uso de widget no existente o deprecated
- **Solución**: Usar widgets estándar de Odoo 18
#### Estrategia de depuración:
1. Leer el error completo en los logs
2. Identificar archivo y línea exacta del problema
3. Verificar que el elemento referenciado existe y está accesible
@ -354,16 +407,18 @@ Cuando crees modelos que se relacionan entre sí en el mismo issue:
### Manejo de códigos de barras en reportes QWeb (Odoo 18)
#### Generación de códigos de barras
Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
```xml
<!-- CORRECTO en Odoo 18 -->
<span t-field="record.barcode_field"
<span t-field="record.barcode_field"
t-options="{'widget': 'barcode', 'type': 'Code128', 'width': 250, 'height': 60, 'humanreadable': 1}"
style="display: block;"/>
```
#### Consideraciones importantes:
1. **NO usar** rutas directas como `/report/barcode/Code128/` - esta sintaxis está deprecated
2. **Usar siempre** `t-field` con el widget barcode para renderizado correcto
3. **Parámetros disponibles** en t-options:
@ -375,19 +430,23 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
#### Problemas comunes y soluciones:
##### Código de barras vacío en PDF
- **Causa**: Campo computed sin store=True o sintaxis incorrecta
- **Solución**: Asegurar que el campo esté almacenado y usar widget barcode
##### Caracteres especiales en reportes (tildes, ñ)
- **Problema**: Aparecen como "ñ" o "í" en lugar de "ñ" o "í"
- **Solución**: Usar referencias numéricas de caracteres XML:
```xml
<!-- En lugar de -->
<h4>LABORATORIO CLÍNICO</h4>
<!-- Usar -->
<h4>LABORATORIO CL&#205;NICO</h4>
```
- í = &#237;
- Í = &#205;
- á = &#225;
@ -402,15 +461,16 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
- Ñ = &#209;
##### Layout de etiquetas múltiples por página
```xml
<!-- Contenedor principal sin salto de página -->
<div class="page">
<t t-foreach="docs" t-as="o">
<!-- Cada etiqueta como inline-block -->
<div style="display: inline-block; vertical-align: top;
<div style="display: inline-block; vertical-align: top;
page-break-inside: avoid; overflow: hidden;">
<!-- Contenido de la etiqueta -->
</div>
</t>
</div>
```
```

78
dashboard_analysis.md Normal file
View File

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

View File

@ -45,6 +45,7 @@
'views/analysis_parameter_views.xml',
'views/product_template_parameter_config_views.xml',
'views/parameter_dashboard_views.xml',
'views/dashboard_views.xml',
'views/menus.xml',
'views/lims_config_views.xml',
'report/sample_label_report.xml',

View File

@ -116,6 +116,21 @@ class LimsTest(models.Model):
default=lambda self: self.env.company
)
# Campos para dashboards demográficos
patient_gender = fields.Selection(
related='patient_id.gender',
string='Género del Paciente',
store=True,
readonly=True
)
patient_age_range = fields.Selection(
related='patient_id.age_range',
string='Rango de Edad',
store=True,
readonly=True
)
@api.depends('company_id')
def _compute_require_validation(self):
"""Calcula si la prueba requiere validación basado en configuración."""

View File

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

View File

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

View File

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

View File

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

View File

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

45
test/test_notification.py Normal file
View File

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