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(gh pr merge:*)",
"Bash(git cherry-pick:*)", "Bash(git cherry-pick:*)",
"Bash(del comment_issue_15.txt)", "Bash(del comment_issue_15.txt)",
"Bash(cat:*)" "Bash(cat:*)",
"Bash(powershell.exe:*)"
], ],
"deny": [] "deny": []
} }

View File

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

View File

@ -116,6 +116,21 @@ class LimsTest(models.Model):
default=lambda self: self.env.company 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') @api.depends('company_id')
def _compute_require_validation(self): def _compute_require_validation(self):
"""Calcula si la prueba requiere validación basado en configuración.""" """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" 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( is_pregnant = fields.Boolean(
string="Embarazada", string="Embarazada",
help="Marcar si la paciente está embarazada (solo aplica para género femenino)" help="Marcar si la paciente está embarazada (solo aplica para género femenino)"
@ -54,6 +65,34 @@ class ResPartner(models.Model):
else: else:
partner.age = 0 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') @api.constrains('is_pregnant', 'gender')
def _check_pregnant_gender(self): def _check_pregnant_gender(self):
"""Valida que solo pacientes de género femenino puedan estar embarazadas""" """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" action="action_lims_result"
sequence="30"/> 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 --> <!-- Submenú de Reportes -->
<menuitem <menuitem
id="lims_menu_reports" id="lims_menu_reports"

View File

@ -63,28 +63,21 @@ try:
env.cr.rollback() env.cr.rollback()
continue continue
# Verificación final con consulta directa a la BD # Verificación final usando el ORM
print("\n" + "="*60) print("\n" + "="*60)
print("VERIFICACIÓN FINAL (consulta directa):") print("VERIFICACIÓN FINAL:")
print("="*60) print("="*60)
env.cr.execute(""" # Verificar a través del ORM que es más seguro
SELECT id, name, for company in env['res.company'].search([], order='id'):
CASE WHEN logo IS NOT NULL THEN 'SI' ELSE 'NO' END as tiene_logo, print(f"\nEmpresa ID {company.id}:")
CASE WHEN logo IS NOT NULL THEN length(logo) ELSE 0 END as logo_size print(f" - Nombre: {company.name}")
FROM res_company print(f" - Logo presente: {'SI' if company.logo else 'NO'}")
ORDER BY id if company.logo:
""") print(f" - Tamaño del logo (base64): {len(company.logo):,} caracteres")
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é # Forzar actualización de caché
env['res.company'].invalidate_cache() env['res.company']._invalidate_cache()
print("\nLogo de la empresa actualizado exitosamente.") print("\nLogo de la empresa actualizado exitosamente.")
print("NOTA: Si el logo no aparece en la interfaz, puede ser necesario:") 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 # Procesar cada muestra
for sample in order.generated_sample_ids: for sample in order.generated_sample_ids:
# Marcar como recolectada # Marcar como recolectada
if sample.sample_state == 'pending_collection': if sample.state == 'pending_collection':
sample.action_collect() sample.action_collect()
print(f" ✓ Muestra {sample.name} recolectada") print(f" ✓ Muestra {sample.name} recolectada")
# Procesar pruebas de esta muestra # 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}") print(f" - Procesando prueba: {test.product_id.name}")
# Iniciar proceso si está en borrador # Iniciar proceso si está en borrador
@ -241,7 +242,7 @@ def create_demo_lab_data(cr):
print(f" ✓ Resultados ingresados") print(f" ✓ Resultados ingresados")
# Validar las primeras 2 órdenes completas y algunas pruebas de la tercera # 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': if should_validate and test.state == 'result_entered':
test.action_validate() 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()