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:
commit
cf1e40726f
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
96
CLAUDE.md
96
CLAUDE.md
|
@ -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ÍNICO</h4>
|
<h4>LABORATORIO CLÍNICO</h4>
|
||||||
```
|
```
|
||||||
|
|
||||||
- í = í
|
- í = í
|
||||||
- Í = Í
|
- Í = Í
|
||||||
- á = á
|
- á = á
|
||||||
|
@ -402,15 +461,16 @@ Para mostrar códigos de barras en reportes PDF, usar el widget nativo de Odoo:
|
||||||
- Ñ = Ñ
|
- Ñ = Ñ
|
||||||
|
|
||||||
##### 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
78
dashboard_analysis.md
Normal 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
|
|
@ -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',
|
||||||
|
|
Binary file not shown.
|
@ -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."""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
338
lims_management/views/dashboard_views.xml
Normal file
338
lims_management/views/dashboard_views.xml
Normal 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 Órdenes" type="pie">
|
||||||
|
<field name="state"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Estado de Órdenes -->
|
||||||
|
<record id="view_lab_order_dashboard_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.lab.dashboard.pivot</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Análisis de Órdenes">
|
||||||
|
<field name="date_order" interval="month" type="col"/>
|
||||||
|
<field name="state" type="row"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Estado de Órdenes -->
|
||||||
|
<record id="action_lab_order_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Estado de Órdenes</field>
|
||||||
|
<field name="res_model">sale.order</field>
|
||||||
|
<field name="view_mode">graph,pivot,list,form</field>
|
||||||
|
<field name="domain">[('is_lab_request', '=', True)]</field>
|
||||||
|
<field name="context">{'search_default_group_by_state': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_lab_order_dashboard_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_lab_order_dashboard_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay órdenes de laboratorio registradas
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra el estado actual de todas las órdenes de laboratorio.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
DASHBOARD 2: Productividad de Técnicos
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Vista Graph para Productividad de Técnicos -->
|
||||||
|
<record id="view_test_technician_productivity_graph" model="ir.ui.view">
|
||||||
|
<field name="name">lims.test.technician.productivity.graph</field>
|
||||||
|
<field name="model">lims.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Productividad de Técnicos" type="bar">
|
||||||
|
<field name="technician_id"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Productividad de Técnicos -->
|
||||||
|
<record id="view_test_technician_productivity_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">lims.test.technician.productivity.pivot</field>
|
||||||
|
<field name="model">lims.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Análisis por Técnico">
|
||||||
|
<field name="technician_id" type="row"/>
|
||||||
|
<field name="state" type="col"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Productividad de Técnicos -->
|
||||||
|
<record id="action_technician_productivity_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Productividad de Técnicos</field>
|
||||||
|
<field name="res_model">lims.test</field>
|
||||||
|
<field name="view_mode">graph,pivot,list,form</field>
|
||||||
|
<field name="context">{'search_default_group_by_technician': 1, 'search_default_this_month': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_technician_productivity_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_technician_productivity_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay pruebas registradas
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra la productividad de cada técnico del laboratorio.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
DASHBOARD 3: Estado de Muestras
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Vista Graph para Estado de Muestras -->
|
||||||
|
<record id="view_sample_status_graph" model="ir.ui.view">
|
||||||
|
<field name="name">stock.lot.sample.status.graph</field>
|
||||||
|
<field name="model">stock.lot</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Estado de Muestras" type="pie">
|
||||||
|
<field name="state"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Muestras por Tipo -->
|
||||||
|
<record id="view_sample_type_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">stock.lot.sample.type.pivot</field>
|
||||||
|
<field name="model">stock.lot</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Muestras por Tipo">
|
||||||
|
<field name="sample_type_product_id" type="row"/>
|
||||||
|
<field name="state" type="col"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Muestras -->
|
||||||
|
<record id="action_sample_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Dashboard de Muestras</field>
|
||||||
|
<field name="res_model">stock.lot</field>
|
||||||
|
<field name="view_mode">graph,pivot,list,form</field>
|
||||||
|
<field name="domain">[('is_lab_sample', '=', True)]</field>
|
||||||
|
<field name="context">{'search_default_group_by_state': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_sample_status_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_sample_type_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay muestras registradas
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra el estado de todas las muestras del laboratorio.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
DASHBOARD 4: Parámetros Fuera de Rango
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Vista Graph para Parámetros Fuera de Rango -->
|
||||||
|
<record id="view_result_out_of_range_graph" model="ir.ui.view">
|
||||||
|
<field name="name">lims.result.out.of.range.graph</field>
|
||||||
|
<field name="model">lims.result</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Parámetros Fuera de Rango" type="bar">
|
||||||
|
<field name="parameter_id"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Resultados Críticos -->
|
||||||
|
<record id="view_result_critical_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">lims.result.critical.pivot</field>
|
||||||
|
<field name="model">lims.result</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Resultados Críticos">
|
||||||
|
<field name="parameter_id" type="row"/>
|
||||||
|
<field name="is_critical" type="col"/>
|
||||||
|
<field name="is_out_of_range" type="col"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Parámetros Fuera de Rango -->
|
||||||
|
<record id="action_out_of_range_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Parámetros Fuera de Rango</field>
|
||||||
|
<field name="res_model">lims.result</field>
|
||||||
|
<field name="view_mode">graph,pivot,list,form</field>
|
||||||
|
<field name="domain">[('test_id.state', '=', 'validated')]</field>
|
||||||
|
<field name="context">{'search_default_out_of_range': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_result_out_of_range_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_result_critical_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay resultados fuera de rango
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra los parámetros que están fuera de los rangos normales.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
DASHBOARD 5: Análisis Más Solicitados
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Vista Graph para Top Análisis -->
|
||||||
|
<record id="view_top_analysis_graph" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.line.top.analysis.graph</field>
|
||||||
|
<field name="model">sale.order.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Análisis Más Solicitados" type="bar">
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="product_uom_qty" type="measure"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Análisis por Período -->
|
||||||
|
<record id="view_analysis_period_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.line.analysis.period.pivot</field>
|
||||||
|
<field name="model">sale.order.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Análisis por Período">
|
||||||
|
<field name="create_date" interval="month" type="col"/>
|
||||||
|
<field name="product_id" type="row"/>
|
||||||
|
<field name="product_uom_qty" type="measure"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Análisis Más Solicitados -->
|
||||||
|
<record id="action_top_analysis_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Análisis Más Solicitados</field>
|
||||||
|
<field name="res_model">sale.order.line</field>
|
||||||
|
<field name="view_mode">graph,pivot,list</field>
|
||||||
|
<field name="domain">[('order_id.is_lab_request', '=', True), ('product_id.is_analysis', '=', True)]</field>
|
||||||
|
<field name="context">{'search_default_group_by_product': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_top_analysis_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_analysis_period_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay análisis registrados
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra los análisis más solicitados en el laboratorio.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
DASHBOARD 6: Distribución de Tests por Demografía
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Vista Graph para Distribución por Sexo -->
|
||||||
|
<record id="view_test_gender_distribution_graph" model="ir.ui.view">
|
||||||
|
<field name="name">lims.test.gender.distribution.graph</field>
|
||||||
|
<field name="model">lims.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Distribución por Género" type="pie">
|
||||||
|
<field name="patient_gender"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Vista Pivot para Tests por Edad y Sexo -->
|
||||||
|
<record id="view_test_demographics_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">lims.test.demographics.pivot</field>
|
||||||
|
<field name="model">lims.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Tests por Demografía">
|
||||||
|
<field name="patient_age_range" type="row"/>
|
||||||
|
<field name="patient_gender" type="col"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Acción para Dashboard de Distribución Demográfica -->
|
||||||
|
<record id="action_test_demographics_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Distribución Demográfica de Tests</field>
|
||||||
|
<field name="res_model">lims.test</field>
|
||||||
|
<field name="view_mode">graph,pivot,list</field>
|
||||||
|
<field name="domain">[('state', '=', 'validated')]</field>
|
||||||
|
<field name="context">{'search_default_this_year': 1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_gender_distribution_graph')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_demographics_pivot')})]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No hay tests validados
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este dashboard muestra la distribución de tests por características demográficas de los pacientes.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
FILTROS DE BÚSQUEDA PARA DASHBOARDS
|
||||||
|
================================================================ -->
|
||||||
|
|
||||||
|
<!-- Filtros para Tests -->
|
||||||
|
<record id="view_lims_test_dashboard_search" model="ir.ui.view">
|
||||||
|
<field name="name">lims.test.dashboard.search</field>
|
||||||
|
<field name="model">lims.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<!-- Filtros de Estado -->
|
||||||
|
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
||||||
|
<filter string="Validados" name="validated" domain="[('state', '=', 'validated')]"/>
|
||||||
|
|
||||||
|
<!-- Filtros de Tiempo -->
|
||||||
|
<filter string="Hoy" name="today" domain="[('create_date', '>=', context_today())]"/>
|
||||||
|
<filter string="Esta Semana" name="this_week" domain="[('create_date', '>=', (context_today() + relativedelta(days=-7)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
<filter string="Este Mes" name="this_month" domain="[('create_date', '>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
<filter string="Este Año" name="this_year" domain="[('create_date', '>=', (context_today() + relativedelta(month=1, day=1)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
|
||||||
|
<!-- Agrupaciones -->
|
||||||
|
<group expand="0" string="Agrupar Por">
|
||||||
|
<filter string="Técnico" name="group_by_technician" context="{'group_by': 'technician_id'}"/>
|
||||||
|
<filter string="Estado" name="group_by_state" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Paciente" name="group_by_patient" context="{'group_by': 'patient_id'}"/>
|
||||||
|
<filter string="Análisis" name="group_by_product" context="{'group_by': 'product_id'}"/>
|
||||||
|
<filter string="Fecha" name="group_by_date" context="{'group_by': 'create_date:month'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Filtros para Resultados -->
|
||||||
|
<record id="view_lims_result_dashboard_search" model="ir.ui.view">
|
||||||
|
<field name="name">lims.result.dashboard.search</field>
|
||||||
|
<field name="model">lims.result</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<!-- Filtros de Rango -->
|
||||||
|
<filter string="Fuera de Rango" name="out_of_range" domain="[('is_out_of_range', '=', True)]"/>
|
||||||
|
<filter string="Críticos" name="critical" domain="[('is_critical', '=', True)]"/>
|
||||||
|
|
||||||
|
<!-- Agrupaciones -->
|
||||||
|
<group expand="0" string="Agrupar Por">
|
||||||
|
<filter string="Parámetro" name="group_by_parameter" context="{'group_by': 'parameter_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -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 Órdenes"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_lab_order_dashboard"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_technician_productivity_dashboard"
|
||||||
|
name="Productividad de Técnicos"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_technician_productivity_dashboard"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_sample_dashboard"
|
||||||
|
name="Dashboard de Muestras"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_sample_dashboard"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_out_of_range_dashboard"
|
||||||
|
name="Parámetros Fuera de Rango"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_out_of_range_dashboard"
|
||||||
|
sequence="40"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_top_analysis_dashboard"
|
||||||
|
name="Análisis Más Solicitados"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_top_analysis_dashboard"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_test_demographics_dashboard"
|
||||||
|
name="Distribución Demográfica"
|
||||||
|
parent="menu_lims_dashboards"
|
||||||
|
action="action_test_demographics_dashboard"
|
||||||
|
sequence="60"/>
|
||||||
|
|
||||||
<!-- Submenú de Reportes -->
|
<!-- Submenú de Reportes -->
|
||||||
<menuitem
|
<menuitem
|
||||||
id="lims_menu_reports"
|
id="lims_menu_reports"
|
||||||
|
|
|
@ -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:")
|
||||||
|
|
|
@ -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
45
test/test_notification.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user