fix(#71): Corregir errores en dashboards y scripts de inicialización

- Cambiar 'tree' por 'list' en view_mode de todas las acciones de dashboard
- Corregir sintaxis de filtros de fecha usando context_today() y relativedelta
- Eliminar campo booleano is_out_of_range como medida en gráfico
- Corregir referencia a sample.state en lugar de sample.sample_state
- Reemplazar sample.test_ids por búsqueda de tests asociados
- Eliminar consulta SQL directa a columna logo inexistente
- Corregir método invalidate_cache() por _invalidate_cache()
- Agregar sección de notificaciones en CLAUDE.md

Los dashboards ahora funcionan correctamente sin errores de JavaScript.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-18 12:11:01 -06:00
parent 753b84936e
commit 02237c6d8c
6 changed files with 149 additions and 50 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>
```
```

View File

@ -31,7 +31,7 @@
<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,tree,form</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),
@ -79,7 +79,7 @@
<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,tree,form</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')}),
@ -125,7 +125,7 @@
<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,tree,form</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),
@ -152,7 +152,6 @@
<field name="arch" type="xml">
<graph string="Par&#225;metros Fuera de Rango" type="bar">
<field name="parameter_id"/>
<field name="is_out_of_range" type="measure"/>
</graph>
</field>
</record>
@ -174,7 +173,7 @@
<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,tree,form</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),
@ -223,7 +222,7 @@
<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,tree</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),
@ -270,7 +269,7 @@
<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,tree</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),
@ -301,10 +300,10 @@
<filter string="Validados" name="validated" domain="[('state', '=', 'validated')]"/>
<!-- Filtros de Tiempo -->
<filter string="Hoy" name="today" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(hour=0, minute=0, second=0))]"/>
<filter string="Esta Semana" name="this_week" domain="[('create_date', '&gt;=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
<filter string="Este Mes" name="this_month" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(day=1).strftime('%Y-%m-%d'))]"/>
<filter string="Este A&#241;o" name="this_year" domain="[('create_date', '&gt;=', datetime.datetime.now().replace(month=1, day=1).strftime('%Y-%m-%d'))]"/>
<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">

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