Compare commits
No commits in common. "dev" and "feature/5-analysis-catalog" have entirely different histories.
dev
...
feature/5-
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python:*)",
|
||||
"Bash(tea issue:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(docker-compose up:*)",
|
||||
"Bash(docker:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(move lab_logo.png lims_management/static/img/lab_logo.png)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:apps.odoo.com)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(true)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(gh pr merge:*)",
|
||||
"Bash(git cherry-pick:*)",
|
||||
"Bash(del comment_issue_15.txt)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(gh pr create:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
5
.env
|
@ -9,8 +9,3 @@ POSTGRES_PASSWORD=supersegura
|
|||
ODOO_DB_NAME=lims_demo
|
||||
ODOO_MASTER_PASSWORD=admin
|
||||
ODOO_WEB_PORT=8069
|
||||
|
||||
GITEA_API_KEY=1ad57b1e553ee0b092d51f061e78c5c3df9f8107
|
||||
GITEA_API_KEY_URL=https://gitea.grupoconsiti.com/api/v1/
|
||||
GITEA_USERNAME=luis_portillo
|
||||
GITEA_REPO_NAME=clinical_laboratory
|
BIN
.gitignore
vendored
476
CLAUDE.md
|
@ -1,476 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
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.
|
||||
|
||||
## Key Technologies
|
||||
|
||||
- **Odoo 18**: ERP framework (Python-based)
|
||||
- **PostgreSQL 15**: Database
|
||||
- **Docker & Docker Compose**: Containerization
|
||||
- **Gitea**: Version control and issue tracking
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Starting the Environment
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose up -d
|
||||
|
||||
# MANDATORY: View initialization logs to check for errors
|
||||
docker-compose logs odoo_init
|
||||
|
||||
# Stop and clean everything (removes volumes)
|
||||
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`
|
||||
4. Only proceed to next task if no errors are found
|
||||
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)
|
||||
4. **Validate logs**: Check for errors in initialization
|
||||
5. **Commit & Push**: `git add -A && git commit -m "feat(#X): Task description" && git push`
|
||||
6. **Comment on issue**: Update issue with task completion
|
||||
7. **Mark task completed**: Update todo list
|
||||
8. **Proceed to next task**: Only if no errors found
|
||||
|
||||
### 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
|
||||
|
||||
def verify_lab_order_products(cr):
|
||||
cr.execute("""SELECT ... FROM sale_order ...""")
|
||||
return cr.fetchall()
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
results = verify_lab_order_products(cr)
|
||||
print(json.dumps(results, indent=4))
|
||||
```
|
||||
|
||||
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"
|
||||
|
||||
# Create PR with inline description
|
||||
python utils/gitea_cli_helper.py create-pr --head "feature-branch" --base "dev" --title "Title" --body "Description"
|
||||
|
||||
# Create PR with description from file
|
||||
python utils/gitea_cli_helper.py create-pr dev --title "feat(#31): Sample lifecycle" --description-file pr_description.txt
|
||||
|
||||
# Comment on issue
|
||||
python utils/gitea_cli_helper.py comment-issue --issue-number 123 --body "Comment text"
|
||||
|
||||
# Close issue
|
||||
python utils/gitea_cli_helper.py close-issue --issue-number 123
|
||||
|
||||
# Get issue details and comments
|
||||
python utils/gitea_cli_helper.py get-issue --issue-number 8
|
||||
|
||||
# List all open issues
|
||||
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
|
||||
- `sale_order.py`: Analysis orders and sample management
|
||||
- `stock_lot.py`: Sample tracking and lifecycle
|
||||
- `analysis_range.py`: Normal ranges for test results
|
||||
|
||||
### 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 -->
|
||||
<xpath expr="//field[@name='order_line']//field[@name='product_id']" position="attributes">
|
||||
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
|
||||
</xpath>
|
||||
```
|
||||
|
||||
### Data Management
|
||||
|
||||
- **Initial Data**: `lims_management/data/` - Sequences, categories, basic configuration
|
||||
- **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.
|
||||
|
||||
## 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)
|
||||
- `GITEA_REPO_NAME`: Repository name (e.g., `clinical_laboratory`)
|
||||
|
||||
## Important Patterns
|
||||
|
||||
### Sample Lifecycle States
|
||||
|
||||
```python
|
||||
STATE_PENDING_COLLECTION = 'pending_collection'
|
||||
STATE_COLLECTED = 'collected'
|
||||
STATE_IN_ANALYSIS = 'in_analysis'
|
||||
STATE_COMPLETED = 'completed'
|
||||
STATE_CANCELLED = 'cancelled'
|
||||
```
|
||||
|
||||
### Barcode Generation
|
||||
|
||||
- 13-digit format: YYMMDDNNNNNNC
|
||||
- Uses `barcode` Python library for Code-128 generation
|
||||
- Stored as PDF with human-readable text
|
||||
|
||||
### 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,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': hemograma.product_variant_id.id,
|
||||
'product_uom_qty': 1
|
||||
})]
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
create_lab_requests(cr)
|
||||
cr.commit()
|
||||
```
|
||||
|
||||
2. Integrate in initialization or run separately
|
||||
|
||||
## 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
|
||||
|
||||
Al agregar archivos al manifest, seguir SIEMPRE este orden:
|
||||
|
||||
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:
|
||||
- Modelos base (sin dependencias)
|
||||
- Modelos dependientes
|
||||
- Vistas que referencian acciones
|
||||
- menus.xml (SIEMPRE al final de views)
|
||||
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?
|
||||
- [ ] ¿Usaste `id` en lugar de `active_id` en contextos de formulario?
|
||||
- [ ] ¿Verificaste que todos los campos en las vistas existen en los modelos?
|
||||
- [ ] ¿Los nombres de métodos/acciones coinciden exactamente con los definidos en Python?
|
||||
- [ ] ¿Los widgets utilizados son válidos en Odoo 18?
|
||||
|
||||
### 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`
|
||||
- Selección: `selection`, `radio`, `selection_badge`
|
||||
- Relaciones: `many2one`, `many2many_tags`
|
||||
- Estado: `statusbar`, `badge`, `progressbar`
|
||||
|
||||
#### 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
|
||||
|
||||
##### 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
|
||||
4. Si es necesario, simplificar la vista temporalmente para aislar el problema
|
||||
|
||||
### 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"
|
||||
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:
|
||||
- `type`: Tipo de código ('Code128', 'EAN13', 'QR', etc.)
|
||||
- `width`: Ancho en píxeles
|
||||
- `height`: Alto en píxeles
|
||||
- `humanreadable`: 1 para mostrar texto legible, 0 para ocultarlo
|
||||
|
||||
#### 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ÍNICO</h4>
|
||||
```
|
||||
|
||||
- í = í
|
||||
- Í = Í
|
||||
- á = á
|
||||
- Á = Á
|
||||
- é = é
|
||||
- É = É
|
||||
- ó = ó
|
||||
- Ó = Ó
|
||||
- ú = ú
|
||||
- Ú = Ú
|
||||
- ñ = ñ
|
||||
- Ñ = Ñ
|
||||
|
||||
##### 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;
|
||||
page-break-inside: avoid; overflow: hidden;">
|
||||
<!-- Contenido de la etiqueta -->
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
```
|
373
GEMINI.md
|
@ -2,124 +2,106 @@
|
|||
|
||||
Este proyecto utiliza `tea` para interactuar con el repositorio de Gitea.
|
||||
|
||||
## Gestión de Gitea con `gitea_cli_helper.py`
|
||||
## Crear un Issue (Modo no Interactivo)
|
||||
|
||||
Para interactuar con el repositorio de Gitea (crear issues, pull requests, comentar y cerrar issues) de forma robusta y con soporte para contenido multilínea, se recomienda utilizar el script de Python `gitea_cli_helper.py`. Este script lee la configuración sensible directamente desde el archivo `.env`.
|
||||
|
||||
### Configuración
|
||||
|
||||
Asegúrate de que las siguientes variables estén definidas en tu archivo `.env`:
|
||||
|
||||
- `GITEA_API_KEY`: Tu Token de Acceso Personal (PAT) de Gitea.
|
||||
- `GITEA_API_KEY_URL`: La URL base de la API de tu instancia de Gitea (ej. `https://gitea.grupoconsiti.com/api/v1/`).
|
||||
- `GITEA_USERNAME`: Tu nombre de usuario de Gitea (propietario del repositorio).
|
||||
- `GITEA_REPO_NAME`: El nombre del repositorio (ej. `clinical_laboratory`).
|
||||
|
||||
### Uso
|
||||
|
||||
El script `gitea_cli_helper.py` utiliza `argparse` para diferentes comandos:
|
||||
|
||||
**IMPORTANTE**: Los archivos descriptivos (como `pr_description.txt`) creados para usar con el helper de Gitea:
|
||||
- Pueden crearse dentro del proyecto temporalmente
|
||||
- NO deben versionarse en git
|
||||
- Deben eliminarse después de ser utilizados por el helper
|
||||
- Se recomienda usar nombres descriptivos que faciliten su identificación para eliminación posterior
|
||||
|
||||
#### 1. Crear un Issue
|
||||
Para crear un nuevo issue de forma no interactiva, se utiliza el siguiente comando, proporcionando todos los datos necesarios mediante flags:
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py create-issue --title "Título del Issue" --body "Descripción detallada del issue.\nSoporta múltiples líneas."
|
||||
tea issue create --title "Título del Issue" --description "Descripción detallada del issue." --labels "etiqueta1,etiqueta2"
|
||||
```
|
||||
|
||||
- `--title`: Título del issue.
|
||||
- `--body`: Cuerpo o descripción del issue. Los saltos de línea (`\n`) se interpretarán correctamente.
|
||||
|
||||
#### 2. Crear un Pull Request
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py create-pr --head "tu-rama" --base "rama-destino" --title "Título del PR" --body "Descripción del Pull Request.\nSoporta múltiples líneas."
|
||||
```
|
||||
|
||||
- `--head`: Rama de origen (tu rama actual).
|
||||
- `--base`: Rama de destino (ej. `dev`, `main`).
|
||||
- `--title`: Título del Pull Request.
|
||||
- `--body`: Cuerpo o descripción del Pull Request.
|
||||
|
||||
#### 3. Comentar en un Issue
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py comment-issue --issue-number 123 --body "Este es un nuevo comentario.\nTambién soporta múltiples líneas."
|
||||
```
|
||||
|
||||
- `--issue-number`: Número del issue al que se desea añadir el comentario.
|
||||
- `--body`: Contenido del comentario.
|
||||
|
||||
#### 4. Cerrar un Issue
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py close-issue --issue-number 123
|
||||
```
|
||||
|
||||
- `--issue-number`: Número del issue a cerrar.
|
||||
|
||||
#### 5. Hacer Merge de un Pull Request
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py merge-pr --pr-number 46 --merge-method merge
|
||||
```
|
||||
|
||||
- `--pr-number`: Número del Pull Request a mergear.
|
||||
- `--merge-method`: Método de merge a utilizar. Opciones disponibles: `merge` (default), `squash`, `rebase`.
|
||||
|
||||
**IMPORTANTE**: Solo se permite hacer merge a la rama `dev`. El script validará automáticamente que el PR tenga como destino la rama `dev` antes de proceder. Si el PR apunta a otra rama (como `main`), el merge será rechazado con un mensaje de error.
|
||||
|
||||
**Ejemplo de uso:**
|
||||
```bash
|
||||
# Merge estándar (commit de merge)
|
||||
python gitea_cli_helper.py merge-pr --pr-number 46
|
||||
|
||||
# Merge con squash (un solo commit con todos los cambios)
|
||||
python gitea_cli_helper.py merge-pr --pr-number 46 --merge-method squash
|
||||
|
||||
# Merge con rebase (aplica commits individualmente sobre la rama base)
|
||||
python gitea_cli_helper.py merge-pr --pr-number 46 --merge-method rebase
|
||||
```
|
||||
|
||||
El script también verifica:
|
||||
- Si el PR ya fue mergeado (mostrará mensaje informativo)
|
||||
- Si el PR está cerrado sin mergear (error)
|
||||
- Si el PR tiene conflictos o no es mergeable (error)
|
||||
|
||||
#### 6. Listar Issues Abiertos
|
||||
|
||||
```bash
|
||||
python gitea_cli_helper.py list-open-issues
|
||||
```
|
||||
|
||||
Lista todos los issues abiertos del repositorio, mostrando:
|
||||
- Número del issue
|
||||
- Título
|
||||
- Etiquetas (si las tiene)
|
||||
- Autor y fecha de creación
|
||||
- URL del issue
|
||||
|
||||
**Ejemplo de salida:**
|
||||
```
|
||||
Issues abiertos (3):
|
||||
--------------------------------------------------------------------------------
|
||||
#15: [Extensión Opcional] Integración con Calendario para Citas
|
||||
Autor: luis_portillo | Creado: 2025-07-12
|
||||
URL: https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory/issues/15
|
||||
|
||||
#14: [Extensión Opcional] Portal Web para Pacientes/Médicos
|
||||
Autor: luis_portillo | Creado: 2025-07-12
|
||||
URL: https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory/issues/14
|
||||
--------------------------------------------------------------------------------
|
||||
Total: 2 issues abiertos
|
||||
```
|
||||
- `--title`: Especifica el título del issue.
|
||||
- `--description`: Especifica la descripción o cuerpo del issue.
|
||||
- `--labels`: Especifica una o más etiquetas separadas por comas.
|
||||
|
||||
---
|
||||
|
||||
## Comentar en un Issue
|
||||
|
||||
Para agregar un comentario a un issue existente, se utiliza el comando `comment` seguido del número del issue y el texto del comentario entre comillas.
|
||||
|
||||
**Formato correcto:**
|
||||
|
||||
```bash
|
||||
tea comment <NÚMERO_ISSUE> "Tu comentario aquí"
|
||||
```
|
||||
|
||||
**Ejemplo:**
|
||||
|
||||
```bash
|
||||
tea comment 3 "Comentario de prueba"
|
||||
```
|
||||
|
||||
**Nota:** No se deben utilizar flags como `-i` o `--message`. El formato es directo.
|
||||
|
||||
---
|
||||
|
||||
## Realizar Commits
|
||||
|
||||
Debido a problemas de interpretación de comillas en el shell de ejecución, el uso de `git commit -m "mensaje"` puede fallar. Para evitar estos problemas, se debe pasar el mensaje del commit a través de la entrada estándar (`stdin`).
|
||||
|
||||
### Política de Mensajes de Commit
|
||||
|
||||
**Es mandatorio que el título de cada commit referencie el número del issue que resuelve.** Esto se hace para mantener una trazabilidad clara entre el código y las tareas.
|
||||
|
||||
**Formato del Título:**
|
||||
```
|
||||
<tipo>(#<issue_id>): <descripción breve>
|
||||
```
|
||||
- **`<tipo>`:** `feat` (nueva funcionalidad), `fix` (corrección de bug), `docs` (cambios en documentación), `style` (formato), `refactor`, `test`, `chore` (otras tareas).
|
||||
- **`(<issue_id>)`:** El número del issue entre paréntesis y precedido de `#`.
|
||||
|
||||
**Ejemplo:**
|
||||
```
|
||||
feat(#4): Agregar campos de género y fecha de nacimiento al paciente
|
||||
```
|
||||
|
||||
### Método Recomendado
|
||||
|
||||
Utiliza el comando `echo` y una tubería (`|`) para enviar el mensaje a `git commit -F -`.
|
||||
|
||||
**Commit de una sola línea:**
|
||||
|
||||
```bash
|
||||
echo "feat(#4): Tu mensaje de commit conciso" | git commit -F -
|
||||
```
|
||||
|
||||
**Commit multilínea:**
|
||||
Para mensajes de commit multilínea, la forma más segura es usar `printf` que maneja mejor los saltos de línea (`
|
||||
`):
|
||||
|
||||
```bash
|
||||
printf "feat(#4): Título del commit
|
||||
|
||||
Cuerpo del mensaje con descripción detallada." | git commit -F -
|
||||
```
|
||||
|
||||
Esto asegura que el formato del mensaje del commit se preserve correctamente.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Crear un Pull Request
|
||||
|
||||
Para crear un pull request (PR), se utiliza el comando `tea pulls create`. Debes especificar la rama base (hacia donde van los cambios) y la rama `head` (tu rama actual), junto con un título que referencie el issue que resuelve.
|
||||
|
||||
**Formato del comando:**
|
||||
|
||||
```bash
|
||||
tea pulls create --base "<rama_base>" --head "<tu_rama>" --title "<Tipo>(#issue): Título descriptivo"
|
||||
```
|
||||
|
||||
**Ejemplo:**
|
||||
|
||||
```bash
|
||||
tea pulls create --base "dev" --head "feature/3-core-setup" --title "feat(#3): Actualiza instrucciones en GEMINI.md"
|
||||
```
|
||||
|
||||
- `--base`: La rama de destino (ej. `dev`, `main`).
|
||||
- `--head`: Tu rama de trabajo actual.
|
||||
- `--title`: Un título claro que incluya el tipo de cambio (`feat`, `fix`, `docs`) y el número de issue.
|
||||
|
||||
---
|
||||
|
||||
## Contexto del Proyecto
|
||||
|
||||
|
@ -258,175 +240,4 @@ Esto envía la cadena `"{'default_categ_id': ref(...)}"` al cliente, que no pued
|
|||
}"/>
|
||||
```
|
||||
Al usar `eval`, Odoo ejecuta la expresión en el servidor, reemplaza `ref(...)` por el ID numérico correspondiente, y envía un diccionario JSON válido al cliente.
|
||||
|
||||
### Herencia de Vistas y XPath
|
||||
|
||||
Al heredar vistas para modificarlas, es crucial que las expresiones `XPath` sean precisas. Un error común es hacer referencia a campos o estructuras que han cambiado en la nueva versión de Odoo.
|
||||
|
||||
**Ejemplo de Error:**
|
||||
Al intentar modificar las líneas de una orden de venta (`sale.order`), una expresión XPath que funcionaba en versiones anteriores puede fallar en Odoo 18.
|
||||
|
||||
**Expresión Incorrecta (para Odoo < 18):**
|
||||
```xml
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="attributes">
|
||||
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
|
||||
</xpath>
|
||||
```
|
||||
Esta expresión falla por dos razones:
|
||||
1. La vista de líneas ahora usa `<list>` en lugar de `<tree>`.
|
||||
2. El campo del producto en las líneas de venta es `product_id`, no `product_template_id`.
|
||||
|
||||
**Expresión Correcta (para Odoo 18):**
|
||||
Para hacer la expresión más robusta y compatible, se puede usar `//` para buscar en cualquier nivel descendiente.
|
||||
```xml
|
||||
<xpath expr="//field[@name='order_line']//field[@name='product_id']" position="attributes">
|
||||
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
|
||||
</xpath>
|
||||
```
|
||||
Esta expresión busca el campo `product_id` dentro del campo `order_line`, sin importar si está dentro de una etiqueta `<list>` o `<tree>`, haciendo la herencia más resistente a cambios menores de estructura.
|
||||
|
||||
---
|
||||
|
||||
## Consultar la Base de Datos con un Script
|
||||
|
||||
Interactuar con la base de datos directamente usando `psql` a través de `docker-compose exec` puede ser complicado debido a la forma en que el shell maneja las comillas. Una alternativa más robusta y confiable es utilizar un script de Python que aproveche el ORM de Odoo.
|
||||
|
||||
### Procedimiento
|
||||
|
||||
1. **Crear un Script de Python:**
|
||||
Crea un script que se conecte a la base de datos y ejecute la consulta deseada.
|
||||
|
||||
**Ejemplo (`verify_products.py`):**
|
||||
```python
|
||||
import odoo
|
||||
import json
|
||||
|
||||
def verify_lab_order_products(cr):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
so.name AS order_name,
|
||||
sol.id AS line_id,
|
||||
pt.name->>'en_US' AS product_name,
|
||||
pt.is_analysis
|
||||
FROM
|
||||
sale_order so
|
||||
JOIN
|
||||
sale_order_line sol ON so.id = sol.order_id
|
||||
JOIN
|
||||
product_product pp ON sol.product_id = pp.id
|
||||
JOIN
|
||||
product_template pt ON pp.product_tmpl_id = pt.id
|
||||
WHERE
|
||||
so.is_lab_request = TRUE;
|
||||
""")
|
||||
return cr.fetchall()
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
results = verify_lab_order_products(cr)
|
||||
print(json.dumps(results, indent=4))
|
||||
```
|
||||
|
||||
2. **Copiar el Script al Contenedor:**
|
||||
Usa el comando `docker cp` para copiar el script al contenedor de Odoo.
|
||||
```bash
|
||||
docker cp verify_products.py lims_odoo:/tmp/verify_products.py
|
||||
```
|
||||
|
||||
3. **Ejecutar el Script:**
|
||||
Ejecuta el script dentro del contenedor usando `docker-compose exec`.
|
||||
```bash
|
||||
docker-compose exec odoo python3 /tmp/verify_products.py
|
||||
```
|
||||
|
||||
Este método evita los problemas de entrecomillado y permite ejecutar consultas complejas de manera confiable.
|
||||
|
||||
---
|
||||
|
||||
## Creación de Datos de Demostración Complejos
|
||||
|
||||
Cuando los datos de demostración tienen dependencias complejas o requieren lógica de negocio (por ejemplo, cambiar el estado de un registro, o crear registros relacionados que dependen de otros), el uso de archivos XML puede ser limitado y propenso a errores de carga.
|
||||
|
||||
En estos casos, es preferible utilizar un script de Python para crear los datos de demostración.
|
||||
|
||||
### Procedimiento
|
||||
|
||||
1. **Crear un Script de Python:**
|
||||
Crea un script que utilice el ORM de Odoo para crear los registros de demostración. Esto permite utilizar la lógica de negocio de los modelos, como los métodos `create` y `write`, y buscar registros existentes con `search` y `ref`.
|
||||
|
||||
**Ejemplo (`create_lab_requests.py`):**
|
||||
```python
|
||||
import odoo
|
||||
|
||||
def create_lab_requests(cr):
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
# Eliminar órdenes de venta de demostración no deseadas
|
||||
unwanted_orders = env['sale.order'].search([('name', 'in', ['S00001', ...])])
|
||||
for order in unwanted_orders:
|
||||
try:
|
||||
order.action_cancel()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
unwanted_orders.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Crear solicitudes de laboratorio
|
||||
patient1 = env.ref('lims_management.demo_patient_1')
|
||||
doctor1 = env.ref('lims_management.demo_doctor_1')
|
||||
hemograma = env.ref('lims_management.analysis_hemograma')
|
||||
|
||||
env['sale.order'].create({
|
||||
'partner_id': patient1.id,
|
||||
'doctor_id': doctor1.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [
|
||||
(0, 0, {'product_id': hemograma.product_variant_id.id, 'product_uom_qty': 1})
|
||||
]
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
create_lab_requests(cr)
|
||||
cr.commit()
|
||||
```
|
||||
|
||||
2. **Integrar el Script en la Inicialización:**
|
||||
Modifica el script `init_odoo.py` para que ejecute el script de creación de datos después de que Odoo haya terminado de instalar los módulos.
|
||||
|
||||
**En `docker-compose.yml`**, asegúrate de que el script esté disponible en el contenedor `odoo_init`:
|
||||
```yaml
|
||||
volumes:
|
||||
- ./create_lab_requests.py:/app/create_lab_requests.py
|
||||
```
|
||||
|
||||
**En `init_odoo.py`**, añade la lógica para ejecutar el script:
|
||||
```python
|
||||
# --- Lógica para crear datos de demostración personalizados ---
|
||||
print("Creando datos de demostración complejos...")
|
||||
sys.stdout.flush()
|
||||
|
||||
with open("/app/create_lab_requests.py", "r") as f:
|
||||
script_content = f.read()
|
||||
|
||||
create_requests_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
create_requests_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
```
|
||||
Este enfoque proporciona un control total sobre la creación de datos de demostración y evita los problemas de dependencia y orden de carga de los archivos XML.
|
||||
|
|
26
README.md
|
@ -1,26 +0,0 @@
|
|||
# Proyecto de Laboratorio Clínico (LIMS)
|
||||
|
||||
Este proyecto contiene el desarrollo de un módulo de gestión de laboratorios clínicos para Odoo 18.
|
||||
|
||||
## Desarrollo
|
||||
|
||||
### Hook de Pre-Commit
|
||||
|
||||
Para asegurar la integridad de los commits y evitar que se suban cambios incompletos, este repositorio incluye un hook de `pre-commit`.
|
||||
|
||||
**Propósito:**
|
||||
El hook revisa automáticamente si existen archivos modificados que no han sido agregados al "staging area" cada vez que se intenta realizar un commit. Si se detectan cambios sin agregar, el commit es abortado.
|
||||
|
||||
**Instalación (Obligatoria para todos los desarrolladores):**
|
||||
|
||||
Para activar el hook en tu copia local del repositorio, ejecuta los siguientes comandos desde la raíz del proyecto:
|
||||
|
||||
```bash
|
||||
# Copia el hook desde el directorio de scripts a tu directorio local de git
|
||||
cp scripts/hooks/pre-commit .git/hooks/
|
||||
|
||||
# Dale permisos de ejecución (necesario en macOS y Linux)
|
||||
chmod +x .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
Una vez instalado, el hook se ejecutará en cada commit, ayudando a mantener un historial de cambios limpio y completo.
|
|
@ -1,43 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script para crear issues específicos sobre el ciclo de vida y automatización de muestras.
|
||||
|
||||
# Issue 8: Implementar Ciclo de Vida para Muestras de Laboratorio
|
||||
tea issue create --title "feat: Implementar Ciclo de Vida para Muestras de Laboratorio" --labels "feature,enhancement" --description "$(cat <<'EOT'
|
||||
**Objetivo:** Implementar una máquina de estados para el modelo de muestra (`stock.lot`) que permita seguir su ciclo de vida desde la recolección hasta el descarte.
|
||||
|
||||
**Tareas:**
|
||||
|
||||
1. **Modelo (`stock.lot`):**
|
||||
* Añadir un campo `state` de tipo `Selection` con los siguientes estados:
|
||||
- `collected` (Recolectada)
|
||||
- `received` (Recibida en Laboratorio)
|
||||
- `in_process` (En Proceso)
|
||||
- `analyzed` (Analizada)
|
||||
- `stored` (Almacenada)
|
||||
- `disposed` (Desechada)
|
||||
* Definir métodos para las transiciones de estado (ej. `action_receive`, `action_start_analysis`, etc.).
|
||||
|
||||
2. **Vistas (`stock_lot_views.xml`):**
|
||||
* Añadir un `statusbar` en la vista de formulario para visualizar y gestionar el estado.
|
||||
* Incorporar botones en el `header` para ejecutar las acciones de cambio de estado.
|
||||
* Mostrar el campo `state` en la vista de lista y añadirlo a los filtros.
|
||||
* Aplicar `readonly` a campos clave en función del estado para prevenir modificaciones no deseadas.
|
||||
EOT
|
||||
)"
|
||||
|
||||
# Issue 9: Automatizar Creación de Muestras desde la Solicitud de Laboratorio
|
||||
tea issue create --title "feat: Automatizar Creación de Muestras desde la Solicitud" --labels "feature,automation" --description "$(cat <<'EOT'
|
||||
**Objetivo:** Automatizar la generación de registros de muestra (`stock.lot`) cuando una Solicitud de Laboratorio (`sale.order`) es confirmada.
|
||||
|
||||
**Tareas:**
|
||||
|
||||
1. **Lógica de Negocio (`sale_order.py`):**
|
||||
* Heredar y extender el método `action_confirm` del modelo `sale.order`.
|
||||
* Dentro del método, añadir la lógica para crear un nuevo registro en `stock.lot` por cada tipo de muestra requerido en la solicitud.
|
||||
* Asociar la muestra creada con la solicitud (`request_id`) y el paciente (`patient_id`) correspondientes.
|
||||
* Asegurarse de que la muestra se cree en el estado inicial correcto (ej. 'Recolectada' o 'Pendiente de Recolección').
|
||||
EOT
|
||||
)"
|
||||
|
||||
echo "Script 'create_lifecycle_issues.sh' generado. Ejecútalo para crear los nuevos issues."
|
|
@ -1,78 +0,0 @@
|
|||
# Análisis de Dashboards para LIMS - Issue #71
|
||||
|
||||
## Dashboards Implementables sin Módulos Adicionales ni Cambios Estructurales
|
||||
|
||||
### 1. ✅ Dashboard de Estado de Órdenes
|
||||
**Factibilidad**: Alta
|
||||
- Usar vistas graph y pivot nativas de Odoo
|
||||
- Datos disponibles: sale.order con is_lab_request=True
|
||||
- Métricas: órdenes por estado, por fecha, por paciente
|
||||
|
||||
### 2. ✅ Dashboard de Productividad de Técnicos
|
||||
**Factibilidad**: Alta
|
||||
- Datos disponibles: lims.test (technician_id, state, create_date, validation_date)
|
||||
- Métricas: pruebas procesadas por técnico, tiempos promedio, estados
|
||||
|
||||
### 3. ✅ Dashboard de Muestras
|
||||
**Factibilidad**: Alta
|
||||
- Datos disponibles: stock.lot con is_lab_sample=True
|
||||
- Métricas: muestras por estado, rechazos, re-muestreos
|
||||
|
||||
### 4. ✅ Dashboard de Parámetros Fuera de Rango
|
||||
**Factibilidad**: Alta
|
||||
- Datos disponibles: lims.result (is_out_of_range, is_critical)
|
||||
- Métricas: resultados críticos, fuera de rango por parámetro
|
||||
|
||||
### 5. ✅ Dashboard de Análisis Más Solicitados
|
||||
**Factibilidad**: Alta
|
||||
- Datos disponibles: sale.order.line con productos is_analysis=True
|
||||
- Métricas: top análisis, tendencias por período
|
||||
|
||||
### 6. ⚠️ Dashboard de Tiempos de Respuesta
|
||||
**Factibilidad**: Media
|
||||
- Requiere campos calculados (no almacenados actualmente)
|
||||
- Necesitaría agregar campos store=True para métricas de tiempo
|
||||
|
||||
### 7. ❌ Dashboard de Facturación
|
||||
**Factibilidad**: Baja
|
||||
- Requiere módulo account (facturación)
|
||||
- No está en las dependencias actuales
|
||||
|
||||
### 8. ❌ Dashboard de Inventario de Reactivos
|
||||
**Factibilidad**: Baja
|
||||
- Requiere configuración adicional de stock
|
||||
- No hay modelo específico para reactivos
|
||||
|
||||
## Implementación Técnica
|
||||
|
||||
### Herramientas Disponibles en Odoo 18:
|
||||
1. **Vistas Graph**: Gráficos de barras, líneas, pie
|
||||
2. **Vistas Pivot**: Tablas dinámicas
|
||||
3. **Vistas Cohort**: Análisis de cohortes
|
||||
4. **Filtros y Agrupaciones**: Para segmentar datos
|
||||
5. **Acciones de Servidor**: Para cálculos complejos
|
||||
|
||||
### Estructura Propuesta:
|
||||
```xml
|
||||
<!-- Menú principal de Dashboards -->
|
||||
<menuitem id="menu_lims_dashboards"
|
||||
name="Dashboards"
|
||||
parent="lims_management.menu_lims_root"
|
||||
sequence="5"
|
||||
groups="group_lims_admin,group_lims_manager"/>
|
||||
```
|
||||
|
||||
## Recomendación
|
||||
|
||||
Sugiero comenzar con los 5 dashboards marcados con ✅ ya que:
|
||||
1. Utilizan datos existentes
|
||||
2. No requieren cambios en modelos
|
||||
3. Usan herramientas nativas de Odoo
|
||||
4. Proveen valor inmediato al administrador
|
||||
|
||||
Orden de implementación sugerido:
|
||||
1. Dashboard de Estado de Órdenes (más básico)
|
||||
2. Dashboard de Productividad de Técnicos
|
||||
3. Dashboard de Muestras
|
||||
4. Dashboard de Parámetros Fuera de Rango
|
||||
5. Dashboard de Análisis Más Solicitados
|
|
@ -24,9 +24,6 @@ services:
|
|||
- ./lims_management:/mnt/extra-addons/lims_management
|
||||
- ./odoo.conf:/etc/odoo/odoo.conf
|
||||
- ./init_odoo.py:/app/init_odoo.py
|
||||
- ./test/create_lab_requests.py:/app/create_lab_requests.py
|
||||
- ./test:/app/test
|
||||
- ./scripts:/app/scripts
|
||||
command: ["/usr/bin/python3", "/app/init_odoo.py"]
|
||||
environment:
|
||||
HOST: db
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
# Issue #32 Implementation Summary
|
||||
|
||||
## Overview
|
||||
Automatic sample generation when lab orders are confirmed has been successfully implemented, building upon the test-sample relationships established in Issue #44.
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
### 1. Extended sale.order Model ✅
|
||||
- Added `generated_sample_ids` Many2many field to track generated samples
|
||||
- Override `action_confirm()` to intercept lab order confirmation
|
||||
- Implemented `_generate_lab_samples()` main logic
|
||||
- Implemented `_group_analyses_by_sample_type()` for intelligent grouping
|
||||
- Implemented `_create_sample_for_group()` for sample creation
|
||||
|
||||
### 2. Sample Generation Logic ✅
|
||||
- Analyses requiring the same sample type are grouped together
|
||||
- Volumes are summed for all analyses in a group
|
||||
- Each sample is linked to the originating order
|
||||
- Error handling with user notifications
|
||||
|
||||
### 3. Enhanced Barcode Generation ✅
|
||||
- Unique barcode format: YYMMDDNNNNNNC (13 digits)
|
||||
- Sequential numbering with date prefix
|
||||
- Luhn check digit for validation
|
||||
- Collision detection and retry mechanism
|
||||
- Sample type prefixes for high-volume scenarios
|
||||
|
||||
### 4. Updated Views ✅
|
||||
- Added "Muestras Generadas" tab in sale.order form
|
||||
- Embedded list shows barcode, type, volume, and analyses
|
||||
- Added workflow buttons in the sample list
|
||||
- List view indicators for lab requests and generated samples
|
||||
|
||||
### 5. Notifications System ✅
|
||||
- Warning messages for analyses without sample types
|
||||
- Success messages listing all generated samples
|
||||
- Error messages if generation fails
|
||||
- All messages posted to order chatter
|
||||
|
||||
### 6. Verification Script ✅
|
||||
- Comprehensive testing of automatic generation
|
||||
- Barcode uniqueness validation
|
||||
- Analysis grouping verification
|
||||
- Edge case handling
|
||||
|
||||
### 7. Demo Data ✅
|
||||
- 4 demo orders showcasing different scenarios
|
||||
- Multiple analyses with same sample type
|
||||
- Multiple analyses with different sample types
|
||||
- Pediatric orders
|
||||
|
||||
## Key Features
|
||||
|
||||
### Automatic Grouping
|
||||
When a lab order contains multiple analyses requiring the same type of sample (e.g., multiple EDTA tube tests), they are automatically grouped into a single sample container.
|
||||
|
||||
### Volume Calculation
|
||||
The system automatically sums the required volumes for all analyses in a group, ensuring adequate sample collection.
|
||||
|
||||
### Barcode Generation
|
||||
Each sample receives a unique 13-digit barcode with:
|
||||
- Date prefix for daily sequencing
|
||||
- Sequential numbering
|
||||
- Check digit for validation
|
||||
|
||||
### Error Handling
|
||||
- Analyses without sample types generate warnings but don't stop the process
|
||||
- Failed generations are logged with clear error messages
|
||||
- Orders can still be confirmed even if sample generation fails
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
1. Create a lab order with multiple analyses
|
||||
2. Confirm the order
|
||||
3. Samples are automatically generated and visible in the "Muestras Generadas" tab
|
||||
4. Each sample has a unique barcode ready for printing
|
||||
|
||||
### For Developers
|
||||
The implementation is modular and extensible:
|
||||
- Override `_group_analyses_by_sample_type()` for custom grouping logic
|
||||
- Extend `_create_sample_for_group()` for additional sample attributes
|
||||
- Barcode format can be customized in `_generate_unique_barcode()`
|
||||
|
||||
## Testing
|
||||
Run the verification script to validate the implementation:
|
||||
```bash
|
||||
docker cp verify_automatic_sample_generation.py lims_odoo:/tmp/
|
||||
docker exec lims_odoo python3 /tmp/verify_automatic_sample_generation.py
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
- Optional: Implement configuration wizard (Task 5)
|
||||
- Optional: Add barcode printing functionality
|
||||
- Optional: Add sample label generation
|
||||
- Optional: Configure grouping rules per analysis type
|
|
@ -1,99 +0,0 @@
|
|||
# Issue #44 Implementation Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the implementation of Issue #44: Adding relationships between analyses and sample types in the LIMS module.
|
||||
|
||||
## Changes Implemented
|
||||
|
||||
### 1. Model Updates
|
||||
|
||||
#### ProductTemplate (`lims_management/models/product.py`)
|
||||
- Added `required_sample_type_id` (Many2one): Links analysis to required sample type
|
||||
- Added `sample_volume_ml` (Float): Specifies required sample volume in ml
|
||||
- Added validation constraints to ensure fields are only used for analysis products
|
||||
|
||||
#### StockLot (`lims_management/models/stock_lot.py`)
|
||||
- Added `sample_type_product_id` (Many2one): References the sample type product
|
||||
- Kept `container_type` field for backward compatibility (marked as legacy)
|
||||
- Added `@api.onchange` method to synchronize both fields
|
||||
- Added `get_container_name()` method to retrieve container name from either field
|
||||
|
||||
### 2. View Updates
|
||||
|
||||
#### Product Views (`lims_management/views/analysis_views.xml`)
|
||||
- Added sample type fields to analysis configuration page
|
||||
- Created list views showing test-sample relationships
|
||||
- Added `is_sample_type` field to product form
|
||||
|
||||
#### Stock Lot Views (`lims_management/views/stock_lot_views.xml`)
|
||||
- Added `sample_type_product_id` to both list and form views
|
||||
- Made `container_type` optional and conditionally visible
|
||||
- Proper readonly states based on workflow
|
||||
|
||||
### 3. Data Files
|
||||
|
||||
#### Initial Data (`lims_management/data/sample_types.xml`)
|
||||
Created 10 common laboratory sample types:
|
||||
- Serum Tube (Red Cap)
|
||||
- EDTA Tube (Purple Cap)
|
||||
- Citrate Tube (Blue Cap)
|
||||
- Heparin Tube (Green Cap)
|
||||
- Glucose Tube (Gray Cap)
|
||||
- Urine Container
|
||||
- Stool Container
|
||||
- Swab
|
||||
- Blood Culture Bottle
|
||||
- CSF Tube
|
||||
|
||||
#### Demo Data Updates
|
||||
- Updated all demo analyses with sample type requirements and volumes
|
||||
- Updated demo samples to use the new `sample_type_product_id` field
|
||||
- Added complete test-sample mappings
|
||||
|
||||
### 4. Verification Tools
|
||||
|
||||
Created `verify_sample_relationships.py` script that checks:
|
||||
- Analyses with proper sample type assignments
|
||||
- Available sample types and their usage
|
||||
- Laboratory samples field synchronization
|
||||
- Data integrity and consistency
|
||||
|
||||
## Usage
|
||||
|
||||
### For Developers
|
||||
1. When creating a new analysis product:
|
||||
- Set `is_analysis = True`
|
||||
- Select the appropriate `required_sample_type_id`
|
||||
- Specify `sample_volume_ml` if needed
|
||||
|
||||
2. When creating a laboratory sample (stock.lot):
|
||||
- Use `sample_type_product_id` to select the sample type
|
||||
- The legacy `container_type` field will auto-synchronize
|
||||
|
||||
### For Users
|
||||
1. Analysis products now show their required sample type
|
||||
2. When viewing samples, the sample type is clearly displayed
|
||||
3. The system maintains backward compatibility with existing data
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Automation Ready**: Foundation for automatic sample generation (Issue #32)
|
||||
2. **Data Integrity**: Clear relationships between tests and samples
|
||||
3. **User Clarity**: Users know exactly which container to use for each test
|
||||
4. **Grouping Capability**: Can group analyses requiring the same sample type
|
||||
5. **Backward Compatible**: Existing data continues to work
|
||||
|
||||
## Testing
|
||||
|
||||
Run the verification script to check implementation:
|
||||
```bash
|
||||
docker cp verify_sample_relationships.py lims_odoo:/tmp/
|
||||
docker exec lims_odoo python3 /tmp/verify_sample_relationships.py
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
With this foundation in place, Issue #32 (automatic sample generation) can now be implemented by:
|
||||
1. Reading the `required_sample_type_id` from ordered analyses
|
||||
2. Grouping analyses by sample type
|
||||
3. Creating appropriate `stock.lot` records with correct `sample_type_product_id`
|
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 23 KiB |
|
@ -1,169 +0,0 @@
|
|||
{
|
||||
"account_tag_ids": [],
|
||||
"active": true,
|
||||
"activity_date_deadline": false,
|
||||
"activity_exception_decoration": false,
|
||||
"activity_exception_icon": false,
|
||||
"activity_ids": [],
|
||||
"activity_state": false,
|
||||
"activity_summary": false,
|
||||
"activity_type_icon": false,
|
||||
"activity_type_id": false,
|
||||
"activity_user_id": false,
|
||||
"analysis_type": false,
|
||||
"attribute_line_ids": [],
|
||||
"barcode": false,
|
||||
"can_image_1024_be_zoomed": false,
|
||||
"categ_id": [
|
||||
10,
|
||||
"All / Home Construction"
|
||||
],
|
||||
"color": 0,
|
||||
"combo_ids": [],
|
||||
"company_id": false,
|
||||
"cost_currency_id": [
|
||||
1,
|
||||
"USD"
|
||||
],
|
||||
"cost_method": "standard",
|
||||
"create_date": "2025-07-14 07:23:12",
|
||||
"create_uid": [
|
||||
1,
|
||||
"OdooBot"
|
||||
],
|
||||
"currency_id": [
|
||||
1,
|
||||
"USD"
|
||||
],
|
||||
"default_code": false,
|
||||
"description": false,
|
||||
"description_picking": false,
|
||||
"description_pickingin": false,
|
||||
"description_pickingout": false,
|
||||
"description_purchase": false,
|
||||
"description_sale": false,
|
||||
"display_name": "Furniture Assembly",
|
||||
"expense_policy": "no",
|
||||
"fiscal_country_codes": "US",
|
||||
"has_available_route_ids": false,
|
||||
"has_configurable_attributes": false,
|
||||
"has_message": true,
|
||||
"id": 29,
|
||||
"image_1024": false,
|
||||
"image_128": false,
|
||||
"image_1920": false,
|
||||
"image_256": false,
|
||||
"image_512": false,
|
||||
"incoming_qty": 0,
|
||||
"invoice_policy": "order",
|
||||
"is_analysis": false,
|
||||
"is_favorite": false,
|
||||
"is_product_variant": false,
|
||||
"is_storable": false,
|
||||
"list_price": 2000,
|
||||
"location_id": false,
|
||||
"lot_valuated": false,
|
||||
"message_attachment_count": 0,
|
||||
"message_follower_ids": [],
|
||||
"message_has_error": false,
|
||||
"message_has_error_counter": 0,
|
||||
"message_has_sms_error": false,
|
||||
"message_ids": [
|
||||
188,
|
||||
114
|
||||
],
|
||||
"message_is_follower": false,
|
||||
"message_needaction": false,
|
||||
"message_needaction_counter": 0,
|
||||
"message_partner_ids": [],
|
||||
"my_activity_date_deadline": false,
|
||||
"name": "Furniture Assembly",
|
||||
"nbr_moves_in": 0,
|
||||
"nbr_moves_out": 0,
|
||||
"nbr_reordering_rules": 0,
|
||||
"optional_product_ids": [],
|
||||
"outgoing_qty": 0,
|
||||
"packaging_ids": [],
|
||||
"pricelist_item_count": 0,
|
||||
"product_document_count": 0,
|
||||
"product_document_ids": [],
|
||||
"product_properties": [],
|
||||
"product_tag_ids": [],
|
||||
"product_tooltip": "Invoice ordered quantities as soon as this service is sold.",
|
||||
"product_variant_count": 1,
|
||||
"product_variant_id": [
|
||||
38,
|
||||
"Furniture Assembly"
|
||||
],
|
||||
"product_variant_ids": [
|
||||
38
|
||||
],
|
||||
"property_account_expense_id": false,
|
||||
"property_account_income_id": false,
|
||||
"property_stock_inventory": [
|
||||
14,
|
||||
"Virtual Locations/Inventory adjustment"
|
||||
],
|
||||
"property_stock_production": [
|
||||
15,
|
||||
"Virtual Locations/Production"
|
||||
],
|
||||
"purchase_ok": true,
|
||||
"qty_available": 0,
|
||||
"reordering_max_qty": 0,
|
||||
"reordering_min_qty": 0,
|
||||
"responsible_id": [
|
||||
1,
|
||||
"OdooBot"
|
||||
],
|
||||
"route_from_categ_ids": [],
|
||||
"route_ids": [],
|
||||
"sale_delay": 0,
|
||||
"sale_line_warn": "no-message",
|
||||
"sale_line_warn_msg": false,
|
||||
"sale_ok": true,
|
||||
"sales_count": 0,
|
||||
"seller_ids": [],
|
||||
"sequence": 1,
|
||||
"service_tracking": "no",
|
||||
"service_type": "manual",
|
||||
"show_forecasted_qty_status_button": false,
|
||||
"show_on_hand_qty_status_button": false,
|
||||
"standard_price": 2500,
|
||||
"supplier_taxes_id": [],
|
||||
"tax_string": " ",
|
||||
"taxes_id": [],
|
||||
"technical_specifications": false,
|
||||
"tracking": "none",
|
||||
"type": "service",
|
||||
"uom_category_id": [
|
||||
3,
|
||||
"Working Time"
|
||||
],
|
||||
"uom_id": [
|
||||
4,
|
||||
"Hours"
|
||||
],
|
||||
"uom_name": "Hours",
|
||||
"uom_po_id": [
|
||||
4,
|
||||
"Hours"
|
||||
],
|
||||
"valid_product_template_attribute_line_ids": [],
|
||||
"valuation": "manual_periodic",
|
||||
"value_range_ids": [],
|
||||
"variant_seller_ids": [],
|
||||
"virtual_available": 0,
|
||||
"visible_expense_policy": false,
|
||||
"volume": 0,
|
||||
"volume_uom_name": "m³",
|
||||
"warehouse_id": false,
|
||||
"website_message_ids": [],
|
||||
"weight": 0,
|
||||
"weight_uom_name": "kg",
|
||||
"write_date": "2025-07-14 07:23:55",
|
||||
"write_uid": [
|
||||
1,
|
||||
"OdooBot"
|
||||
]
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"access_token": false,
|
||||
"access_url": "/my/orders/21",
|
||||
"access_warning": "",
|
||||
"activity_date_deadline": false,
|
||||
"activity_exception_decoration": false,
|
||||
"activity_exception_icon": false,
|
||||
"activity_ids": [],
|
||||
"activity_state": false,
|
||||
"activity_summary": false,
|
||||
"activity_type_icon": false,
|
||||
"activity_type_id": false,
|
||||
"activity_user_id": false,
|
||||
"amount_invoiced": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_tax": 0,
|
||||
"amount_to_invoice": 2589,
|
||||
"amount_total": 2589,
|
||||
"amount_undiscounted": 2589,
|
||||
"amount_untaxed": 2589,
|
||||
"authorized_transaction_ids": [],
|
||||
"available_product_document_ids": [
|
||||
2
|
||||
],
|
||||
"campaign_id": false,
|
||||
"client_order_ref": false,
|
||||
"commitment_date": false,
|
||||
"company_id": [
|
||||
1,
|
||||
"My Company (San Francisco)"
|
||||
],
|
||||
"company_price_include": "tax_excluded",
|
||||
"country_code": "US",
|
||||
"create_date": "2025-07-14 07:24:01",
|
||||
"create_uid": [
|
||||
1,
|
||||
"OdooBot"
|
||||
],
|
||||
"currency_id": [
|
||||
1,
|
||||
"USD"
|
||||
],
|
||||
"currency_rate": 1,
|
||||
"customizable_pdf_form_fields": false,
|
||||
"date_order": "2025-07-14 07:24:02",
|
||||
"delivery_count": 0,
|
||||
"delivery_status": false,
|
||||
"display_name": "S00021",
|
||||
"doctor_id": [
|
||||
46,
|
||||
"Dr. Luis Herrera"
|
||||
],
|
||||
"duplicated_order_ids": [],
|
||||
"effective_date": false,
|
||||
"expected_date": "2025-07-14 07:26:23",
|
||||
"fiscal_position_id": false,
|
||||
"has_active_pricelist": false,
|
||||
"has_archived_products": false,
|
||||
"has_message": true,
|
||||
"id": 21,
|
||||
"incoterm": false,
|
||||
"incoterm_location": false,
|
||||
"invoice_count": 0,
|
||||
"invoice_ids": [],
|
||||
"invoice_status": "no",
|
||||
"is_expired": false,
|
||||
"is_lab_request": true,
|
||||
"is_pdf_quote_builder_available": true,
|
||||
"journal_id": false,
|
||||
"json_popover": "{\"popoverTemplate\": \"sale_stock.DelayAlertWidget\", \"late_elements\": []}",
|
||||
"locked": false,
|
||||
"medium_id": false,
|
||||
"message_attachment_count": 0,
|
||||
"message_follower_ids": [],
|
||||
"message_has_error": false,
|
||||
"message_has_error_counter": 0,
|
||||
"message_has_sms_error": false,
|
||||
"message_ids": [
|
||||
310
|
||||
],
|
||||
"message_is_follower": false,
|
||||
"message_needaction": false,
|
||||
"message_needaction_counter": 0,
|
||||
"message_partner_ids": [],
|
||||
"my_activity_date_deadline": false,
|
||||
"name": "S00021",
|
||||
"note": false,
|
||||
"order_line": [
|
||||
45,
|
||||
46
|
||||
],
|
||||
"origin": false,
|
||||
"partner_credit_warning": "",
|
||||
"partner_id": [
|
||||
44,
|
||||
"Ana Torres"
|
||||
],
|
||||
"partner_invoice_id": [
|
||||
44,
|
||||
"Ana Torres"
|
||||
],
|
||||
"partner_shipping_id": [
|
||||
44,
|
||||
"Ana Torres"
|
||||
],
|
||||
"payment_term_id": false,
|
||||
"pending_email_template_id": false,
|
||||
"picking_ids": [],
|
||||
"picking_policy": "direct",
|
||||
"prepayment_percent": 1,
|
||||
"pricelist_id": false,
|
||||
"procurement_group_id": false,
|
||||
"quotation_document_ids": [],
|
||||
"reference": false,
|
||||
"require_payment": true,
|
||||
"require_signature": true,
|
||||
"sale_order_option_ids": [],
|
||||
"sale_order_template_id": false,
|
||||
"show_json_popover": false,
|
||||
"show_update_fpos": false,
|
||||
"show_update_pricelist": false,
|
||||
"signature": false,
|
||||
"signed_by": false,
|
||||
"signed_on": false,
|
||||
"source_id": false,
|
||||
"state": "draft",
|
||||
"tag_ids": [],
|
||||
"tax_calculation_rounding_method": "round_per_line",
|
||||
"tax_country_id": [
|
||||
233,
|
||||
"United States"
|
||||
],
|
||||
"tax_totals": {
|
||||
"currency_id": 1
|
||||
},
|
||||
"team_id": [
|
||||
1,
|
||||
"Sales"
|
||||
],
|
||||
"terms_type": "plain",
|
||||
"transaction_ids": [],
|
||||
"type_name": "Quotation",
|
||||
"user_id": [
|
||||
1,
|
||||
"OdooBot"
|
||||
],
|
||||
"validity_date": "2025-08-13",
|
||||
"warehouse_id": [
|
||||
1,
|
||||
"YourCompany"
|
||||
],
|
||||
"website_message_ids": [],
|
||||
"write_date": "2025-07-14 07:24:04",
|
||||
"write_uid": [
|
||||
1,
|
||||
"OdooBot"
|
||||
]
|
||||
}
|
|
@ -1,650 +0,0 @@
|
|||
{
|
||||
"sale_order": [
|
||||
[
|
||||
"id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"message_main_attachment_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"access_token",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"name",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"origin",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"client_order_ref",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"reference",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"state",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"date_order",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"validity_date",
|
||||
"date"
|
||||
],
|
||||
[
|
||||
"commitment_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"expected_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"user_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"partner_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"partner_invoice_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"partner_shipping_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"pricelist_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"currency_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"analytic_account_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"order_line",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"invoice_count",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"invoice_status",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"note",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"amount_untaxed",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"amount_tax",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"amount_total",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"currency_rate",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"payment_term_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"fiscal_position_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"company_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"team_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"signature",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"signed_by",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"signed_on",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"create_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"create_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"write_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"write_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"sale_order_template_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"incoterm",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"picking_policy",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"warehouse_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"procurement_group_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"campaign_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"medium_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"source_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"delivery_count",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"is_lab_request",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"doctor_id",
|
||||
"integer"
|
||||
]
|
||||
],
|
||||
"sale_order_line": [
|
||||
[
|
||||
"id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"order_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"name",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"sequence",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"invoice_status",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"price_unit",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"price_subtotal",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"price_tax",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"price_total",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"price_reduce",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"tax_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"price_reduce_taxinc",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"price_reduce_taxexcl",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"discount",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"product_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_template_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_uom_category_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_uom",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_uom_qty",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"product_uom_readonly",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"qty_delivered_method",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"qty_delivered",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"qty_delivered_manual",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"qty_to_invoice",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"qty_invoiced",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"untaxed_amount_invoiced",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"untaxed_amount_to_invoice",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"salesman_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"currency_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"company_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"order_partner_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"is_expense",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"is_downpayment",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"state",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"customer_lead",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"display_type",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"create_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"create_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"write_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"write_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"analytic_distribution",
|
||||
"jsonb"
|
||||
],
|
||||
[
|
||||
"analytic_line_ids",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"is_service",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"sale_order_option_ids",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"linked_line_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_packaging_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_packaging_qty",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"product_packaging_description",
|
||||
"text"
|
||||
]
|
||||
],
|
||||
"product_template": [
|
||||
[
|
||||
"id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"message_main_attachment_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"sequence",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"name",
|
||||
"jsonb"
|
||||
],
|
||||
[
|
||||
"description",
|
||||
"jsonb"
|
||||
],
|
||||
[
|
||||
"description_purchase",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"description_sale",
|
||||
"jsonb"
|
||||
],
|
||||
[
|
||||
"type",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"categ_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"currency_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"cost_currency_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"list_price",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"volume",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"weight",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"sale_ok",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"purchase_ok",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"uom_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"uom_po_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"company_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"active",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"color",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"default_code",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"can_image_1024_be_zoomed",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"create_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"create_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"write_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"write_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"service_type",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"sale_line_warn",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"sale_line_warn_msg",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"expense_policy",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"visible_expense_policy",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"invoice_policy",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"sale_delay",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"tracking",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"description_picking",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"description_pickingout",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"description_pickingin",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"responsible_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"property_stock_production",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"property_stock_inventory",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"service_tracking",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"is_analysis",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"analysis_type",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"technical_specifications",
|
||||
"text"
|
||||
]
|
||||
],
|
||||
"product_product": [
|
||||
[
|
||||
"id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"message_main_attachment_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"product_tmpl_id",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"default_code",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"barcode",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"combination_indices",
|
||||
"character varying"
|
||||
],
|
||||
[
|
||||
"volume",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"weight",
|
||||
"double precision"
|
||||
],
|
||||
[
|
||||
"active",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"can_be_expensed",
|
||||
"boolean"
|
||||
],
|
||||
[
|
||||
"create_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"create_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"write_uid",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"write_date",
|
||||
"timestamp without time zone"
|
||||
],
|
||||
[
|
||||
"lst_price",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"standard_price",
|
||||
"numeric"
|
||||
],
|
||||
[
|
||||
"property_stock_production",
|
||||
"integer"
|
||||
],
|
||||
[
|
||||
"property_stock_inventory",
|
||||
"integer"
|
||||
]
|
||||
]
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
# Plan de Actividades: Issue #31 - Ciclo de Vida de la Muestra
|
||||
|
||||
## Objetivo
|
||||
|
||||
Implementar una máquina de estados completa para el modelo `stock.lot` con el fin de gestionar y trazar el ciclo de vida de una muestra de laboratorio, desde su recolección hasta su descarte.
|
||||
|
||||
---
|
||||
|
||||
## Plan de Ejecución
|
||||
|
||||
### 1. Modificación del Modelo (`stock.lot`)
|
||||
|
||||
- **Archivo:** `lims_management/models/stock_lot.py`
|
||||
- **Tareas:**
|
||||
- [x] **Añadir campo `state`:**
|
||||
- Tipo: `Selection`
|
||||
- Nombre técnico: `state`
|
||||
- String: "Estado"
|
||||
- Opciones:
|
||||
- `collected`: 'Recolectada' (Estado por defecto)
|
||||
- `received`: 'Recibida en Laboratorio'
|
||||
- `in_process`: 'En Proceso'
|
||||
- `analyzed`: 'Analizada'
|
||||
- `stored`: 'Almacenada'
|
||||
- `disposed`: 'Desechada'
|
||||
- Atributos: `tracking=True` para registrar cambios en el chatter.
|
||||
- [x] **Definir métodos para transiciones:**
|
||||
- `action_receive()`: Cambia el estado a `received`.
|
||||
- `action_start_analysis()`: Cambia el estado a `in_process`.
|
||||
- `action_complete_analysis()`: Cambia el estado a `analyzed`.
|
||||
- `action_store()`: Cambia el estado a `stored`.
|
||||
- `action_dispose()`: Cambia el estado a `disposed`.
|
||||
- Cada método debe realizar una transición de estado simple y registrar un mensaje en el chatter.
|
||||
|
||||
### 2. Adaptación de las Vistas (`stock_lot_views.xml`)
|
||||
|
||||
- **Archivo:** `lims_management/views/stock_lot_views.xml`
|
||||
- **Tareas:**
|
||||
- [x] **Vista de Formulario:**
|
||||
- [x] **Añadir `header`:**
|
||||
- Incorporar botones para las acciones (`action_receive`, `action_start_analysis`, etc.).
|
||||
- Controlar la visibilidad de los botones según el estado actual (ej. el botón "Recibir" solo debe ser visible si el estado es 'Recolectada').
|
||||
- [x] **Añadir `statusbar`:**
|
||||
- Visualizar el campo `state` usando el widget `statusbar`.
|
||||
- Definir el `statusbar_visible` para mostrar los estados clave del flujo principal.
|
||||
- [x] **Hacer campos `readonly`:**
|
||||
- Campos como `patient_id`, `request_id`, `collection_date` deben volverse de solo lectura después de que la muestra es recibida para asegurar la integridad de los datos. Se usará el atributo `attrs` con el nuevo formato `invisible` o `readonly` basado en el campo `state`.
|
||||
- [x] **Vista de Lista:**
|
||||
- [x] Añadir el campo `state` para que sea visible.
|
||||
- [x] Añadir el campo `state` a los filtros por defecto en el `search` para poder agrupar por estado fácilmente.
|
||||
|
||||
### 3. Seguridad (Opcional, si es necesario)
|
||||
|
||||
- **Archivo:** `lims_management/security/lims_security.xml` o `ir.model.access.csv`
|
||||
- **Tareas:**
|
||||
- [x] Evaluar si se necesitan reglas de seguridad específicas para controlar quién puede ejecutar las transiciones de estado. Por ahora, se asumirá que los grupos existentes (`group_lims_technician`, `group_lims_admin`) tienen los permisos.
|
||||
|
||||
### 4. Verificación y Pruebas
|
||||
|
||||
- **Pasos:**
|
||||
- [x] Reiniciar la instancia de Odoo con el módulo actualizado.
|
||||
- [x] Crear una nueva muestra de laboratorio manualmente.
|
||||
- [x] Verificar que el estado por defecto sea 'Recolectada'.
|
||||
- [x] Probar cada uno de los botones de transición de estado en la vista de formulario.
|
||||
- [x] Confirmar que el `statusbar` se actualiza correctamente.
|
||||
- [x] Revisar el chatter para asegurarse de que los cambios de estado se están registrando.
|
||||
- [x] Verificar la visibilidad condicional de los botones y el modo de solo lectura de los campos.
|
||||
- [x] Filtrar y agrupar por estado en la vista de lista.
|
||||
|
||||
---
|
|
@ -1,191 +0,0 @@
|
|||
# Plan de Implementación - Issue #32: Generación Automática de Muestras
|
||||
|
||||
## Objetivo
|
||||
Automatizar la generación de muestras cuando se confirman órdenes de laboratorio, basándose en las relaciones test-muestra establecidas en Issue #44.
|
||||
|
||||
## Análisis de Requisitos
|
||||
|
||||
### Funcionalidad Esperada
|
||||
1. Al confirmar una orden de laboratorio (`sale.order` con `is_lab_request=True`):
|
||||
- Analizar todos los análisis incluidos en las líneas de orden
|
||||
- Agrupar análisis por tipo de muestra requerida
|
||||
- Generar automáticamente registros `stock.lot` (muestras) para cada grupo
|
||||
- Asignar códigos de barras únicos a cada muestra
|
||||
- Establecer el estado inicial como 'pending_collection'
|
||||
|
||||
### Reglas de Negocio
|
||||
1. **Agrupación de Análisis**: Múltiples análisis que requieran el mismo tipo de muestra deben compartir un único contenedor
|
||||
2. **Volumen de Muestra**: Sumar los volúmenes requeridos de todos los análisis del grupo
|
||||
3. **Identificación**: Cada muestra debe tener un código de barras único generado automáticamente
|
||||
4. **Trazabilidad**: Las muestras deben estar vinculadas a la orden de laboratorio original
|
||||
5. **Manejo de Errores**: Si un análisis no tiene tipo de muestra definido, generar advertencia pero continuar con los demás
|
||||
|
||||
## Tareas de Implementación
|
||||
|
||||
### 1. Extender el modelo sale.order ✅
|
||||
**Archivo:** `lims_management/models/sale_order.py`
|
||||
- [x] Agregar campo Many2many para referenciar las muestras generadas:
|
||||
```python
|
||||
generated_sample_ids = fields.Many2many(
|
||||
'stock.lot',
|
||||
'sale_order_stock_lot_rel',
|
||||
'order_id',
|
||||
'lot_id',
|
||||
string='Muestras Generadas',
|
||||
domain="[('is_lab_sample', '=', True)]",
|
||||
readonly=True
|
||||
)
|
||||
```
|
||||
- [x] Override del método `action_confirm()` para interceptar la confirmación
|
||||
- [x] Implementar método `_generate_lab_samples()` con la lógica principal
|
||||
- [x] Agregar método `_group_analyses_by_sample_type()` para agrupar análisis
|
||||
|
||||
### 2. Lógica de generación de muestras ✅
|
||||
**Archivo:** `lims_management/models/sale_order.py`
|
||||
- [x] Implementar algoritmo de agrupación:
|
||||
```python
|
||||
def _group_analyses_by_sample_type(self):
|
||||
"""Agrupa las líneas de orden por tipo de muestra requerida"""
|
||||
groups = {}
|
||||
for line in self.order_line:
|
||||
if line.product_id.is_analysis:
|
||||
sample_type = line.product_id.required_sample_type_id
|
||||
if sample_type:
|
||||
if sample_type.id not in groups:
|
||||
groups[sample_type.id] = {
|
||||
'sample_type': sample_type,
|
||||
'lines': [],
|
||||
'total_volume': 0.0
|
||||
}
|
||||
groups[sample_type.id]['lines'].append(line)
|
||||
groups[sample_type.id]['total_volume'] += line.product_id.sample_volume_ml or 0.0
|
||||
return groups
|
||||
```
|
||||
- [x] Crear método para generar muestras por grupo
|
||||
- [x] Implementar logging para trazabilidad
|
||||
|
||||
### 3. Generación de códigos de barras ✅
|
||||
**Archivo:** `lims_management/models/stock_lot.py`
|
||||
- [x] Mejorar el método `_compute_barcode()` para asegurar unicidad
|
||||
- [x] Agregar validación de duplicados
|
||||
- [x] Considerar prefijos por tipo de muestra
|
||||
|
||||
### 4. Actualizar vistas de sale.order ✅
|
||||
**Archivo:** `lims_management/views/sale_order_views.xml`
|
||||
- [x] Agregar pestaña "Muestras Generadas" en formulario de orden
|
||||
- [x] Mostrar campo `generated_sample_ids` con vista de lista embebida
|
||||
- [x] Agregar botón para regenerar muestras (si es necesario)
|
||||
- [x] Incluir indicadores visuales del estado de generación
|
||||
|
||||
### 5. Crear wizard de configuración (opcional)
|
||||
**Archivos:**
|
||||
- `lims_management/wizard/sample_generation_wizard.py`
|
||||
- `lims_management/wizard/sample_generation_wizard_view.xml`
|
||||
- [ ] Crear wizard para revisar/modificar la generación antes de confirmar
|
||||
- [ ] Permitir ajustes manuales de agrupación si es necesario
|
||||
- [ ] Opción para excluir ciertos análisis de la generación automática
|
||||
|
||||
### 6. Notificaciones y alertas ✅
|
||||
**Archivo:** `lims_management/models/sale_order.py`
|
||||
- [x] Implementar sistema de notificaciones:
|
||||
- Análisis sin tipo de muestra definido
|
||||
- Muestras generadas exitosamente
|
||||
- Errores en la generación
|
||||
- [x] Usar el sistema de mensajería de Odoo (`mail.thread`)
|
||||
|
||||
### 7. Pruebas y validación ✅
|
||||
**Archivo:** `verify_automatic_sample_generation.py`
|
||||
- [x] Crear script de verificación que pruebe:
|
||||
- Generación correcta de muestras
|
||||
- Agrupación adecuada de análisis
|
||||
- Cálculo correcto de volúmenes
|
||||
- Unicidad de códigos de barras
|
||||
- Manejo de casos edge (análisis sin tipo de muestra)
|
||||
|
||||
### 8. Actualizar datos de demostración ✅
|
||||
**Archivo:** `lims_management/demo/z_automatic_generation_demo.xml`
|
||||
- [x] Crear órdenes de laboratorio de ejemplo que demuestren:
|
||||
- Orden con múltiples análisis del mismo tipo de muestra
|
||||
- Orden con análisis de diferentes tipos de muestra
|
||||
- Orden mixta con algunos análisis sin tipo de muestra
|
||||
|
||||
## Consideraciones Técnicas
|
||||
|
||||
### Performance
|
||||
- La generación debe ser eficiente incluso con órdenes grandes (20+ análisis)
|
||||
- Usar creación en batch para múltiples muestras
|
||||
- Considerar uso de SQL para verificación de unicidad de barcodes
|
||||
|
||||
### Transaccionalidad
|
||||
- Todo el proceso debe ser atómico: o se generan todas las muestras o ninguna
|
||||
- Usar `@api.model` con manejo adecuado de excepciones
|
||||
- Rollback automático en caso de error
|
||||
|
||||
### Configurabilidad
|
||||
- Considerar agregar configuración a nivel de compañía:
|
||||
- Habilitar/deshabilitar generación automática
|
||||
- Formato de código de barras personalizable
|
||||
- Reglas de agrupación personalizables
|
||||
|
||||
### Compatibilidad
|
||||
- Mantener compatibilidad con flujo manual existente
|
||||
- Permitir creación manual de muestras adicionales si es necesario
|
||||
- No interferir con órdenes de venta regulares (no laboratorio)
|
||||
|
||||
## Flujo de Trabajo
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Orden de Laboratorio] --> B{¿Confirmar Orden?}
|
||||
B -->|Sí| C[Analizar Líneas de Orden]
|
||||
C --> D[Identificar Análisis]
|
||||
D --> E[Agrupar por Tipo de Muestra]
|
||||
E --> F{¿Todos tienen tipo de muestra?}
|
||||
F -->|No| G[Generar Advertencia]
|
||||
F -->|Sí| H[Continuar]
|
||||
G --> H
|
||||
H --> I[Crear Muestras por Grupo]
|
||||
I --> J[Generar Códigos de Barras]
|
||||
J --> K[Asociar a la Orden]
|
||||
K --> L[Confirmar Orden]
|
||||
L --> M[Notificar Usuario]
|
||||
```
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
1. [x] Al confirmar una orden de laboratorio, se generan automáticamente las muestras necesarias
|
||||
2. [x] Los análisis que requieren el mismo tipo de muestra se agrupan en un solo contenedor
|
||||
3. [x] Cada muestra tiene un código de barras único
|
||||
4. [x] Se muestra claramente qué muestras fueron generadas para cada orden
|
||||
5. [x] Se manejan adecuadamente los análisis sin tipo de muestra definido
|
||||
6. [x] El sistema registra un log de la generación para auditoría
|
||||
7. [ ] La funcionalidad se puede deshabilitar si es necesario (opcional - no implementado)
|
||||
8. [x] No afecta el rendimiento de confirmación de órdenes regulares
|
||||
|
||||
## Estimación de Tiempo
|
||||
|
||||
- Tarea 1-2: 2-3 horas (lógica principal)
|
||||
- Tarea 3: 1 hora (mejoras barcode)
|
||||
- Tarea 4: 1 hora (vistas)
|
||||
- Tarea 5: 2 horas (wizard opcional)
|
||||
- Tarea 6: 1 hora (notificaciones)
|
||||
- Tarea 7-8: 1-2 horas (pruebas y demo)
|
||||
|
||||
**Total estimado: 8-10 horas**
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Completo**: Issue #44 (Relaciones test-muestra) ✓
|
||||
- **Requerido**: Módulo `stock` de Odoo para `stock.lot`
|
||||
- **Requerido**: Librería `python-barcode` para generación de códigos
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
1. **Riesgo**: Conflictos con otros módulos que modifiquen `sale.order.action_confirm()`
|
||||
- **Mitigación**: Usar `super()` correctamente y documentar la integración
|
||||
|
||||
2. **Riesgo**: Rendimiento con órdenes muy grandes
|
||||
- **Mitigación**: Implementar creación en batch y considerar procesamiento asíncrono
|
||||
|
||||
3. **Riesgo**: Duplicación de códigos de barras
|
||||
- **Mitigación**: Implementar verificación robusta y regeneración si es necesario
|
|
@ -1,160 +0,0 @@
|
|||
# Plan de Implementación - Issue #44: Agregar relación entre análisis y tipos de muestra
|
||||
|
||||
## Objetivo
|
||||
Establecer una relación entre los productos tipo análisis (tests) y los tipos de muestra que requieren, para permitir la automatización de generación de muestras al confirmar órdenes de laboratorio.
|
||||
|
||||
## Análisis Previo
|
||||
|
||||
### Situación Actual
|
||||
- Los productos tipo análisis (`is_analysis=True`) no tienen campo para indicar qué tipo de muestra requieren
|
||||
- Los productos tipo muestra (`is_sample_type=True`) existen pero no están relacionados con los análisis
|
||||
- El modelo `stock.lot` tiene `container_type` como Selection hardcodeado, no como relación con productos
|
||||
|
||||
### Impacto
|
||||
- Sin esta relación, no es posible automatizar la generación de muestras (Issue #32)
|
||||
- No se puede validar que se use el contenedor correcto para cada análisis
|
||||
- Dificulta la agrupación de análisis que usan el mismo tipo de muestra
|
||||
|
||||
## Tareas de Implementación
|
||||
|
||||
### 1. Modificar el modelo ProductTemplate
|
||||
- **Archivo:** `lims_management/models/product.py`
|
||||
- **Tareas:**
|
||||
- [x] Agregar campo `required_sample_type_id`:
|
||||
```python
|
||||
required_sample_type_id = fields.Many2one(
|
||||
'product.template',
|
||||
string='Tipo de Muestra Requerida',
|
||||
domain="[('is_sample_type', '=', True)]",
|
||||
help="Tipo de muestra/contenedor requerido para realizar este análisis"
|
||||
)
|
||||
```
|
||||
- [x] Agregar validación para asegurar que solo se puede asignar a productos con `is_analysis=True`
|
||||
- [x] Considerar agregar campo `sample_volume_ml` para indicar volumen requerido
|
||||
|
||||
### 2. Actualizar el modelo StockLot
|
||||
- **Archivo:** `lims_management/models/stock_lot.py`
|
||||
- **Tareas:**
|
||||
- [ ] **Opción A - Migrar container_type a Many2one:**
|
||||
```python
|
||||
# Deprecar el campo Selection actual
|
||||
container_type_legacy = fields.Selection([...], deprecated=True)
|
||||
|
||||
# Nuevo campo relacional
|
||||
sample_type_product_id = fields.Many2one(
|
||||
'product.template',
|
||||
string='Tipo de Muestra',
|
||||
domain="[('is_sample_type', '=', True)]"
|
||||
)
|
||||
```
|
||||
- [ ] **Opción B - Mantener ambos campos:**
|
||||
- Mantener `container_type` para compatibilidad
|
||||
- Agregar `sample_type_product_id` como campo principal
|
||||
- Sincronizar ambos campos con un @api.onchange
|
||||
- [ ] Agregar método para obtener el nombre del contenedor desde el producto
|
||||
|
||||
### 3. Actualizar las vistas
|
||||
|
||||
#### 3.1 Vista de Producto (Análisis)
|
||||
- **Archivo:** `lims_management/views/product_views.xml`
|
||||
- **Tareas:**
|
||||
- [ ] Agregar campo `required_sample_type_id` en el formulario cuando `is_analysis=True`
|
||||
- [ ] Mostrarlo en la pestaña de especificaciones técnicas
|
||||
- [ ] Agregar en la vista lista de análisis
|
||||
|
||||
#### 3.2 Vista de Stock Lot
|
||||
- **Archivo:** `lims_management/views/stock_lot_views.xml`
|
||||
- **Tareas:**
|
||||
- [ ] Reemplazar/actualizar el campo `container_type` con `sample_type_product_id`
|
||||
- [ ] Actualizar vistas de lista y formulario
|
||||
- [ ] Considerar mostrar imagen del contenedor desde el producto
|
||||
|
||||
### 4. Migración de datos existentes
|
||||
- **Archivo:** `lims_management/migrations/18.0.1.1.0/post-migration.py`
|
||||
- **Tareas:**
|
||||
- [ ] Crear script de migración para mapear valores de `container_type` a productos:
|
||||
```python
|
||||
mapping = {
|
||||
'serum_tube': 'lims_management.sample_type_serum_tube',
|
||||
'edta_tube': 'lims_management.sample_type_edta_tube',
|
||||
'urine': 'lims_management.sample_type_urine_container',
|
||||
# etc...
|
||||
}
|
||||
```
|
||||
- [ ] Actualizar registros `stock.lot` existentes con el producto correspondiente
|
||||
- [ ] Marcar `container_type` como deprecated
|
||||
|
||||
### 5. Actualizar datos de demostración
|
||||
- **Archivos:**
|
||||
- `lims_management/demo/z_analysis_demo.xml`
|
||||
- `lims_management/demo/z_sample_demo.xml`
|
||||
- **Tareas:**
|
||||
- [ ] Asignar `required_sample_type_id` a cada análisis de demo:
|
||||
- Hemograma → Tubo EDTA
|
||||
- Glucosa → Tubo Suero
|
||||
- Urocultivo → Contenedor Orina
|
||||
- etc.
|
||||
- [ ] Verificar que todos los tipos de muestra necesarios estén creados
|
||||
|
||||
### 6. Crear datos iniciales de tipos de muestra
|
||||
- **Archivo:** `lims_management/data/sample_types.xml`
|
||||
- **Tareas:**
|
||||
- [ ] Crear productos para tipos de muestra comunes:
|
||||
```xml
|
||||
<record id="sample_type_serum_tube" model="product.template">
|
||||
<field name="name">Tubo de Suero (Tapa Roja)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
</record>
|
||||
```
|
||||
- [ ] Incluir todos los tipos básicos: EDTA, Suero, Orina, Hisopado, etc.
|
||||
|
||||
### 7. Documentación y pruebas
|
||||
- **Tareas:**
|
||||
- [ ] Actualizar README o documentación técnica
|
||||
- [ ] Crear script de verificación `verify_sample_relationships.py`
|
||||
- [ ] Pruebas manuales:
|
||||
- Crear nuevo análisis y asignar tipo de muestra
|
||||
- Verificar que la relación se guarda correctamente
|
||||
- Crear stock.lot y verificar el nuevo campo
|
||||
- Probar migración con datos existentes
|
||||
|
||||
### 8. Preparación para Issue #32
|
||||
- **Tareas:**
|
||||
- [ ] Documentar cómo usar la nueva relación para automatización
|
||||
- [ ] Identificar lógica de agrupación (múltiples análisis → misma muestra)
|
||||
- [ ] Considerar reglas de negocio adicionales:
|
||||
- ¿Qué pasa si un análisis no tiene tipo de muestra asignado?
|
||||
- ¿Se pueden hacer múltiples análisis con la misma muestra física?
|
||||
|
||||
## Consideraciones Técnicas
|
||||
|
||||
### Compatibilidad hacia atrás
|
||||
- Mantener el campo `container_type` temporalmente para no romper integraciones existentes
|
||||
- Usar decorador `@api.depends` para sincronizar valores
|
||||
|
||||
### Performance
|
||||
- Indexar el campo `is_sample_type` si no está indexado
|
||||
- Considerar vista SQL para reportes que unan análisis con tipos de muestra
|
||||
|
||||
### Seguridad
|
||||
- Solo usuarios con permisos de edición de productos pueden modificar `required_sample_type_id`
|
||||
- Validar que no se pueda eliminar un tipo de muestra si está siendo usado por algún análisis
|
||||
|
||||
## Orden de Ejecución
|
||||
1. Crear tipos de muestra en data inicial
|
||||
2. Modificar modelos (product.py, stock_lot.py)
|
||||
3. Actualizar vistas
|
||||
4. Actualizar datos demo
|
||||
5. Crear y ejecutar migración
|
||||
6. Pruebas exhaustivas
|
||||
7. Documentación
|
||||
|
||||
## Criterios de Aceptación
|
||||
- [ ] Cada análisis puede tener asignado un tipo de muestra
|
||||
- [ ] Los stock.lot pueden referenciar productos tipo muestra
|
||||
- [ ] Migración exitosa de datos existentes
|
||||
- [ ] Vistas actualizadas y funcionales
|
||||
- [ ] Sin errores en logs de Odoo
|
||||
- [ ] Datos demo coherentes y completos
|
|
@ -1,58 +0,0 @@
|
|||
# Plan de Desarrollo: Issue #6 - Solicitudes de Laboratorio
|
||||
|
||||
## Análisis
|
||||
|
||||
El objetivo de este issue es implementar la funcionalidad para que un recepcionista pueda registrar una **"Solicitud de Laboratorio"**. Esta solicitud debe estar vinculada a un paciente, a un médico remitente (opcional) y debe contener los análisis clínicos que se realizarán.
|
||||
|
||||
Basándose en los documentos de diseño (`ToBeDesing.md`) y los requerimientos, la estrategia principal es **reutilizar el modelo de Órdenes de Venta (`sale.order`) de Odoo** para representar las solicitudes de laboratorio. Esta decisión es clave porque aprovecha el flujo de facturación y contabilidad ya existente en Odoo, evitando desarrollar una lógica de cobro paralela y asegurando una integración nativa.
|
||||
|
||||
El trabajo realizado en el **Issue #5** ya nos proporciona el "Catálogo de Análisis Clínicos" como productos de tipo servicio, que serán los elementos que se añadirán a estas solicitudes.
|
||||
|
||||
Por lo tanto, el plan se centrará en adaptar y extender el modelo `sale.order` para que se comporte y se presente al usuario como una "Solicitud de Laboratorio".
|
||||
|
||||
---
|
||||
|
||||
## Plan de Actividades
|
||||
|
||||
- **1. Extender el Modelo `sale.order`:**
|
||||
- [x] Crear el archivo `lims_management/models/sale_order.py`.
|
||||
- [x] Heredar del modelo `sale.order` para añadir los siguientes campos:
|
||||
- `is_lab_request` (Booleano): Un campo técnico para identificar que la orden de venta es una solicitud de laboratorio. Será invisible en la interfaz y se usará para filtrar y aplicar lógica específica.
|
||||
- `doctor_id` (Many2one a `res.partner`): Para seleccionar al médico que remite la solicitud. Se debe aplicar un dominio para que solo muestre los contactos que estén marcados como doctores (`is_doctor = True`).
|
||||
- [x] Añadir el nuevo archivo `sale_order.py` al `__init__.py` de la carpeta `models`.
|
||||
|
||||
- **2. Crear Vistas para Solicitudes de Laboratorio:**
|
||||
- [x] Crear el archivo de vistas `lims_management/views/sale_order_views.xml`.
|
||||
- [x] **Heredar la vista de formulario de `sale.order`** para:
|
||||
- [x] Añadir el campo `doctor_id` cerca del campo del paciente.
|
||||
- [x] Cambiar la etiqueta (string) del campo `partner_id` de "Cliente" a "Paciente".
|
||||
- [x] **(Nuevo)** Aplicar un dominio al campo `partner_id` para que solo muestre contactos que sean pacientes (`is_patient = True`).
|
||||
- [x] **(Nuevo)** Corregir y asegurar que el dominio en el campo `product_template_id` de las líneas de la orden restrinja la selección únicamente a análisis clínicos (`is_analysis = True`).
|
||||
- [x] **Heredar la vista de lista (tree/list) de `sale.order`** para:
|
||||
- Añadir la columna "Médico Remitente" (`doctor_id`).
|
||||
|
||||
- **3. Crear Men<65><6E> y Acción de Ventana:**
|
||||
- [x] Modificar el archivo `lims_management/views/menus.xml`.
|
||||
- [x] Crear una nueva **Acción de Ventana** (`ir.actions.act_window`) para las solicitudes de laboratorio:
|
||||
- `name`: "Solicitudes de Laboratorio".
|
||||
- `res_model`: `sale.order`.
|
||||
- `view_mode`: `list,form`.
|
||||
- `domain`: `[('is_lab_request', '=', True)]` para mostrar solo las solicitudes de laboratorio.
|
||||
- `context`: `{'default_is_lab_request': True}` para que las nuevas solicitudes se marquen correctamente por defecto.
|
||||
- [x] Crear un nuevo `menuitem` llamado "Solicitudes de Laboratorio" bajo el menú principal de "Laboratorio", que dispare la acción anterior.
|
||||
|
||||
- **4. Actualizar Manifiesto y Seguridad:**
|
||||
- [x] Añadir la dependencia del módulo `sale` en el archivo `__manifest__.py`.
|
||||
- [x] Añadir el nuevo archivo de vistas `sale_order_views.xml` a la lista `data` en `__manifest__.py`.
|
||||
- [x] Asegurar que los grupos de usuarios del laboratorio (ej. Recepcionista) tengan los permisos adecuados para crear y modificar órdenes de venta (`sale.order`).
|
||||
|
||||
- **5. Crear Datos de Demostración:**
|
||||
- [x] Crear un nuevo archivo de datos de demostración para las solicitudes de laboratorio.
|
||||
- [x] Definir al menos dos solicitudes de ejemplo que incluyan diferentes análisis y pacientes.
|
||||
- [x] Añadir el nuevo archivo a la clave `demo` en `__manifest__.py`.
|
||||
|
||||
- **6. Verificación Final:**
|
||||
- [x] Reiniciar la instancia de Odoo para aplicar los cambios.
|
||||
- [x] Verificar en la interfaz que el nuevo menú "Solicitudes de Laboratorio" aparece y funciona.
|
||||
- [x] Comprobar que al crear una nueva solicitud, solo se puedan añadir análisis del catálogo y que se pueda seleccionar un médico remitente.
|
||||
- [x] Revisar los logs para asegurar que no haya errores.
|
|
@ -1,57 +0,0 @@
|
|||
# Plan de Actividades: Issue #7 - Gestión de Muestras de Laboratorio
|
||||
|
||||
## Objetivo
|
||||
|
||||
Extender el modelo de Lotes/Números de Serie de Odoo (`stock.lot`) para representar y gestionar las **Muestras de Laboratorio**. Esto permitirá la trazabilidad completa de la muestra desde su recolección hasta el análisis.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] **Extender el Modelo de Lote/Número de Serie (`stock.lot`):**
|
||||
- [x] Crear el archivo `lims_management/models/stock_lot.py`.
|
||||
- [x] Heredar del modelo `stock.lot`.
|
||||
- [x] Añadir campos: `is_lab_sample`, `patient_id`, `request_id`, `collection_date`, `container_type`.
|
||||
- [ ] **(Nuevo)** Añadir campo `collector_id` (Many2one a `res.users`) para registrar quién tomó la muestra.
|
||||
|
||||
- [x] **Adaptar las Vistas de Lote/Número de Serie:**
|
||||
- [x] Crear el archivo `lims_management/views/stock_lot_views.xml`.
|
||||
- [x] Crear vistas de lista y formulario para las muestras.
|
||||
- [x] Crear un producto de servicio por defecto para las muestras.
|
||||
- [ ] **(Nuevo)** Añadir el campo `collector_id` a las vistas de lista y formulario.
|
||||
|
||||
- [x] **Crear el Menú "Gestión de Muestras":**
|
||||
- [x] Modificar `lims_management/views/menus.xml`.
|
||||
- [x] Crear acción de ventana y `menuitem` para `stock.lot` con el dominio y contexto adecuados.
|
||||
|
||||
- [x] **Establecer Permisos y Reglas de Dominio:**
|
||||
- [x] Modificar `lims_management/security/ir.model.access.csv` para dar permisos sobre `stock.lot`.
|
||||
- [x] Añadir dominios en las vistas para los campos relacionales.
|
||||
|
||||
- [x] **Actualizar el Manifiesto (`__manifest__.py`):**
|
||||
- [x] Añadir nuevos archivos de modelos, vistas y datos al manifiesto.
|
||||
|
||||
- [x] **Verificación Final:**
|
||||
- [x] Reiniciar y verificar la instancia de Odoo.
|
||||
|
||||
- [x] **Mejorar Modelo de Productos para Tipos de Muestra:**
|
||||
- [x] Añadir un campo booleano `is_sample_type` al modelo `product.template`.
|
||||
|
||||
- [x] **Crear Menú para "Tipos de Muestra":**
|
||||
- [x] Añadir acción de ventana y `menuitem` para los tipos de muestra.
|
||||
|
||||
- [x] **Actualizar Vista de Muestras (`stock.lot`):**
|
||||
- [x] Hacer visible y aplicar dominio al campo `product_id` (Tipo de Muestra).
|
||||
- [x] Eliminar el producto genérico y su referencia en el contexto.
|
||||
|
||||
- [x] **Crear Datos de Demostración:**
|
||||
- [x] Crear archivo `demo/z_sample_demo.xml` con tipos de muestra y muestras de ejemplo.
|
||||
- [x] Añadir el archivo de demostración al manifiesto.
|
||||
- [ ] **(Nuevo)** Actualizar los datos de demostración para incluir el `collector_id`.
|
||||
|
||||
- [x] **Verificación Final (con Demo):**
|
||||
- [x] Validar la funcionalidad completa con los datos de demostración.
|
||||
|
||||
---
|
||||
## Consideraciones Futuras (Siguientes Issues)
|
||||
|
||||
- **Ciclo de Vida de la Muestra:** Implementar un campo de estado (`state`) con su lógica de transiciones (ej. 'Recolectada' -> 'Recibida' -> 'En Proceso' -> 'Completada' -> 'Almacenada').
|
||||
- **Informes de Muestras:** Crear informes en PDF o vistas dinámicas sobre el estado y trazabilidad de las muestras.
|
|
@ -1,163 +0,0 @@
|
|||
# Plan de Implementación - Issue #8: Gestión de Pruebas y Resultados
|
||||
|
||||
## Objetivo
|
||||
Implementar los modelos y la interfaz básica para la gestión de pruebas y resultados de laboratorio, específicamente los modelos `lims.test` y `lims.result` con entrada dinámica de resultados.
|
||||
|
||||
## Análisis de Requisitos
|
||||
|
||||
### Funcionalidad Esperada (según Issue #8)
|
||||
1. **Modelo lims.test**: Representar la ejecución de un análisis con estados
|
||||
2. **Modelo lims.result**: Almacenar cada valor de resultado con soporte para múltiples tipos
|
||||
3. **Interfaz de entrada dinámica**: Vista formulario con lista editable de resultados
|
||||
4. **Resaltado visual**: Mostrar en rojo los resultados fuera de rango
|
||||
5. **Validación opcional**: Permitir configurar si se requiere validación por administrador
|
||||
|
||||
### Modelos de Datos Requeridos (según Issue #8)
|
||||
1. **lims.test**: Representa la ejecución de un análisis
|
||||
2. **lims.result**: Almacena cada valor de resultado
|
||||
3. **lims.test.parameter**: Modelo referenciado (asumimos ya existe o se creará)
|
||||
|
||||
## Tareas de Implementación
|
||||
|
||||
### 1. Crear modelo lims.test
|
||||
**Archivo:** `lims_management/models/lims_test.py`
|
||||
- [ ] Definir modelo según especificación del issue:
|
||||
```python
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Línea de Orden')
|
||||
patient_id = fields.Many2one('res.partner', string='Paciente',
|
||||
related='sale_order_line_id.order_id.partner_id')
|
||||
product_id = fields.Many2one('product.product', string='Análisis',
|
||||
related='sale_order_line_id.product_id')
|
||||
sample_id = fields.Many2one('stock.lot', string='Muestra')
|
||||
state = fields.Selection([
|
||||
('draft', 'Borrador'),
|
||||
('in_process', 'En Proceso'),
|
||||
('result_entered', 'Resultado Ingresado'),
|
||||
('validated', 'Validado'),
|
||||
('cancelled', 'Cancelado')
|
||||
], string='Estado', default='draft')
|
||||
validator_id = fields.Many2one('res.users', string='Validador')
|
||||
validation_date = fields.Datetime(string='Fecha de Validación')
|
||||
require_validation = fields.Boolean(string='Requiere Validación',
|
||||
compute='_compute_require_validation')
|
||||
```
|
||||
- [ ] Implementar _compute_require_validation basado en configuración
|
||||
- [ ] Agregar métodos de transición de estados
|
||||
|
||||
### 2. Crear modelo lims.result
|
||||
**Archivo:** `lims_management/models/lims_result.py`
|
||||
- [ ] Definir modelo según especificación:
|
||||
```python
|
||||
test_id = fields.Many2one('lims.test', string='Prueba', required=True, ondelete='cascade')
|
||||
parameter_id = fields.Many2one('lims.test.parameter', string='Parámetro')
|
||||
value_numeric = fields.Float(string='Valor Numérico')
|
||||
value_text = fields.Char(string='Valor de Texto')
|
||||
value_selection = fields.Selection([], string='Valor de Selección')
|
||||
is_out_of_range = fields.Boolean(string='Fuera de Rango', compute='_compute_is_out_of_range')
|
||||
notes = fields.Text(string='Notas del Técnico')
|
||||
```
|
||||
- [ ] Implementar _compute_is_out_of_range para detectar valores anormales
|
||||
- [ ] Agregar validación para asegurar que solo un tipo de valor esté lleno
|
||||
|
||||
### 3. Desarrollar interfaz de ingreso de resultados
|
||||
**Archivo:** `lims_management/views/lims_test_views.xml`
|
||||
- [ ] Crear vista formulario para lims.test con:
|
||||
- Información de cabecera (paciente, análisis, muestra)
|
||||
- Lista editable (One2many) de lims.result
|
||||
- Campos dinámicos según parámetros del análisis
|
||||
- [ ] Implementar widget o CSS para resaltar en rojo valores fuera de rango
|
||||
- [ ] Agregar botones de acción según estado
|
||||
|
||||
### 4. Implementar lógica visual para valores fuera de rango
|
||||
**Archivo:** `lims_management/static/src/` (CSS/JS)
|
||||
- [ ] Crear CSS para clase .out-of-range con color rojo
|
||||
- [ ] Implementar widget o computed field que aplique la clase
|
||||
- [ ] Asegurar que funcione en vista formulario y lista
|
||||
|
||||
### 5. Agregar configuración de validación opcional
|
||||
**Archivo:** `lims_management/models/res_config_settings.py`
|
||||
- [ ] Agregar campo booleano lims_require_validation
|
||||
- [ ] Extender res.config.settings para incluir esta configuración
|
||||
- [ ] Modificar lims.test para usar esta configuración en flujo de trabajo
|
||||
|
||||
### 6. Crear vistas básicas
|
||||
**Archivo:** `lims_management/views/lims_test_views.xml`
|
||||
- [ ] Vista lista de pruebas con campos básicos
|
||||
- [ ] Vista kanban agrupada por estado
|
||||
- [ ] Menú de acceso en Laboratorio > Pruebas
|
||||
|
||||
### 7. Crear datos de demostración básicos
|
||||
**Archivo:** `lims_management/demo/lims_test_demo.xml`
|
||||
- [ ] Crear algunos registros lims.test de ejemplo
|
||||
- [ ] Agregar resultados de demostración
|
||||
- [ ] Incluir casos con valores dentro y fuera de rango
|
||||
|
||||
|
||||
## Consideraciones Técnicas
|
||||
|
||||
### Performance
|
||||
- Usar compute fields con store=True para is_out_of_range
|
||||
- Carga eficiente de parámetros relacionados
|
||||
|
||||
### Usabilidad
|
||||
- Interfaz clara para entrada de resultados
|
||||
- Feedback visual inmediato para valores fuera de rango
|
||||
- Navegación intuitiva entre estados
|
||||
|
||||
### Validación de Datos
|
||||
- Solo un tipo de valor debe estar lleno por resultado
|
||||
- Validar que el parámetro corresponda al análisis
|
||||
- Estados coherentes con el flujo de trabajo
|
||||
|
||||
## Flujo de Trabajo
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Línea de Orden] --> B[Crear lims.test]
|
||||
B --> C[Estado: draft]
|
||||
C --> D[Estado: in_process]
|
||||
D --> E[Técnico Ingresa Resultados]
|
||||
E --> F[Estado: result_entered]
|
||||
F --> G{¿Requiere Validación?}
|
||||
G -->|Sí| H[Esperar Validación]
|
||||
G -->|No| I[Proceso Completo]
|
||||
H --> J[Estado: validated]
|
||||
```
|
||||
|
||||
## Criterios de Aceptación (según Issue #8)
|
||||
|
||||
1. [ ] Modelo lims.test creado con todos los campos especificados
|
||||
2. [ ] Modelo lims.result creado con soporte para múltiples tipos de valor
|
||||
3. [ ] Interfaz de formulario con lista editable de resultados
|
||||
4. [ ] Valores fuera de rango se muestran en rojo
|
||||
5. [ ] La validación por administrador es configurable
|
||||
6. [ ] Los campos relacionados (patient_id, product_id) funcionan correctamente
|
||||
|
||||
## Estimación de Tiempo
|
||||
|
||||
- Tarea 1: 2 horas (modelo lims.test)
|
||||
- Tarea 2: 1.5 horas (modelo lims.result)
|
||||
- Tarea 3: 2 horas (interfaz de entrada)
|
||||
- Tarea 4: 1 hora (lógica visual)
|
||||
- Tarea 5: 1 hora (configuración)
|
||||
- Tareas 6-7: 1.5 horas (vistas y demo)
|
||||
|
||||
**Total estimado: 9 horas**
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Issue #31: Configuración inicial del módulo ✓
|
||||
- Issue #32: Generación automática de muestras ✓
|
||||
- Modelo lims.test.parameter (debe existir o crearse)
|
||||
- Módulos de Odoo: sale, stock
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
1. **Riesgo**: El modelo lims.test.parameter no está definido
|
||||
- **Mitigación**: Crear modelo básico o usar product.product temporalmente
|
||||
|
||||
2. **Riesgo**: Complejidad en la detección de valores fuera de rango
|
||||
- **Mitigación**: Implementar lógica simple inicialmente
|
||||
|
||||
3. **Riesgo**: Integración con flujo existente de órdenes
|
||||
- **Mitigación**: Crear pruebas manualmente en primera versión
|
|
@ -1,173 +0,0 @@
|
|||
# Plan de Implementación - Issue #51: Catálogo de Parámetros de Laboratorio
|
||||
|
||||
## Objetivo
|
||||
Implementar un catálogo maestro de parámetros de laboratorio con configuración por análisis y rangos de referencia flexibles basados en edad, sexo y otras condiciones del paciente.
|
||||
|
||||
## Arquitectura Propuesta
|
||||
|
||||
### Modelos Principales
|
||||
1. **lims.analysis.parameter** - Catálogo maestro de parámetros
|
||||
2. **product.template.parameter** - Asociación parámetro-análisis
|
||||
3. **lims.parameter.range** - Rangos de referencia flexibles
|
||||
4. **lims.result** (modificado) - Usar parameter_id en lugar de parameter_name
|
||||
|
||||
## Fases de Implementación
|
||||
|
||||
### Fase 1: Creación de Modelos Base (Tasks 1-4)
|
||||
**Objetivo**: Establecer la estructura de datos fundamental
|
||||
|
||||
#### Task 1: Crear modelo lims.analysis.parameter
|
||||
- Crear archivo `lims_management/models/analysis_parameter.py`
|
||||
- Definir campos: name, code, value_type, unit, selection_values, description, active
|
||||
- Implementar constraints y validaciones
|
||||
- Crear vistas (list, form) para gestión del catálogo
|
||||
- Agregar menú de configuración
|
||||
- Crear permisos de seguridad
|
||||
|
||||
#### Task 2: Crear modelo product.template.parameter
|
||||
- Crear archivo `lims_management/models/product_template_parameter.py`
|
||||
- Definir relación entre product.template y lims.analysis.parameter
|
||||
- Implementar campos: sequence, required, instructions
|
||||
- Agregar constraint de unicidad
|
||||
- Crear vista embebida en product.template
|
||||
- Actualizar herencia de product.template
|
||||
|
||||
#### Task 3: Crear modelo lims.parameter.range
|
||||
- Crear archivo `lims_management/models/parameter_range.py`
|
||||
- Implementar campos de condiciones: gender, age_min, age_max, pregnant
|
||||
- Implementar campos de valores: normal_min/max, critical_min/max
|
||||
- Crear método _compute_name()
|
||||
- Agregar constraint de unicidad
|
||||
- Crear vistas de configuración
|
||||
|
||||
#### Task 4: Agregar método _compute_age() en res.partner
|
||||
- Extender modelo res.partner
|
||||
- Implementar cálculo de edad basado en birth_date
|
||||
- Agregar campo is_pregnant (Boolean)
|
||||
- Crear tests unitarios para el cálculo
|
||||
|
||||
### Fase 2: Migración y Adaptación (Tasks 5-7)
|
||||
**Objetivo**: Adaptar el sistema existente al nuevo modelo
|
||||
|
||||
#### Task 5: Modificar modelo lims.result
|
||||
- Cambiar parameter_name (Char) a parameter_id (Many2one)
|
||||
- Mantener parameter_name como campo related (compatibilidad)
|
||||
- Implementar _compute_applicable_range()
|
||||
- Actualizar _compute_is_out_of_range() para usar rangos flexibles
|
||||
- Crear script de migración de datos
|
||||
|
||||
#### Task 6: Actualizar generación automática de resultados
|
||||
- Modificar _generate_test_results() en lims.test
|
||||
- Generar líneas basadas en product.template.parameter
|
||||
- Respetar orden (sequence) y obligatoriedad
|
||||
- Asignar tipos de dato correctos
|
||||
|
||||
#### Task 7: Eliminar modelo obsoleto lims.analysis.range
|
||||
- Remover archivo del modelo
|
||||
- Eliminar referencias en product.template
|
||||
- Actualizar vistas que lo referencian
|
||||
- Limpiar datos de demo
|
||||
- Actualizar __init__.py y __manifest__.py
|
||||
|
||||
### Fase 3: Interfaz de Usuario (Tasks 8-10)
|
||||
**Objetivo**: Crear interfaces intuitivas para configuración y uso
|
||||
|
||||
#### Task 8: Crear vistas de configuración de parámetros
|
||||
- Vista de catálogo de parámetros (búsqueda, filtros)
|
||||
- Formulario de parámetro con smart buttons
|
||||
- Vista de configuración de parámetros por análisis
|
||||
- Vista de rangos con filtros por parámetro
|
||||
|
||||
#### Task 9: Actualizar vistas de ingreso de resultados
|
||||
- Adaptar formulario de lims.result
|
||||
- Mostrar tipo de dato esperado
|
||||
- Validación en tiempo real
|
||||
- Indicadores visuales de valores fuera de rango
|
||||
- Mostrar rango aplicable según paciente
|
||||
|
||||
#### Task 10: Crear wizards de configuración masiva
|
||||
- Wizard para copiar configuración entre análisis
|
||||
- Wizard para importar parámetros desde CSV
|
||||
- Wizard para aplicar rangos a múltiples parámetros
|
||||
|
||||
### Fase 4: Datos y Validación (Tasks 11-13)
|
||||
**Objetivo**: Poblar el sistema con datos útiles y validar funcionamiento
|
||||
|
||||
#### Task 11: Crear datos de demostración
|
||||
- Parámetros comunes de hematología
|
||||
- Parámetros de química sanguínea
|
||||
- Configuración para análisis existentes
|
||||
- Rangos por edad/sexo realistas
|
||||
- Casos de prueba especiales
|
||||
|
||||
#### Task 12: Desarrollar tests automatizados
|
||||
- Tests unitarios para modelos
|
||||
- Tests de integración para flujos
|
||||
- Tests de validación de rangos
|
||||
- Tests de migración de datos
|
||||
- Tests de rendimiento
|
||||
|
||||
#### Task 13: Actualizar reportes
|
||||
- Modificar report_test_result
|
||||
- Incluir información del catálogo
|
||||
- Mostrar rangos aplicables
|
||||
- Resaltar valores anormales
|
||||
- Agregar interpretación cuando esté disponible
|
||||
|
||||
## Consideraciones Técnicas
|
||||
|
||||
### Migración de Datos
|
||||
- Script Python para migrar parameter_name existentes
|
||||
- Crear parámetros automáticamente desde histórico
|
||||
- Mantener compatibilidad durante transición
|
||||
- Backup antes de migración
|
||||
|
||||
### Performance
|
||||
- Índices en campos de búsqueda frecuente
|
||||
- Cache para rangos aplicables
|
||||
- Lazy loading en vistas con muchos parámetros
|
||||
|
||||
### Seguridad
|
||||
- Solo administradores pueden crear/modificar catálogo
|
||||
- Técnicos pueden ver pero no editar parámetros
|
||||
- Logs de auditoría para cambios en rangos
|
||||
|
||||
## Cronograma Estimado
|
||||
|
||||
- **Fase 1**: 2-3 días (Modelos base y estructura)
|
||||
- **Fase 2**: 2 días (Migración y adaptación)
|
||||
- **Fase 3**: 2 días (Interfaces de usuario)
|
||||
- **Fase 4**: 1-2 días (Datos y validación)
|
||||
|
||||
**Total estimado**: 7-9 días de desarrollo
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
1. **Riesgo**: Pérdida de datos durante migración
|
||||
- **Mitigación**: Scripts de backup y rollback
|
||||
|
||||
2. **Riesgo**: Resistencia al cambio de usuarios
|
||||
- **Mitigación**: Mantener compatibilidad temporal, capacitación
|
||||
|
||||
3. **Riesgo**: Complejidad en rangos múltiples
|
||||
- **Mitigación**: UI intuitiva, valores por defecto sensatos
|
||||
|
||||
## Criterios de Éxito
|
||||
|
||||
- [ ] Todos los tests automatizados pasan
|
||||
- [ ] Migración sin pérdida de datos
|
||||
- [ ] Validación automática funcional
|
||||
- [ ] Reportes muestran información correcta
|
||||
- [ ] Performance aceptable (< 2s carga de resultados)
|
||||
- [ ] Documentación actualizada
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Revisar y aprobar este plan
|
||||
2. Comenzar con Task 1: Crear modelo lims.analysis.parameter
|
||||
3. Seguir el orden de las fases para mantener coherencia
|
||||
4. Validar cada fase antes de continuar
|
||||
|
||||
---
|
||||
|
||||
**Nota**: Este plan está sujeto a ajustes según se descubran nuevos requerimientos o complejidades durante la implementación.
|
|
@ -1,294 +0,0 @@
|
|||
# Plan de Desarrollo - Issue #11: Informe Final de Resultados en PDF
|
||||
|
||||
## Resumen del Issue
|
||||
Crear una plantilla de reporte QWeb compleja y profesional para el informe de resultados de laboratorio, con capacidad de resaltar valores fuera de rango, incluir datos del laboratorio y paciente, y guardarse automáticamente como adjunto.
|
||||
|
||||
## Análisis de Requerimientos
|
||||
|
||||
### Componentes del Reporte
|
||||
1. **Encabezado**
|
||||
- Logo del laboratorio
|
||||
- Datos del laboratorio (nombre, dirección, teléfono)
|
||||
- Datos del paciente (nombre, ID, edad, sexo)
|
||||
- Número de orden y fecha
|
||||
|
||||
2. **Sección de Resultados**
|
||||
- Agrupación por tipo de análisis
|
||||
- Tabla con columnas: Parámetro | Resultado | Unidad | Valor de Referencia
|
||||
- Resaltado visual de valores fuera de rango (color/símbolo)
|
||||
- Indicación especial para valores críticos
|
||||
|
||||
3. **Sección de Comentarios**
|
||||
- Observaciones generales de la orden
|
||||
- Notas específicas por resultado si las hay
|
||||
|
||||
4. **Pie del Informe**
|
||||
- Datos del profesional validador (nombre, título, registro)
|
||||
- Fecha y hora de validación
|
||||
- Firma digital o espacio para firma
|
||||
|
||||
### Requisitos Técnicos
|
||||
- Botón "Imprimir Informe de Resultados" solo activo cuando todas las pruebas estén en estado "validated"
|
||||
- PDF generado se guarda automáticamente como adjunto en la orden
|
||||
- Formato profesional y limpio
|
||||
|
||||
## Estructura de Archivos a Crear/Modificar
|
||||
|
||||
### 1. Reporte QWeb
|
||||
```
|
||||
lims_management/
|
||||
├── reports/
|
||||
│ ├── lab_results_report.xml # Plantilla QWeb del reporte
|
||||
│ └── lab_results_report_data.xml # Definición del reporte y paper format
|
||||
```
|
||||
|
||||
### 2. Modelos a Modificar
|
||||
```
|
||||
lims_management/
|
||||
├── models/
|
||||
│ └── sale_order.py # Agregar método para generar reporte
|
||||
```
|
||||
|
||||
### 3. Vistas a Modificar
|
||||
```
|
||||
lims_management/
|
||||
├── views/
|
||||
│ └── sale_order_views.xml # Agregar botón de impresión
|
||||
```
|
||||
|
||||
### 4. Manifest
|
||||
```
|
||||
lims_management/
|
||||
├── __manifest__.py # Agregar archivos de reportes
|
||||
```
|
||||
|
||||
## Implementación Detallada
|
||||
|
||||
### Fase 1: Estructura Base del Reporte
|
||||
|
||||
#### 1.1 Definir Paper Format Personalizado
|
||||
```xml
|
||||
<!-- lab_results_report_data.xml -->
|
||||
<record id="paperformat_lab_results" model="report.paperformat">
|
||||
<field name="name">Formato Resultados de Laboratorio</field>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">40</field>
|
||||
<field name="margin_bottom">25</field>
|
||||
<field name="margin_left">10</field>
|
||||
<field name="margin_right">10</field>
|
||||
<field name="header_spacing">35</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
#### 1.2 Definir Acción del Reporte
|
||||
```xml
|
||||
<record id="action_report_lab_results" model="ir.actions.report">
|
||||
<field name="name">Informe de Resultados</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">lims_management.report_lab_results</field>
|
||||
<field name="report_file">lims_management.report_lab_results</field>
|
||||
<field name="paperformat_id" ref="paperformat_lab_results"/>
|
||||
<field name="attachment">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="attachment_use">True</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
### Fase 2: Plantilla QWeb del Reporte
|
||||
|
||||
#### 2.1 Estructura Principal
|
||||
```xml
|
||||
<!-- lab_results_report.xml -->
|
||||
<template id="report_lab_results">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="lims_management.report_lab_results_document"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 2.2 Documento Individual
|
||||
```xml
|
||||
<template id="report_lab_results_document">
|
||||
<div class="page">
|
||||
<!-- Encabezado -->
|
||||
<div class="header">
|
||||
<!-- Logo y datos del laboratorio -->
|
||||
<!-- Datos del paciente -->
|
||||
</div>
|
||||
|
||||
<!-- Cuerpo con resultados -->
|
||||
<div class="body">
|
||||
<!-- Iterar por pruebas validadas -->
|
||||
<t t-foreach="o.lab_test_ids.filtered(lambda t: t.state == 'validated')" t-as="test">
|
||||
<!-- Tabla de resultados -->
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Pie con validación -->
|
||||
<div class="footer">
|
||||
<!-- Datos del validador -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Fase 3: Lógica del Modelo
|
||||
|
||||
#### 3.1 Método para Verificar Estado
|
||||
```python
|
||||
# En sale_order.py
|
||||
@api.depends('lab_test_ids.state')
|
||||
def _compute_can_print_results(self):
|
||||
for order in self:
|
||||
tests = order.lab_test_ids
|
||||
order.can_print_results = (
|
||||
tests and
|
||||
all(test.state == 'validated' for test in tests)
|
||||
)
|
||||
|
||||
can_print_results = fields.Boolean(
|
||||
compute='_compute_can_print_results',
|
||||
string="Puede Imprimir Resultados"
|
||||
)
|
||||
```
|
||||
|
||||
#### 3.2 Método para Generar y Adjuntar PDF
|
||||
```python
|
||||
def action_print_lab_results(self):
|
||||
"""Genera el informe de resultados y lo adjunta"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar que todas las pruebas estén validadas
|
||||
if not self.can_print_results:
|
||||
raise ValidationError("No se puede imprimir: hay pruebas sin validar")
|
||||
|
||||
# Generar el reporte
|
||||
return self.env.ref('lims_management.action_report_lab_results').report_action(self)
|
||||
```
|
||||
|
||||
### Fase 4: Botón en la Vista
|
||||
|
||||
```xml
|
||||
<!-- En sale_order_views.xml -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_print_lab_results"
|
||||
string="Imprimir Informe de Resultados"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="not can_print_results or not is_lab_request"/>
|
||||
</xpath>
|
||||
```
|
||||
|
||||
### Fase 5: Estilos CSS para el Reporte
|
||||
|
||||
#### 5.1 Estilos para Resaltado
|
||||
```xml
|
||||
<style>
|
||||
.result-out-of-range {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-critical {
|
||||
background-color: #f2dede;
|
||||
color: #a94442;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-normal {
|
||||
color: #5cb85c;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
#### 5.2 Aplicación Condicional
|
||||
```xml
|
||||
<td t-attf-class="#{result.is_critical and 'result-critical' or result.is_out_of_range and 'result-out-of-range' or 'result-normal'}">
|
||||
<t t-esc="result.value_display"/>
|
||||
</td>
|
||||
```
|
||||
|
||||
### Fase 6: Datos Demo para Pruebas
|
||||
|
||||
Crear script Python que:
|
||||
1. Genere órdenes con múltiples análisis
|
||||
2. Ingrese resultados variados (normales, fuera de rango, críticos)
|
||||
3. Valide las pruebas
|
||||
4. Permita probar la generación del PDF
|
||||
|
||||
## Consideraciones Especiales
|
||||
|
||||
### 1. Manejo de Caracteres Especiales
|
||||
- Usar entidades HTML para tildes y ñ en el reporte
|
||||
- Ejemplo: `Í` para Í, `ñ` para ñ
|
||||
|
||||
### 2. Códigos de Barras
|
||||
- Usar widget nativo de Odoo 18: `t-options="{'widget': 'barcode', 'type': 'Code128'}"`
|
||||
- NO usar rutas deprecated como `/report/barcode/`
|
||||
|
||||
### 3. Agrupación de Resultados
|
||||
- Agrupar por tipo de análisis para mejor legibilidad
|
||||
- Mantener orden por secuencia definida en parámetros
|
||||
|
||||
### 4. Seguridad
|
||||
- Solo usuarios con permisos de lectura en órdenes pueden generar el reporte
|
||||
- El PDF se adjunta con permisos heredados de la orden
|
||||
|
||||
## Secuencia de Implementación
|
||||
|
||||
1. **Crear estructura base de reportes**
|
||||
- Crear carpeta reports/
|
||||
- Definir paper format y acción
|
||||
|
||||
2. **Implementar plantilla QWeb básica**
|
||||
- Estructura HTML con secciones
|
||||
- Iterar sobre pruebas y resultados
|
||||
|
||||
3. **Agregar lógica en modelo**
|
||||
- Campo computado can_print_results
|
||||
- Método action_print_lab_results
|
||||
|
||||
4. **Integrar botón en vista**
|
||||
- Agregar botón con visibilidad condicional
|
||||
|
||||
5. **Implementar estilos y resaltado**
|
||||
- CSS para valores fuera de rango
|
||||
- Clases condicionales en plantilla
|
||||
|
||||
6. **Configurar adjunto automático**
|
||||
- Configurar attachment en ir.actions.report
|
||||
- Verificar guardado en ir.attachment
|
||||
|
||||
7. **Crear datos demo y probar**
|
||||
- Script para generar casos de prueba
|
||||
- Validar formato y contenido del PDF
|
||||
|
||||
## Validación y Pruebas
|
||||
|
||||
### Casos de Prueba
|
||||
1. **Orden sin pruebas validadas**: Botón invisible
|
||||
2. **Orden parcialmente validada**: Botón invisible
|
||||
3. **Orden completamente validada**: Botón visible, genera PDF
|
||||
4. **Valores normales**: Sin resaltado
|
||||
5. **Valores fuera de rango**: Resaltado en color
|
||||
6. **Valores críticos**: Resaltado especial
|
||||
7. **PDF adjunto**: Verificar que se guarda en la orden
|
||||
|
||||
### Criterios de Aceptación
|
||||
- [ ] Reporte muestra todos los datos requeridos
|
||||
- [ ] Valores fuera de rango se resaltan correctamente
|
||||
- [ ] Botón solo visible cuando todas las pruebas están validadas
|
||||
- [ ] PDF se genera con formato profesional
|
||||
- [ ] PDF se adjunta automáticamente a la orden
|
||||
- [ ] Datos del validador aparecen correctamente
|
||||
- [ ] Comentarios y observaciones se muestran si existen
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar Odoo 18 syntax para invisibility: `invisible="not can_print_results"`
|
||||
- Verificar compatibilidad con wkhtmltopdf para renderizado PDF
|
||||
- Considerar tamaño del archivo para órdenes con muchos análisis
|
||||
- El attachment_use=True garantiza que no se regenere si ya existe
|
197
init_odoo.py
|
@ -35,7 +35,6 @@ odoo_command = [
|
|||
"-c", ODOO_CONF,
|
||||
"-d", DB_NAME,
|
||||
"-i", MODULES_TO_INSTALL,
|
||||
"--load-language", "es_ES",
|
||||
"--stop-after-init"
|
||||
]
|
||||
|
||||
|
@ -59,203 +58,11 @@ try:
|
|||
sys.exit(result.returncode)
|
||||
|
||||
print("Inicialización de Odoo completada exitosamente.")
|
||||
|
||||
# --- Lógica para crear datos de demostración personalizados ---
|
||||
print("Creando solicitudes de laboratorio de demostración...")
|
||||
sys.stdout.flush()
|
||||
|
||||
with open("/app/create_lab_requests.py", "r") as f:
|
||||
script_content = f.read()
|
||||
|
||||
# Reutilizamos el entorno de Odoo para ejecutar un script
|
||||
create_requests_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
create_requests_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Create Lab Requests stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Create Lab Requests stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Fallo al crear las solicitudes de laboratorio con código de salida {result.returncode}")
|
||||
sys.exit(result.returncode)
|
||||
|
||||
print("Solicitudes de laboratorio de demostración creadas exitosamente.")
|
||||
|
||||
# --- Crear datos de demostración de pruebas ---
|
||||
print("\nCreando datos de demostración de pruebas de laboratorio...")
|
||||
sys.stdout.flush()
|
||||
|
||||
# Usar el nuevo script consolidado de datos demo
|
||||
demo_script_path = "/app/test/create_demo_data.py"
|
||||
if os.path.exists(demo_script_path):
|
||||
with open(demo_script_path, "r") as f:
|
||||
demo_script_content = f.read()
|
||||
|
||||
create_demo_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{demo_script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
create_demo_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Create Demo Data stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Create Demo Data stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Datos de demostración creados exitosamente.")
|
||||
else:
|
||||
print(f"Advertencia: Fallo al crear datos de demostración (código {result.returncode})")
|
||||
else:
|
||||
# Fallback al script anterior si existe
|
||||
old_script_path = "/app/test/create_test_demo_data.py"
|
||||
if os.path.exists(old_script_path):
|
||||
print("Usando script de demostración anterior...")
|
||||
with open(old_script_path, "r") as f:
|
||||
test_script_content = f.read()
|
||||
|
||||
create_tests_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{test_script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
create_tests_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Datos de demostración de pruebas creados exitosamente.")
|
||||
else:
|
||||
print(f"Advertencia: Fallo al crear datos de demostración de pruebas (código {result.returncode})")
|
||||
|
||||
# --- Actualizar logo de la empresa ---
|
||||
print("\nActualizando logo de la empresa...")
|
||||
sys.stdout.flush()
|
||||
|
||||
if os.path.exists("/app/scripts/update_company_logo_odoo18.py"):
|
||||
with open("/app/scripts/update_company_logo_odoo18.py", "r") as f:
|
||||
logo_script_content = f.read()
|
||||
|
||||
update_logo_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{logo_script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
update_logo_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Update Company Logo stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Update Company Logo stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Logo de empresa actualizado exitosamente.")
|
||||
else:
|
||||
print(f"Advertencia: Fallo al actualizar logo de empresa (código {result.returncode})")
|
||||
|
||||
# --- Asignar admin al grupo de Administrador de Laboratorio ---
|
||||
print("\nAsignando usuario admin al grupo de Administrador de Laboratorio...")
|
||||
sys.stdout.flush()
|
||||
|
||||
if os.path.exists("/app/scripts/assign_admin_to_lab_group.py"):
|
||||
with open("/app/scripts/assign_admin_to_lab_group.py", "r") as f:
|
||||
admin_group_script = f.read()
|
||||
|
||||
assign_admin_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{admin_group_script}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
assign_admin_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Assign Admin to Lab Group stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Assign Admin to Lab Group stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Usuario admin asignado exitosamente al grupo de Administrador de Laboratorio.")
|
||||
else:
|
||||
print(f"Advertencia: Fallo al asignar admin al grupo (código {result.returncode})")
|
||||
|
||||
# --- Validación final del logo ---
|
||||
print("\nValidando estado final del logo y nombre...")
|
||||
sys.stdout.flush()
|
||||
|
||||
if os.path.exists("/app/test/verify_company_logo.py"):
|
||||
with open("/app/test/verify_company_logo.py", "r") as f:
|
||||
verify_script_content = f.read()
|
||||
|
||||
verify_command = f"""
|
||||
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
|
||||
{verify_script_content}
|
||||
EOF
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
verify_command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
print("--- Verify Company Logo stdout ---")
|
||||
print(result.stdout)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ocurrió un error inesperado al crear las solicitudes de laboratorio: {e}")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: El comando 'odoo' no se encontró. Asegúrate de que la imagen del contenedor es correcta y odoo está en el PATH.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Ocurrió un error inesperado al ejecutar Odoo: {e}")
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
# Determinar automáticamente valores críticos/anormales para parámetros de selección múltiple
|
||||
|
||||
## Descripción
|
||||
|
||||
Actualmente, el sistema puede determinar automáticamente si un valor numérico es crítico basándose en rangos mínimos y máximos. Sin embargo, para parámetros de tipo selección (como Positivo/Negativo, Reactivo/No Reactivo), no existe una forma dinámica de determinar cuándo un valor es crítico o anormal.
|
||||
|
||||
## Problema actual
|
||||
|
||||
Los parámetros de selección múltiple no tienen forma de indicar qué valores son:
|
||||
- Normales
|
||||
- Anormales
|
||||
- Críticos
|
||||
|
||||
Ejemplos de parámetros afectados:
|
||||
- Prueba de embarazo: Positivo/Negativo
|
||||
- HIV: Reactivo/No Reactivo/Indeterminado
|
||||
- Hepatitis: Reactivo/No Reactivo
|
||||
- Otros marcadores infecciosos
|
||||
|
||||
## Solución propuesta
|
||||
|
||||
### Opción 1: Agregar campos al modelo `lims.analysis.parameter`
|
||||
|
||||
Agregar campos que permitan definir qué valores de selección son críticos:
|
||||
```python
|
||||
critical_values = fields.Text(
|
||||
string="Valores Críticos",
|
||||
help="Lista de valores separados por coma que se consideran críticos"
|
||||
)
|
||||
abnormal_values = fields.Text(
|
||||
string="Valores Anormales",
|
||||
help="Lista de valores separados por coma que se consideran anormales"
|
||||
)
|
||||
```
|
||||
|
||||
### Opción 2: Crear modelo relacionado `lims.parameter.selection.value`
|
||||
|
||||
Crear un modelo que defina cada opción de selección con sus propiedades:
|
||||
```python
|
||||
class LimsParameterSelectionValue(models.Model):
|
||||
_name = 'lims.parameter.selection.value'
|
||||
|
||||
parameter_id = fields.Many2one('lims.analysis.parameter')
|
||||
value = fields.Char(string="Valor")
|
||||
is_normal = fields.Boolean(string="Es Normal", default=True)
|
||||
is_critical = fields.Boolean(string="Es Crítico", default=False)
|
||||
sequence = fields.Integer(string="Secuencia")
|
||||
notes_template = fields.Text(string="Plantilla de Notas")
|
||||
```
|
||||
|
||||
### Opción 3: Usar configuración JSON
|
||||
|
||||
Almacenar la configuración en un campo JSON:
|
||||
```python
|
||||
selection_config = fields.Json(
|
||||
string="Configuración de Valores",
|
||||
help="Configuración de valores normales, anormales y críticos"
|
||||
)
|
||||
```
|
||||
|
||||
## Beneficios esperados
|
||||
|
||||
1. **Automatización completa**: El sistema podrá determinar automáticamente si cualquier tipo de resultado es crítico
|
||||
2. **Flexibilidad**: Cada laboratorio podrá configurar qué valores considera críticos según sus protocolos
|
||||
3. **Consistencia**: Aplicación uniforme de criterios en todos los resultados
|
||||
4. **Alertas mejoradas**: Mejor identificación de resultados que requieren atención inmediata
|
||||
|
||||
## Casos de uso
|
||||
|
||||
1. **Prueba de embarazo**:
|
||||
- Normal: Negativo (para pacientes no embarazadas)
|
||||
- Anormal: Positivo (puede requerir seguimiento)
|
||||
- Crítico: Indeterminado (requiere repetición)
|
||||
|
||||
2. **HIV**:
|
||||
- Normal: No Reactivo
|
||||
- Crítico: Reactivo, Indeterminado
|
||||
|
||||
3. **Marcadores tumorales**:
|
||||
- Normal: Negativo, No Detectado
|
||||
- Anormal: Débilmente Positivo
|
||||
- Crítico: Positivo, Fuertemente Positivo
|
||||
|
||||
## Consideraciones técnicas
|
||||
|
||||
- Mantener compatibilidad con el sistema actual
|
||||
- Permitir migración de datos existentes
|
||||
- Interfaz de usuario intuitiva para configuración
|
||||
- Integración con el autocompletado de notas críticas existente
|
||||
|
||||
## Tareas propuestas
|
||||
|
||||
1. Análisis de la mejor opción de implementación
|
||||
2. Diseño del modelo de datos
|
||||
3. Implementación de campos/modelos necesarios
|
||||
4. Actualización de la lógica de `is_critical` en `lims.result`
|
||||
5. Creación de interfaz de configuración
|
||||
6. Migración de parámetros existentes
|
||||
7. Pruebas exhaustivas
|
||||
8. Documentación
|
||||
|
||||
## Prioridad
|
||||
|
||||
Media-Alta: Esta mejora completaría la funcionalidad de detección automática de valores críticos para todos los tipos de parámetros.
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
|
@ -16,51 +16,19 @@
|
|||
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
|
||||
'category': 'Industries',
|
||||
'version': '18.0.1.0.0',
|
||||
'depends': ['base', 'product', 'sale', 'stock', 'base_setup'],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'lims_management/static/src/css/lims_test.css',
|
||||
],
|
||||
},
|
||||
'depends': ['base', 'product'],
|
||||
'data': [
|
||||
'security/lims_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_sequence.xml',
|
||||
'data/product_category.xml',
|
||||
'data/sample_types.xml',
|
||||
'data/lims_sequence.xml',
|
||||
'data/rejection_reason_data.xml',
|
||||
'views/partner_views.xml',
|
||||
'views/analysis_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/rejection_reason_views.xml',
|
||||
'wizards/sample_rejection_wizard_views.xml',
|
||||
'views/stock_lot_views.xml',
|
||||
'views/lims_test_views.xml',
|
||||
'views/lims_result_views.xml',
|
||||
'views/lims_result_bulk_entry_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/product_template_parameter_views.xml',
|
||||
'views/parameter_range_views.xml',
|
||||
'views/analysis_parameter_views.xml',
|
||||
'views/product_template_parameter_config_views.xml',
|
||||
'views/parameter_dashboard_views.xml',
|
||||
'views/dashboard_views.xml',
|
||||
'views/menus.xml',
|
||||
'views/lims_config_views.xml',
|
||||
'report/sample_label_report.xml',
|
||||
'reports/lab_results_report_data.xml',
|
||||
'reports/lab_results_report.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo_users.xml',
|
||||
'demo/z_lims_demo.xml',
|
||||
'demo/z_analysis_demo.xml',
|
||||
'demo/z_sample_demo.xml',
|
||||
'demo/parameter_demo.xml',
|
||||
'demo/parameter_range_demo.xml',
|
||||
'demo/analysis_parameter_config_demo.xml',
|
||||
'demo/z_automatic_generation_demo.xml',
|
||||
'demo/lims_demo.xml',
|
||||
'demo/analysis_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Secuencia para lims.test -->
|
||||
<record id="seq_lims_test" model="ir.sequence">
|
||||
<field name="name">Secuencia de Pruebas de Laboratorio</field>
|
||||
<field name="code">lims.test</field>
|
||||
<field name="prefix">LAB-%(year)s-</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Secuencia para muestras de laboratorio -->
|
||||
<record id="seq_stock_lot_serial" model="ir.sequence">
|
||||
<field name="name">Secuencia de Muestras de Laboratorio</field>
|
||||
<field name="code">stock.lot.serial</field>
|
||||
<field name="prefix">M-%(year)s%(month)s%(day)s-</field>
|
||||
<field name="padding">6</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,95 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Rejection Reasons -->
|
||||
<record id="rejection_reason_insufficient" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Insuficiente</field>
|
||||
<field name="code">INSUF</field>
|
||||
<field name="description">El volumen de muestra recibido es insuficiente para realizar los análisis solicitados</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_hemolyzed" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Hemolizada</field>
|
||||
<field name="code">HEMO</field>
|
||||
<field name="description">La muestra presenta hemólisis que interfiere con los análisis</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_coagulated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Coagulada</field>
|
||||
<field name="code">COAG</field>
|
||||
<field name="description">La muestra presenta coágulos que impiden su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_lipemic" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Lipémica</field>
|
||||
<field name="code">LIP</field>
|
||||
<field name="description">La muestra presenta lipemia excesiva que interfiere con los análisis</field>
|
||||
<field name="severity">medium</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_container" model="lims.rejection.reason">
|
||||
<field name="name">Recipiente Inadecuado</field>
|
||||
<field name="code">RECIP</field>
|
||||
<field name="description">El tipo de recipiente utilizado no es apropiado para el análisis solicitado</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_wrong_id" model="lims.rejection.reason">
|
||||
<field name="name">Identificación Incorrecta</field>
|
||||
<field name="code">ID</field>
|
||||
<field name="description">La identificación de la muestra no coincide con la solicitud o es ilegible</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">60</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_no_label" model="lims.rejection.reason">
|
||||
<field name="name">Muestra sin Rotular</field>
|
||||
<field name="code">NOLAB</field>
|
||||
<field name="description">La muestra no tiene etiqueta de identificación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">70</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_transport" model="lims.rejection.reason">
|
||||
<field name="name">Condiciones de Transporte Inadecuadas</field>
|
||||
<field name="code">TRANS</field>
|
||||
<field name="description">La muestra no fue transportada en las condiciones requeridas (temperatura, tiempo, etc.)</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">80</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_contaminated" model="lims.rejection.reason">
|
||||
<field name="name">Muestra Contaminada</field>
|
||||
<field name="code">CONT</field>
|
||||
<field name="description">La muestra presenta signos evidentes de contaminación</field>
|
||||
<field name="severity">critical</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">90</field>
|
||||
</record>
|
||||
|
||||
<record id="rejection_reason_expired" model="lims.rejection.reason">
|
||||
<field name="name">Tiempo de Entrega Excedido</field>
|
||||
<field name="code">TIME</field>
|
||||
<field name="description">La muestra fue recibida fuera del tiempo límite establecido para su procesamiento</field>
|
||||
<field name="severity">high</field>
|
||||
<field name="requires_new_sample" eval="True"/>
|
||||
<field name="sequence">100</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<function model="sale.order" name="search" eval="[('name', 'in', ['S00001', 'S00002', 'S00003', 'S00004', 'S00005', 'S00006', 'S00007', 'S00008', 'S00009', 'S00010', 'S00011', 'S00012', 'S00013', 'S00014', 'S00015', 'S00016', 'S00017', 'S00018', 'S00019', 'S00020', 'S00021', 'S00022'])]"/>
|
||||
<function model="sale.order" name="action_cancel"/>
|
||||
<function model="sale.order" name="unlink"/>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,140 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<!-- Category for sample containers -->
|
||||
<record id="product_category_sample_containers" model="product.category">
|
||||
<field name="name">Contenedores de Muestra</field>
|
||||
<field name="parent_id" ref="product.product_category_all"/>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Serum Tube (Red Cap) -->
|
||||
<record id="sample_type_serum_tube" model="product.template">
|
||||
<field name="name">Tubo de Suero (Tapa Roja)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.50</field>
|
||||
<field name="standard_price">0.30</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo con gel separador para obtención de suero. Usado para química clínica, inmunología y serología.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: EDTA Tube (Purple Cap) -->
|
||||
<record id="sample_type_edta_tube" model="product.template">
|
||||
<field name="name">Tubo EDTA (Tapa Morada)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.55</field>
|
||||
<field name="standard_price">0.35</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo con anticoagulante EDTA. Usado para hematología y algunos estudios de química.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Citrate Tube (Blue Cap) -->
|
||||
<record id="sample_type_citrate_tube" model="product.template">
|
||||
<field name="name">Tubo Citrato (Tapa Azul)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.60</field>
|
||||
<field name="standard_price">0.40</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo con citrato de sodio. Usado para pruebas de coagulación.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Heparin Tube (Green Cap) -->
|
||||
<record id="sample_type_heparin_tube" model="product.template">
|
||||
<field name="name">Tubo Heparina (Tapa Verde)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.65</field>
|
||||
<field name="standard_price">0.45</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo con heparina de litio o sodio. Usado para química clínica en plasma.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Glucose Tube (Gray Cap) -->
|
||||
<record id="sample_type_glucose_tube" model="product.template">
|
||||
<field name="name">Tubo Glucosa (Tapa Gris)</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.70</field>
|
||||
<field name="standard_price">0.50</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo con fluoruro de sodio/oxalato de potasio. Usado para determinación de glucosa.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Urine Container -->
|
||||
<record id="sample_type_urine_container" model="product.template">
|
||||
<field name="name">Contenedor de Orina</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.30</field>
|
||||
<field name="standard_price">0.20</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Contenedor estéril para recolección de muestras de orina.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Stool Container -->
|
||||
<record id="sample_type_stool_container" model="product.template">
|
||||
<field name="name">Contenedor de Heces</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.35</field>
|
||||
<field name="standard_price">0.25</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Contenedor para recolección de muestras de heces fecales.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Swab -->
|
||||
<record id="sample_type_swab" model="product.template">
|
||||
<field name="name">Hisopo</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.25</field>
|
||||
<field name="standard_price">0.15</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Hisopo estéril para toma de muestras de garganta, nasal, etc.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: Blood Culture Bottle -->
|
||||
<record id="sample_type_blood_culture" model="product.template">
|
||||
<field name="name">Frasco de Hemocultivo</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">3.50</field>
|
||||
<field name="standard_price">2.50</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Frasco para cultivo de sangre con medio de cultivo.</field>
|
||||
</record>
|
||||
|
||||
<!-- Sample Type: CSF Tube -->
|
||||
<record id="sample_type_csf_tube" model="product.template">
|
||||
<field name="name">Tubo para LCR</field>
|
||||
<field name="is_sample_type">True</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="categ_id" ref="product_category_sample_containers"/>
|
||||
<field name="list_price">0.80</field>
|
||||
<field name="standard_price">0.60</field>
|
||||
<field name="sale_ok">False</field>
|
||||
<field name="purchase_ok">True</field>
|
||||
<field name="description">Tubo estéril para líquido cefalorraquídeo.</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
71
lims_management/demo/analysis_demo.xml
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Datos de Demostración para Análisis Clínicos -->
|
||||
|
||||
<!-- Análisis: Hemograma Completo -->
|
||||
<record id="analysis_hemograma" model="product.template">
|
||||
<field name="name">Hemograma Completo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">hematology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="technical_specifications">
|
||||
El hemograma completo es un análisis de sangre que mide los niveles de los principales componentes sanguíneos: glóbulos rojos, glóbulos blancos y plaquetas.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos de Referencia para Hemograma -->
|
||||
<record id="range_hemograma_globulos_rojos_m" model="lims.analysis.range">
|
||||
<field name="analysis_id" ref="analysis_hemograma"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="min_value">4.5</field>
|
||||
<field name="max_value">5.9</field>
|
||||
<field name="unit_of_measure">millones/µL</field>
|
||||
</record>
|
||||
<record id="range_hemograma_globulos_rojos_f" model="lims.analysis.range">
|
||||
<field name="analysis_id" ref="analysis_hemograma"/>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="min_value">4.0</field>
|
||||
<field name="max_value">5.2</field>
|
||||
<field name="unit_of_measure">millones/µL</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Perfil Lipídico -->
|
||||
<record id="analysis_perfil_lipidico" model="product.template">
|
||||
<field name="name">Perfil Lipídico</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">chemistry</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="technical_specifications">
|
||||
Mide los niveles de colesterol y otros lípidos en la sangre. Incluye Colesterol Total, LDL, HDL y Triglicéridos.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Colesterol Total -->
|
||||
<record id="range_colesterol_total" model="lims.analysis.range">
|
||||
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="min_value">0</field>
|
||||
<field name="max_value">200</field>
|
||||
<field name="unit_of_measure">mg/dL</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Colesterol LDL -->
|
||||
<record id="range_colesterol_ldl" model="lims.analysis.range">
|
||||
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="min_value">0</field>
|
||||
<field name="max_value">100</field>
|
||||
<field name="unit_of_measure">mg/dL</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,363 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Configuración de parámetros para Hemograma Completo -->
|
||||
<record id="config_hemograma_hgb" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_hemoglobin"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_hct" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_hematocrit"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_rbc" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_rbc"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_wbc" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_wbc"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_plt" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_platelets"/>
|
||||
<field name="sequence">50</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_neut" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_neutrophils"/>
|
||||
<field name="sequence">60</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemograma_lymph" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemograma"/>
|
||||
<field name="parameter_id" ref="param_lymphocytes"/>
|
||||
<field name="sequence">70</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Perfil Lipídico -->
|
||||
<record id="config_lipidos_chol" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="parameter_id" ref="param_cholesterol_total"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_lipidos_hdl" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="parameter_id" ref="param_cholesterol_hdl"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_lipidos_ldl" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="parameter_id" ref="param_cholesterol_ldl"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_lipidos_trig" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_perfil_lipidico"/>
|
||||
<field name="parameter_id" ref="param_triglycerides"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Glucosa -->
|
||||
<record id="config_glucosa" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_glucosa"/>
|
||||
<field name="parameter_id" ref="param_glucose"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Urocultivo -->
|
||||
<record id="config_urocultivo_result" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
|
||||
<field name="parameter_id" ref="param_culture_result"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urocultivo_organism" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
|
||||
<field name="parameter_id" ref="param_isolated_organism"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">False</field>
|
||||
<field name="instructions">Completar solo si el cultivo es positivo</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urocultivo_count" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urocultivo"/>
|
||||
<field name="parameter_id" ref="param_colony_count"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">False</field>
|
||||
<field name="instructions">Completar solo si el cultivo es positivo. Formato: >100,000 UFC/mL</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Tiempo de Protrombina -->
|
||||
<record id="config_tp_time" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_tp"/>
|
||||
<field name="parameter_id" ref="param_pt"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_tp_inr" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_tp"/>
|
||||
<field name="parameter_id" ref="param_inr"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Hemocultivo -->
|
||||
<record id="config_hemocultivo_result" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemocultivo"/>
|
||||
<field name="parameter_id" ref="param_culture_result"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_hemocultivo_organism" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_hemocultivo"/>
|
||||
<field name="parameter_id" ref="param_isolated_organism"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">False</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuración de parámetros para Coprocultivo -->
|
||||
<record id="config_coprocultivo_result" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_coprocultivo"/>
|
||||
<field name="parameter_id" ref="param_culture_result"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_coprocultivo_organism" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_coprocultivo"/>
|
||||
<field name="parameter_id" ref="param_isolated_organism"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">False</field>
|
||||
</record>
|
||||
|
||||
<!-- Crear análisis adicionales comunes -->
|
||||
|
||||
<!-- Análisis: Química Sanguínea -->
|
||||
<record id="analysis_quimica_sanguinea" model="product.template">
|
||||
<field name="name">Química Sanguínea Básica</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">chemistry</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
|
||||
<field name="sample_volume_ml">3.0</field>
|
||||
<field name="technical_specifications">
|
||||
Panel básico de química sanguínea que incluye glucosa, creatinina, urea, ALT y AST.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Configurar parámetros para Química Sanguínea -->
|
||||
<record id="config_quimica_glucose" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
|
||||
<field name="parameter_id" ref="param_glucose"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_quimica_crea" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
|
||||
<field name="parameter_id" ref="param_creatinine"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_quimica_urea" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
|
||||
<field name="parameter_id" ref="param_urea"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_quimica_alt" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
|
||||
<field name="parameter_id" ref="param_alt"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_quimica_ast" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_quimica_sanguinea"/>
|
||||
<field name="parameter_id" ref="param_ast"/>
|
||||
<field name="sequence">50</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Urianálisis Completo -->
|
||||
<record id="analysis_urianalisis" model="product.template">
|
||||
<field name="name">Urianálisis Completo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">other</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_urine_container"/>
|
||||
<field name="sample_volume_ml">10.0</field>
|
||||
<field name="technical_specifications">
|
||||
Examen completo de orina que incluye examen físico, químico y microscópico del sedimento.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Configurar parámetros para Urianálisis -->
|
||||
<record id="config_urine_color" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_color"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_appearance" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_appearance"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_ph" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_ph"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_density" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_density"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_protein" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_protein"/>
|
||||
<field name="sequence">50</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_glucose" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_glucose"/>
|
||||
<field name="sequence">60</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_blood" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_blood"/>
|
||||
<field name="sequence">70</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_leukocytes" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_leukocytes"/>
|
||||
<field name="sequence">80</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_urine_bacteria" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_urianalisis"/>
|
||||
<field name="parameter_id" ref="param_urine_bacteria"/>
|
||||
<field name="sequence">90</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Panel de Serología -->
|
||||
<record id="analysis_serologia" model="product.template">
|
||||
<field name="name">Panel de Serología Básica</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">immunology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
|
||||
<field name="sample_volume_ml">5.0</field>
|
||||
<field name="technical_specifications">
|
||||
Panel serológico que incluye HIV, Hepatitis B, Hepatitis C y VDRL.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Configurar parámetros para Serología -->
|
||||
<record id="config_sero_hiv" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_serologia"/>
|
||||
<field name="parameter_id" ref="param_hiv"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_sero_hbsag" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_serologia"/>
|
||||
<field name="parameter_id" ref="param_hbsag"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_sero_hcv" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_serologia"/>
|
||||
<field name="parameter_id" ref="param_hcv"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<record id="config_sero_vdrl" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_serologia"/>
|
||||
<field name="parameter_id" ref="param_vdrl"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Prueba de Embarazo -->
|
||||
<record id="analysis_prueba_embarazo" model="product.template">
|
||||
<field name="name">Prueba de Embarazo en Sangre</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">immunology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
|
||||
<field name="sample_volume_ml">1.0</field>
|
||||
<field name="technical_specifications">
|
||||
Detección cualitativa de Beta-HCG en sangre.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="config_pregnancy_test" model="product.template.parameter">
|
||||
<field name="product_tmpl_id" ref="analysis_prueba_embarazo"/>
|
||||
<field name="parameter_id" ref="param_pregnancy"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="required">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,61 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Usuario Recepcionista -->
|
||||
<record id="demo_user_receptionist" model="res.users">
|
||||
<field name="name">Recepcionista Demo</field>
|
||||
<field name="login">recepcionista</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">recepcionista@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_receptionist'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Usuario Técnico -->
|
||||
<record id="demo_user_technician" model="res.users">
|
||||
<field name="name">Técnico Demo</field>
|
||||
<field name="login">tecnico</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">tecnico@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_technician'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Usuario Administrador de Laboratorio -->
|
||||
<record id="demo_user_lab_admin" model="res.users">
|
||||
<field name="name">Administrador Lab Demo</field>
|
||||
<field name="login">administrador</field>
|
||||
<field name="password">demo</field>
|
||||
<field name="email">administrador@example.com</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('lims_management.group_lims_admin'), ref('base.group_user')])]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base.main_company'))]"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Partner (empleado) para cada usuario -->
|
||||
<record id="demo_user_receptionist_partner" model="res.partner">
|
||||
<field name="name">Recepcionista Demo</field>
|
||||
<field name="email">recepcionista@example.com</field>
|
||||
<field name="user_id" ref="demo_user_receptionist"/>
|
||||
<field name="is_company" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_user_technician_partner" model="res.partner">
|
||||
<field name="name">Técnico Demo</field>
|
||||
<field name="email">tecnico@example.com</field>
|
||||
<field name="user_id" ref="demo_user_technician"/>
|
||||
<field name="is_company" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_user_lab_admin_partner" model="res.partner">
|
||||
<field name="name">Administrador Lab Demo</field>
|
||||
<field name="email">administrador@example.com</field>
|
||||
<field name="user_id" ref="demo_user_lab_admin"/>
|
||||
<field name="is_company" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
63
lims_management/demo/lims_demo.xml
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Datos de Demostración para Pacientes -->
|
||||
<record id="demo_patient_1" model="res.partner">
|
||||
<field name="name">Ana Torres</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A87B01</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1985-05-15</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+1-202-555-0174</field>
|
||||
<field name="email">ana.torres@example.com</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_2" model="res.partner">
|
||||
<field name="name">Carlos Ruiz</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-C45D02</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1992-11-20</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+1-202-555-0192</field>
|
||||
<field name="email">carlos.ruiz@example.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Datos de Demostración para Médicos -->
|
||||
<record id="demo_doctor_1" model="res.partner">
|
||||
<field name="name">Dr. Luis Herrera</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-98765</field>
|
||||
<field name="phone">+1-202-555-0145</field>
|
||||
<field name="email">luis.herrera@hospital.com</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_doctor_2" model="res.partner">
|
||||
<field name="name">Dra. Sofia Vargas</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-54321</field>
|
||||
<field name="phone">+1-202-555-0133</field>
|
||||
<field name="email">sofia.vargas@clinic.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Datos de Demostración para Tutor y Paciente Menor de Edad -->
|
||||
<record id="demo_tutor_1" model="res.partner">
|
||||
<field name="name">Laura Mendoza</field>
|
||||
<field name="phone">+1-202-555-0188</field>
|
||||
<field name="email">laura.mendoza@example.com</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_minor_1" model="res.partner">
|
||||
<field name="name">Pedro Infante Jr.</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M12E03</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="parent_id" ref="demo_tutor_1"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,339 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Parámetros de Hematología -->
|
||||
|
||||
<!-- Hemoglobina -->
|
||||
<record id="param_hemoglobin" model="lims.analysis.parameter">
|
||||
<field name="code">HGB</field>
|
||||
<field name="name">Hemoglobina</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">g/dL</field>
|
||||
<field name="description">Concentración de hemoglobina en sangre</field>
|
||||
</record>
|
||||
|
||||
<!-- Hematocrito -->
|
||||
<record id="param_hematocrit" model="lims.analysis.parameter">
|
||||
<field name="code">HCT</field>
|
||||
<field name="name">Hematocrito</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">%</field>
|
||||
<field name="description">Porcentaje del volumen de glóbulos rojos</field>
|
||||
</record>
|
||||
|
||||
<!-- Glóbulos Rojos -->
|
||||
<record id="param_rbc" model="lims.analysis.parameter">
|
||||
<field name="code">RBC</field>
|
||||
<field name="name">Glóbulos Rojos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">millones/µL</field>
|
||||
<field name="description">Recuento de eritrocitos</field>
|
||||
</record>
|
||||
|
||||
<!-- Glóbulos Blancos -->
|
||||
<record id="param_wbc" model="lims.analysis.parameter">
|
||||
<field name="code">WBC</field>
|
||||
<field name="name">Glóbulos Blancos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mil/µL</field>
|
||||
<field name="description">Recuento de leucocitos</field>
|
||||
</record>
|
||||
|
||||
<!-- Plaquetas -->
|
||||
<record id="param_platelets" model="lims.analysis.parameter">
|
||||
<field name="code">PLT</field>
|
||||
<field name="name">Plaquetas</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mil/µL</field>
|
||||
<field name="description">Recuento de plaquetas</field>
|
||||
</record>
|
||||
|
||||
<!-- Neutrófilos -->
|
||||
<record id="param_neutrophils" model="lims.analysis.parameter">
|
||||
<field name="code">NEUT</field>
|
||||
<field name="name">Neutrófilos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">%</field>
|
||||
<field name="description">Porcentaje de neutrófilos</field>
|
||||
</record>
|
||||
|
||||
<!-- Linfocitos -->
|
||||
<record id="param_lymphocytes" model="lims.analysis.parameter">
|
||||
<field name="code">LYMPH</field>
|
||||
<field name="name">Linfocitos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">%</field>
|
||||
<field name="description">Porcentaje de linfocitos</field>
|
||||
</record>
|
||||
|
||||
<!-- Parámetros de Química Clínica -->
|
||||
|
||||
<!-- Glucosa -->
|
||||
<record id="param_glucose" model="lims.analysis.parameter">
|
||||
<field name="code">GLU</field>
|
||||
<field name="name">Glucosa</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Nivel de glucosa en sangre</field>
|
||||
</record>
|
||||
|
||||
<!-- Creatinina -->
|
||||
<record id="param_creatinine" model="lims.analysis.parameter">
|
||||
<field name="code">CREA</field>
|
||||
<field name="name">Creatinina</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Nivel de creatinina sérica</field>
|
||||
</record>
|
||||
|
||||
<!-- Urea -->
|
||||
<record id="param_urea" model="lims.analysis.parameter">
|
||||
<field name="code">UREA</field>
|
||||
<field name="name">Urea</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Nivel de urea en sangre</field>
|
||||
</record>
|
||||
|
||||
<!-- Colesterol Total -->
|
||||
<record id="param_cholesterol_total" model="lims.analysis.parameter">
|
||||
<field name="code">CHOL</field>
|
||||
<field name="name">Colesterol Total</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Nivel de colesterol total</field>
|
||||
</record>
|
||||
|
||||
<!-- Colesterol HDL -->
|
||||
<record id="param_cholesterol_hdl" model="lims.analysis.parameter">
|
||||
<field name="code">HDL</field>
|
||||
<field name="name">Colesterol HDL</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Colesterol de alta densidad</field>
|
||||
</record>
|
||||
|
||||
<!-- Colesterol LDL -->
|
||||
<record id="param_cholesterol_ldl" model="lims.analysis.parameter">
|
||||
<field name="code">LDL</field>
|
||||
<field name="name">Colesterol LDL</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Colesterol de baja densidad</field>
|
||||
</record>
|
||||
|
||||
<!-- Triglicéridos -->
|
||||
<record id="param_triglycerides" model="lims.analysis.parameter">
|
||||
<field name="code">TRIG</field>
|
||||
<field name="name">Triglicéridos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">mg/dL</field>
|
||||
<field name="description">Nivel de triglicéridos</field>
|
||||
</record>
|
||||
|
||||
<!-- ALT -->
|
||||
<record id="param_alt" model="lims.analysis.parameter">
|
||||
<field name="code">ALT</field>
|
||||
<field name="name">Alanina Aminotransferasa (ALT)</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">U/L</field>
|
||||
<field name="description">Enzima hepática ALT</field>
|
||||
</record>
|
||||
|
||||
<!-- AST -->
|
||||
<record id="param_ast" model="lims.analysis.parameter">
|
||||
<field name="code">AST</field>
|
||||
<field name="name">Aspartato Aminotransferasa (AST)</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">U/L</field>
|
||||
<field name="description">Enzima hepática AST</field>
|
||||
</record>
|
||||
|
||||
<!-- Parámetros de Urianálisis -->
|
||||
|
||||
<!-- Color de Orina -->
|
||||
<record id="param_urine_color" model="lims.analysis.parameter">
|
||||
<field name="code">U-COLOR</field>
|
||||
<field name="name">Color</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Amarillo claro,Amarillo,Amarillo oscuro,Ámbar,Rojizo,Marrón,Turbio</field>
|
||||
<field name="description">Color de la muestra de orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Aspecto de Orina -->
|
||||
<record id="param_urine_appearance" model="lims.analysis.parameter">
|
||||
<field name="code">U-ASP</field>
|
||||
<field name="name">Aspecto</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Transparente,Ligeramente turbio,Turbio,Muy turbio</field>
|
||||
<field name="description">Aspecto de la muestra de orina</field>
|
||||
</record>
|
||||
|
||||
<!-- pH de Orina -->
|
||||
<record id="param_urine_ph" model="lims.analysis.parameter">
|
||||
<field name="code">U-PH</field>
|
||||
<field name="name">pH</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">unidades</field>
|
||||
<field name="description">pH de la orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Densidad de Orina -->
|
||||
<record id="param_urine_density" model="lims.analysis.parameter">
|
||||
<field name="code">U-DENS</field>
|
||||
<field name="name">Densidad</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">g/mL</field>
|
||||
<field name="description">Densidad específica de la orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Proteínas en Orina -->
|
||||
<record id="param_urine_protein" model="lims.analysis.parameter">
|
||||
<field name="code">U-PROT</field>
|
||||
<field name="name">Proteínas</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Negativo,Trazas,+,++,+++,++++</field>
|
||||
<field name="description">Presencia de proteínas en orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Glucosa en Orina -->
|
||||
<record id="param_urine_glucose" model="lims.analysis.parameter">
|
||||
<field name="code">U-GLU</field>
|
||||
<field name="name">Glucosa</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Negativo,Trazas,+,++,+++,++++</field>
|
||||
<field name="description">Presencia de glucosa en orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Sangre en Orina -->
|
||||
<record id="param_urine_blood" model="lims.analysis.parameter">
|
||||
<field name="code">U-SANG</field>
|
||||
<field name="name">Sangre</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Negativo,Trazas,+,++,+++</field>
|
||||
<field name="description">Presencia de sangre en orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Leucocitos en Orina -->
|
||||
<record id="param_urine_leukocytes" model="lims.analysis.parameter">
|
||||
<field name="code">U-LEU</field>
|
||||
<field name="name">Leucocitos</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">por campo</field>
|
||||
<field name="description">Leucocitos en sedimento urinario</field>
|
||||
</record>
|
||||
|
||||
<!-- Bacterias en Orina -->
|
||||
<record id="param_urine_bacteria" model="lims.analysis.parameter">
|
||||
<field name="code">U-BACT</field>
|
||||
<field name="name">Bacterias</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Escasas,Moderadas,Abundantes</field>
|
||||
<field name="description">Presencia de bacterias en orina</field>
|
||||
</record>
|
||||
|
||||
<!-- Parámetros de Microbiología -->
|
||||
|
||||
<!-- Cultivo -->
|
||||
<record id="param_culture_result" model="lims.analysis.parameter">
|
||||
<field name="code">CULT</field>
|
||||
<field name="name">Resultado del Cultivo</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Negativo,Positivo</field>
|
||||
<field name="description">Resultado del cultivo microbiológico</field>
|
||||
</record>
|
||||
|
||||
<!-- Microorganismo Aislado -->
|
||||
<record id="param_isolated_organism" model="lims.analysis.parameter">
|
||||
<field name="code">MICRO</field>
|
||||
<field name="name">Microorganismo Aislado</field>
|
||||
<field name="value_type">text</field>
|
||||
<field name="description">Identificación del microorganismo</field>
|
||||
</record>
|
||||
|
||||
<!-- Recuento de Colonias -->
|
||||
<record id="param_colony_count" model="lims.analysis.parameter">
|
||||
<field name="code">UFC</field>
|
||||
<field name="name">Recuento de Colonias</field>
|
||||
<field name="value_type">text</field>
|
||||
<field name="description">UFC/mL (Unidades Formadoras de Colonias)</field>
|
||||
</record>
|
||||
|
||||
<!-- Parámetros de Coagulación -->
|
||||
|
||||
<!-- Tiempo de Protrombina -->
|
||||
<record id="param_pt" model="lims.analysis.parameter">
|
||||
<field name="code">TP</field>
|
||||
<field name="name">Tiempo de Protrombina</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">segundos</field>
|
||||
<field name="description">Tiempo de coagulación PT</field>
|
||||
</record>
|
||||
|
||||
<!-- INR -->
|
||||
<record id="param_inr" model="lims.analysis.parameter">
|
||||
<field name="code">INR</field>
|
||||
<field name="name">INR</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">ratio</field>
|
||||
<field name="description">Índice Internacional Normalizado</field>
|
||||
</record>
|
||||
|
||||
<!-- Tiempo de Tromboplastina Parcial -->
|
||||
<record id="param_ptt" model="lims.analysis.parameter">
|
||||
<field name="code">TTP</field>
|
||||
<field name="name">Tiempo de Tromboplastina Parcial</field>
|
||||
<field name="value_type">numeric</field>
|
||||
<field name="unit">segundos</field>
|
||||
<field name="description">Tiempo de coagulación PTT</field>
|
||||
</record>
|
||||
|
||||
<!-- Parámetros de Inmunología -->
|
||||
|
||||
<!-- HIV -->
|
||||
<record id="param_hiv" model="lims.analysis.parameter">
|
||||
<field name="code">HIV</field>
|
||||
<field name="name">HIV 1/2</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
|
||||
<field name="description">Anticuerpos anti-HIV</field>
|
||||
</record>
|
||||
|
||||
<!-- Hepatitis B -->
|
||||
<record id="param_hbsag" model="lims.analysis.parameter">
|
||||
<field name="code">HBsAg</field>
|
||||
<field name="name">Antígeno de Superficie Hepatitis B</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
|
||||
<field name="description">HBsAg</field>
|
||||
</record>
|
||||
|
||||
<!-- Hepatitis C -->
|
||||
<record id="param_hcv" model="lims.analysis.parameter">
|
||||
<field name="code">HCV</field>
|
||||
<field name="name">Anticuerpos Hepatitis C</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">No Reactivo,Reactivo,Indeterminado</field>
|
||||
<field name="description">Anti-HCV</field>
|
||||
</record>
|
||||
|
||||
<!-- VDRL -->
|
||||
<record id="param_vdrl" model="lims.analysis.parameter">
|
||||
<field name="code">VDRL</field>
|
||||
<field name="name">VDRL</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">No Reactivo,Reactivo</field>
|
||||
<field name="description">Prueba de sífilis VDRL</field>
|
||||
</record>
|
||||
|
||||
<!-- Test de Embarazo -->
|
||||
<record id="param_pregnancy" model="lims.analysis.parameter">
|
||||
<field name="code">HCG</field>
|
||||
<field name="name">Prueba de Embarazo</field>
|
||||
<field name="value_type">selection</field>
|
||||
<field name="selection_values">Negativo,Positivo</field>
|
||||
<field name="description">Beta-HCG cualitativa</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,374 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Rangos para Hemoglobina -->
|
||||
<record id="range_hgb_male_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hemoglobin"/>
|
||||
<field name="name">Hombre adulto</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">13.5</field>
|
||||
<field name="normal_max">17.5</field>
|
||||
<field name="critical_min">7.0</field>
|
||||
<field name="critical_max">20.0</field>
|
||||
</record>
|
||||
|
||||
<record id="range_hgb_female_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hemoglobin"/>
|
||||
<field name="name">Mujer adulta</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="pregnant">False</field>
|
||||
<field name="normal_min">12.0</field>
|
||||
<field name="normal_max">15.5</field>
|
||||
<field name="critical_min">7.0</field>
|
||||
<field name="critical_max">20.0</field>
|
||||
</record>
|
||||
|
||||
<record id="range_hgb_female_pregnant" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hemoglobin"/>
|
||||
<field name="name">Mujer embarazada</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">15</field>
|
||||
<field name="age_max">50</field>
|
||||
<field name="pregnant">True</field>
|
||||
<field name="normal_min">11.0</field>
|
||||
<field name="normal_max">14.0</field>
|
||||
<field name="critical_min">7.0</field>
|
||||
<field name="critical_max">20.0</field>
|
||||
</record>
|
||||
|
||||
<record id="range_hgb_child" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hemoglobin"/>
|
||||
<field name="name">Niños 2-12 años</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">2</field>
|
||||
<field name="age_max">12</field>
|
||||
<field name="normal_min">11.5</field>
|
||||
<field name="normal_max">14.5</field>
|
||||
<field name="critical_min">7.0</field>
|
||||
<field name="critical_max">20.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Hematocrito -->
|
||||
<record id="range_hct_male_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hematocrit"/>
|
||||
<field name="name">Hombre adulto</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">41</field>
|
||||
<field name="normal_max">53</field>
|
||||
<field name="critical_min">20</field>
|
||||
<field name="critical_max">60</field>
|
||||
</record>
|
||||
|
||||
<record id="range_hct_female_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_hematocrit"/>
|
||||
<field name="name">Mujer adulta</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">36</field>
|
||||
<field name="normal_max">46</field>
|
||||
<field name="critical_min">20</field>
|
||||
<field name="critical_max">60</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Glóbulos Rojos -->
|
||||
<record id="range_rbc_male_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_rbc"/>
|
||||
<field name="name">Hombre adulto</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">4.5</field>
|
||||
<field name="normal_max">5.9</field>
|
||||
</record>
|
||||
|
||||
<record id="range_rbc_female_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_rbc"/>
|
||||
<field name="name">Mujer adulta</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">4.1</field>
|
||||
<field name="normal_max">5.1</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Glóbulos Blancos -->
|
||||
<record id="range_wbc_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_wbc"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">4.5</field>
|
||||
<field name="normal_max">11.0</field>
|
||||
<field name="critical_min">2.0</field>
|
||||
<field name="critical_max">30.0</field>
|
||||
</record>
|
||||
|
||||
<record id="range_wbc_child" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_wbc"/>
|
||||
<field name="name">Niño</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">2</field>
|
||||
<field name="age_max">17</field>
|
||||
<field name="normal_min">5.0</field>
|
||||
<field name="normal_max">15.0</field>
|
||||
<field name="critical_min">2.0</field>
|
||||
<field name="critical_max">30.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Plaquetas -->
|
||||
<record id="range_platelets_all" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_platelets"/>
|
||||
<field name="name">Todos</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">150</field>
|
||||
<field name="normal_max">400</field>
|
||||
<field name="critical_min">50</field>
|
||||
<field name="critical_max">1000</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Neutrófilos -->
|
||||
<record id="range_neutrophils_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_neutrophils"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">45</field>
|
||||
<field name="normal_max">70</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Linfocitos -->
|
||||
<record id="range_lymphocytes_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_lymphocytes"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">20</field>
|
||||
<field name="normal_max">45</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Glucosa -->
|
||||
<record id="range_glucose_fasting" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_glucose"/>
|
||||
<field name="name">Ayunas</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">70</field>
|
||||
<field name="normal_max">100</field>
|
||||
<field name="critical_min">40</field>
|
||||
<field name="critical_max">500</field>
|
||||
<field name="interpretation">Valores normales en ayunas. Prediabetes: 100-125 mg/dL. Diabetes: ≥126 mg/dL</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Creatinina -->
|
||||
<record id="range_creatinine_male" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_creatinine"/>
|
||||
<field name="name">Hombre adulto</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0.7</field>
|
||||
<field name="normal_max">1.3</field>
|
||||
<field name="critical_max">6.0</field>
|
||||
</record>
|
||||
|
||||
<record id="range_creatinine_female" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_creatinine"/>
|
||||
<field name="name">Mujer adulta</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0.6</field>
|
||||
<field name="normal_max">1.1</field>
|
||||
<field name="critical_max">6.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Urea -->
|
||||
<record id="range_urea_adult" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_urea"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">15</field>
|
||||
<field name="normal_max">45</field>
|
||||
<field name="critical_max">100</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Colesterol Total -->
|
||||
<record id="range_cholesterol_total" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_cholesterol_total"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0</field>
|
||||
<field name="normal_max">200</field>
|
||||
<field name="interpretation">Deseable: <200 mg/dL. Límite alto: 200-239 mg/dL. Alto: ≥240 mg/dL</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para HDL -->
|
||||
<record id="range_hdl_male" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_cholesterol_hdl"/>
|
||||
<field name="name">Hombre</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">40</field>
|
||||
<field name="normal_max">100</field>
|
||||
</record>
|
||||
|
||||
<record id="range_hdl_female" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_cholesterol_hdl"/>
|
||||
<field name="name">Mujer</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">50</field>
|
||||
<field name="normal_max">100</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para LDL -->
|
||||
<record id="range_ldl_all" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_cholesterol_ldl"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0</field>
|
||||
<field name="normal_max">100</field>
|
||||
<field name="interpretation">Óptimo: <100 mg/dL. Casi óptimo: 100-129 mg/dL. Límite alto: 130-159 mg/dL. Alto: 160-189 mg/dL. Muy alto: ≥190 mg/dL</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Triglicéridos -->
|
||||
<record id="range_triglycerides_all" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_triglycerides"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0</field>
|
||||
<field name="normal_max">150</field>
|
||||
<field name="critical_max">500</field>
|
||||
<field name="interpretation">Normal: <150 mg/dL. Límite alto: 150-199 mg/dL. Alto: 200-499 mg/dL. Muy alto: ≥500 mg/dL</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para ALT -->
|
||||
<record id="range_alt_male" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_alt"/>
|
||||
<field name="name">Hombre</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">10</field>
|
||||
<field name="normal_max">40</field>
|
||||
<field name="critical_max">1000</field>
|
||||
</record>
|
||||
|
||||
<record id="range_alt_female" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_alt"/>
|
||||
<field name="name">Mujer</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">10</field>
|
||||
<field name="normal_max">35</field>
|
||||
<field name="critical_max">1000</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para AST -->
|
||||
<record id="range_ast_all" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_ast"/>
|
||||
<field name="name">Adulto</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">18</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">10</field>
|
||||
<field name="normal_max">40</field>
|
||||
<field name="critical_max">1000</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para pH de Orina -->
|
||||
<record id="range_urine_ph" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_urine_ph"/>
|
||||
<field name="name">Normal</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">4.5</field>
|
||||
<field name="normal_max">8.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Densidad de Orina -->
|
||||
<record id="range_urine_density" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_urine_density"/>
|
||||
<field name="name">Normal</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">1.003</field>
|
||||
<field name="normal_max">1.030</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Leucocitos en Orina -->
|
||||
<record id="range_urine_leukocytes" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_urine_leukocytes"/>
|
||||
<field name="name">Normal</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0</field>
|
||||
<field name="normal_max">5</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para Tiempo de Protrombina -->
|
||||
<record id="range_pt" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_pt"/>
|
||||
<field name="name">Normal</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">11</field>
|
||||
<field name="normal_max">13.5</field>
|
||||
<field name="critical_min">9</field>
|
||||
<field name="critical_max">30</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para INR -->
|
||||
<record id="range_inr_normal" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_inr"/>
|
||||
<field name="name">Sin anticoagulación</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">0.8</field>
|
||||
<field name="normal_max">1.2</field>
|
||||
</record>
|
||||
|
||||
<!-- Rangos para TTP -->
|
||||
<record id="range_ptt" model="lims.parameter.range">
|
||||
<field name="parameter_id" ref="param_ptt"/>
|
||||
<field name="name">Normal</field>
|
||||
<field name="gender">both</field>
|
||||
<field name="age_min">0</field>
|
||||
<field name="age_max">99</field>
|
||||
<field name="normal_min">25</field>
|
||||
<field name="normal_max">35</field>
|
||||
<field name="critical_min">20</field>
|
||||
<field name="critical_max">70</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,122 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Datos de Demostración para Análisis Clínicos -->
|
||||
|
||||
<!-- Análisis: Hemograma Completo -->
|
||||
<record id="analysis_hemograma" model="product.template">
|
||||
<field name="name">Hemograma Completo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">hematology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_edta_tube"/>
|
||||
<field name="sample_volume_ml">3.0</field>
|
||||
<field name="technical_specifications">
|
||||
El hemograma completo es un análisis de sangre que mide los niveles de los principales componentes sanguíneos: glóbulos rojos, glóbulos blancos y plaquetas.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Análisis: Perfil Lipídico -->
|
||||
<record id="analysis_perfil_lipidico" model="product.template">
|
||||
<field name="name">Perfil Lipídico</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">chemistry</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_serum_tube"/>
|
||||
<field name="sample_volume_ml">2.0</field>
|
||||
<field name="technical_specifications">
|
||||
Mide los niveles de colesterol y otros lípidos en la sangre. Incluye Colesterol Total, LDL, HDL y Triglicéridos.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<!-- Análisis: Glucosa -->
|
||||
<record id="analysis_glucosa" model="product.template">
|
||||
<field name="name">Glucosa</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">chemistry</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_glucose_tube"/>
|
||||
<field name="sample_volume_ml">1.0</field>
|
||||
<field name="technical_specifications">
|
||||
Medición de glucosa en sangre para diagnóstico y control de diabetes.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Urocultivo -->
|
||||
<record id="analysis_urocultivo" model="product.template">
|
||||
<field name="name">Urocultivo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">microbiology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_urine_container"/>
|
||||
<field name="sample_volume_ml">20.0</field>
|
||||
<field name="technical_specifications">
|
||||
Cultivo de orina para identificación de microorganismos patógenos.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Tiempo de Protrombina -->
|
||||
<record id="analysis_tp" model="product.template">
|
||||
<field name="name">Tiempo de Protrombina (TP)</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">hematology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_citrate_tube"/>
|
||||
<field name="sample_volume_ml">2.7</field>
|
||||
<field name="technical_specifications">
|
||||
Prueba de coagulación para evaluar la vía extrínseca de la coagulación.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Hemocultivo -->
|
||||
<record id="analysis_hemocultivo" model="product.template">
|
||||
<field name="name">Hemocultivo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">microbiology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_blood_culture"/>
|
||||
<field name="sample_volume_ml">10.0</field>
|
||||
<field name="technical_specifications">
|
||||
Cultivo de sangre para detectar bacteriemia o fungemia.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Análisis: Coprocultivo -->
|
||||
<record id="analysis_coprocultivo" model="product.template">
|
||||
<field name="name">Coprocultivo</field>
|
||||
<field name="is_analysis">True</field>
|
||||
<field name="analysis_type">microbiology</field>
|
||||
<field name="categ_id" ref="lims_management.product_category_analysis"/>
|
||||
<field name="type">service</field>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="required_sample_type_id" ref="lims_management.sample_type_stool_container"/>
|
||||
<field name="sample_volume_ml">5.0</field>
|
||||
<field name="technical_specifications">
|
||||
Cultivo de heces para identificación de patógenos intestinales.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!--
|
||||
Note: Sale orders are created via Python script in create_lab_requests.py
|
||||
This file is kept for future non-order demo data if needed
|
||||
-->
|
||||
</odoo>
|
|
@ -1,634 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Datos de Demostración para Pacientes -->
|
||||
<record id="demo_patient_1" model="res.partner">
|
||||
<field name="name">Ana Torres</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A87B01</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1985-05-15</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="email">ana.torres@example.com</field>
|
||||
<field name="vat">03245678-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_2" model="res.partner">
|
||||
<field name="name">Carlos Ruiz</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-C45D02</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1992-11-20</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7892-3456</field>
|
||||
<field name="email">carlos.ruiz@example.com</field>
|
||||
<field name="vat">04567890-1</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_3" model="res.partner">
|
||||
<field name="name">María González</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M78E03</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1978-03-10</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="email">maria.gonzalez@example.com</field>
|
||||
<field name="vat">01234567-8</field>
|
||||
</record>
|
||||
|
||||
<!-- Datos de Demostración para Médicos -->
|
||||
<record id="demo_doctor_1" model="res.partner">
|
||||
<field name="name">Dr. Luis Herrera</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-98765</field>
|
||||
<field name="phone">+503 2234-5678</field>
|
||||
<field name="email">luis.herrera@hospital.com</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_doctor_2" model="res.partner">
|
||||
<field name="name">Dra. Sofia Vargas</field>
|
||||
<field name="is_doctor" eval="True"/>
|
||||
<field name="doctor_license">L-54321</field>
|
||||
<field name="phone">+503 2345-6789</field>
|
||||
<field name="email">sofia.vargas@clinic.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Datos de Demostración para Tutor y Paciente Menor de Edad -->
|
||||
<record id="demo_tutor_1" model="res.partner">
|
||||
<field name="name">Laura Mendoza</field>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="email">laura.mendoza@example.com</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_minor_1" model="res.partner">
|
||||
<field name="name">Pedro Infante Jr.</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M12E03</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="parent_id" ref="demo_tutor_1"/>
|
||||
</record>
|
||||
|
||||
<!-- Pacientes adicionales - Niños (0-12 años) -->
|
||||
<record id="demo_patient_4" model="res.partner">
|
||||
<field name="name">Sofía Jiménez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-S45F04</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=8)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7567-8901</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_5" model="res.partner">
|
||||
<field name="name">Diego Morales</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-D78M05</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7678-9012</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_6" model="res.partner">
|
||||
<field name="name">Valentina Castro</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-V23F06</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=10)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7789-0123</field>
|
||||
</record>
|
||||
|
||||
<!-- Adolescentes (13-17 años) -->
|
||||
<record id="demo_patient_7" model="res.partner">
|
||||
<field name="name">Santiago Pérez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-S90M07</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=15)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7890-1234</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_8" model="res.partner">
|
||||
<field name="name">Isabella Rodríguez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-I34F08</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=16)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7901-2345</field>
|
||||
</record>
|
||||
|
||||
<!-- Adultos jóvenes (18-35 años) - Incluye embarazadas -->
|
||||
<record id="demo_patient_9" model="res.partner">
|
||||
<field name="name">Camila Fernández</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-C67F09</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1995-07-22</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7012-3456</field>
|
||||
<field name="email">camila.fernandez@example.com</field>
|
||||
<field name="vat">05678901-2</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_10" model="res.partner">
|
||||
<field name="name">Alejandro Gutiérrez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A12M10</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1990-02-14</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7123-4567</field>
|
||||
<field name="email">alejandro.gutierrez@example.com</field>
|
||||
<field name="vat">06789012-3</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_11" model="res.partner">
|
||||
<field name="name">Lucía Mendoza</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-L89F11</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1992-09-30</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="vat">07890123-4</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_12" model="res.partner">
|
||||
<field name="name">Miguel Ángel Silva</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M45M12</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1988-11-05</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="vat">08901234-5</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_13" model="res.partner">
|
||||
<field name="name">Natalia Vargas</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-N78F13</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1996-04-18</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="vat">09012345-6</field>
|
||||
</record>
|
||||
|
||||
<!-- Adultos (36-55 años) -->
|
||||
<record id="demo_patient_14" model="res.partner">
|
||||
<field name="name">Roberto Martínez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-R23M14</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1975-06-12</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7567-8901</field>
|
||||
<field name="vat">00123456-7</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_15" model="res.partner">
|
||||
<field name="name">Patricia López</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-P56F15</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1972-12-25</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7678-9012</field>
|
||||
<field name="vat">01234567-8</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_16" model="res.partner">
|
||||
<field name="name">Fernando Díaz</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-F90M16</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1980-03-08</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7789-0123</field>
|
||||
<field name="vat">02345678-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_17" model="res.partner">
|
||||
<field name="name">Andrea Herrera</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A34F17</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1978-08-17</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7890-1234</field>
|
||||
<field name="vat">03456789-0</field>
|
||||
</record>
|
||||
|
||||
<!-- Adultos mayores (56-75 años) -->
|
||||
<record id="demo_patient_18" model="res.partner">
|
||||
<field name="name">José Luis Ramírez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-J67M18</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1965-01-20</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7901-2345</field>
|
||||
<field name="vat">04567890-1</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_19" model="res.partner">
|
||||
<field name="name">Carmen Sánchez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-C12F19</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1958-10-15</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7012-3456</field>
|
||||
<field name="vat">05678901-2</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_20" model="res.partner">
|
||||
<field name="name">Ricardo Flores</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-R89M20</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1960-05-28</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7123-4567</field>
|
||||
<field name="vat">06789012-3</field>
|
||||
</record>
|
||||
|
||||
<!-- Ancianos (76+ años) -->
|
||||
<record id="demo_patient_21" model="res.partner">
|
||||
<field name="name">Esperanza Romero</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-E45F21</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1945-12-03</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="vat">07890123-4</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_22" model="res.partner">
|
||||
<field name="name">Francisco Aguilar</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-F78M22</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1943-07-19</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="vat">08901234-5</field>
|
||||
</record>
|
||||
|
||||
<!-- Más pacientes diversos -->
|
||||
<record id="demo_patient_23" model="res.partner">
|
||||
<field name="name">Daniela Cortés</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-D23F23</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1998-02-11</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="vat">09012345-6</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_24" model="res.partner">
|
||||
<field name="name">Gabriel Moreno</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-G56M24</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=6)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7567-8901</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_25" model="res.partner">
|
||||
<field name="name">Valeria Ruiz</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-V90F25</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1987-09-24</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7678-9012</field>
|
||||
<field name="vat">00123456-7</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_26" model="res.partner">
|
||||
<field name="name">Eduardo Navarro</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-E34M26</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1970-11-30</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7789-0123</field>
|
||||
<field name="vat">01234568-8</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_27" model="res.partner">
|
||||
<field name="name">Mariana Delgado</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M67F27</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1999-06-07</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7890-1234</field>
|
||||
<field name="vat">02345679-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_28" model="res.partner">
|
||||
<field name="name">Andrés Jiménez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A12M28</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1955-08-21</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7901-2345</field>
|
||||
<field name="vat">03456780-0</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_29" model="res.partner">
|
||||
<field name="name">Paola Méndez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-P89F29</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1991-03-16</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7012-3456</field>
|
||||
<field name="vat">04567891-1</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_30" model="res.partner">
|
||||
<field name="name">Sebastián Vega</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-S45M30</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=14)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7123-4567</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_31" model="res.partner">
|
||||
<field name="name">Claudia Paredes</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-C78F31</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1982-10-09</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="vat">05678902-2</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_32" model="res.partner">
|
||||
<field name="name">Raúl Castro</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-R23M32</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1948-04-27</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="vat">06789013-3</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_33" model="res.partner">
|
||||
<field name="name">Adriana Guerrero</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A56F33</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1994-12-13</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="vat">07890124-4</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_34" model="res.partner">
|
||||
<field name="name">Javier Molina</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-J90M34</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=9)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7567-8901</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_35" model="res.partner">
|
||||
<field name="name">Rosa María Ochoa</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-R34F35</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1962-01-05</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7678-9012</field>
|
||||
<field name="vat">08901235-5</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_36" model="res.partner">
|
||||
<field name="name">Manuel Reyes</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M67M36</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1976-07-31</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7789-0123</field>
|
||||
<field name="vat">09012346-6</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_37" model="res.partner">
|
||||
<field name="name">Teresa Campos</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-T12F37</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1940-09-18</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7890-1234</field>
|
||||
<field name="vat">00123457-7</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_38" model="res.partner">
|
||||
<field name="name">Pablo Espinoza</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-P89M38</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1989-05-03</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7901-2345</field>
|
||||
<field name="vat">01234569-8</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_39" model="res.partner">
|
||||
<field name="name">Mónica Villanueva</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-M45F39</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1985-11-26</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7012-3456</field>
|
||||
<field name="vat">02345670-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_40" model="res.partner">
|
||||
<field name="name">Diego Alejandro Luna</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-D78M40</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7123-4567</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_41" model="res.partner">
|
||||
<field name="name">Beatriz Salazar</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-B23F41</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1968-02-14</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
<field name="vat">03456781-0</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_42" model="res.partner">
|
||||
<field name="name">Héctor Valdés</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-H56M42</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1973-06-29</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="vat">04567892-1</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_43" model="res.partner">
|
||||
<field name="name">Silvia Peña</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-S90F43</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1997-08-11</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="vat">05678903-2</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_44" model="res.partner">
|
||||
<field name="name">Arturo Domínguez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A34M44</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1951-12-07</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7567-8901</field>
|
||||
<field name="vat">06789014-3</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_45" model="res.partner">
|
||||
<field name="name">Gloria Ríos</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-G67F45</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1983-04-22</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7678-9012</field>
|
||||
<field name="vat">07890125-4</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_46" model="res.partner">
|
||||
<field name="name">Emilio Núñez</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-E12M46</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=11)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7789-0123</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_47" model="res.partner">
|
||||
<field name="name">Laura Patricia Ibarra</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-L89F47</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1979-10-16</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7890-1234</field>
|
||||
<field name="vat">08901236-5</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_48" model="res.partner">
|
||||
<field name="name">Óscar Medina</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-O45M48</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1966-03-25</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7901-2345</field>
|
||||
<field name="vat">09012347-6</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_49" model="res.partner">
|
||||
<field name="name">Verónica Soto</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-V78F49</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1993-07-08</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="is_pregnant" eval="True"/>
|
||||
<field name="phone">+503 7012-3456</field>
|
||||
<field name="vat">00123458-7</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_50" model="res.partner">
|
||||
<field name="name">Rubén Contreras</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-R23M50</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1937-11-14</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7123-4567</field>
|
||||
<field name="vat">01234560-8</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_51" model="res.partner">
|
||||
<field name="name">Alejandra Fuentes</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-A56F51</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date" eval="(datetime.now() - relativedelta(years=7)).strftime('%Y-%m-%d')"/>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7234-5678</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_52" model="res.partner">
|
||||
<field name="name">Nicolás Ramos</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-N90M52</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">1986-01-19</field>
|
||||
<field name="gender">male</field>
|
||||
<field name="phone">+503 7345-6789</field>
|
||||
<field name="vat">02345671-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_53" model="res.partner">
|
||||
<field name="name">Fernanda Acosta</field>
|
||||
<field name="is_patient" eval="True"/>
|
||||
<field name="patient_identifier">P-F34F53</field>
|
||||
<field name="origin">Carga Inicial</field>
|
||||
<field name="birthdate_date">2000-05-12</field>
|
||||
<field name="gender">female</field>
|
||||
<field name="phone">+503 7456-7890</field>
|
||||
<field name="vat">03456782-0</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Muestras de Laboratorio (Lotes) con el nuevo campo sample_type_product_id -->
|
||||
<record id="lab_sample_01" model="stock.lot">
|
||||
<field name="name">SAM-2025-00001</field>
|
||||
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_serum_tube').product_variant_id.id"/>
|
||||
<field name="is_lab_sample" eval="True"/>
|
||||
<field name="patient_id" ref="lims_management.demo_patient_1"/>
|
||||
<field name="collector_id" ref="base.user_admin"/>
|
||||
<field name="collection_date" eval="(DateTime.now() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="sample_type_product_id" ref="lims_management.sample_type_serum_tube"/>
|
||||
<field name="container_type">serum_tube</field>
|
||||
<field name="state">received</field>
|
||||
</record>
|
||||
|
||||
<record id="lab_sample_02" model="stock.lot">
|
||||
<field name="name">SAM-2025-00002</field>
|
||||
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_edta_tube').product_variant_id.id"/>
|
||||
<field name="is_lab_sample" eval="True"/>
|
||||
<field name="patient_id" ref="lims_management.demo_patient_2"/>
|
||||
<field name="collector_id" ref="base.user_admin"/>
|
||||
<field name="collection_date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="sample_type_product_id" ref="lims_management.sample_type_edta_tube"/>
|
||||
<field name="container_type">edta_tube</field>
|
||||
<field name="state">in_process</field>
|
||||
</record>
|
||||
|
||||
<record id="lab_sample_03" model="stock.lot">
|
||||
<field name="name">SAM-2025-00003</field>
|
||||
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_urine_container').product_variant_id.id"/>
|
||||
<field name="is_lab_sample" eval="True"/>
|
||||
<field name="patient_id" ref="lims_management.demo_patient_3"/>
|
||||
<field name="collector_id" ref="base.user_admin"/>
|
||||
<field name="collection_date" eval="(DateTime.now() - timedelta(hours=6)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="sample_type_product_id" ref="lims_management.sample_type_urine_container"/>
|
||||
<field name="container_type">urine</field>
|
||||
<field name="state">collected</field>
|
||||
</record>
|
||||
|
||||
<record id="lab_sample_04" model="stock.lot">
|
||||
<field name="name">SAM-2025-00004</field>
|
||||
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_citrate_tube').product_variant_id.id"/>
|
||||
<field name="is_lab_sample" eval="True"/>
|
||||
<field name="patient_id" ref="lims_management.demo_patient_1"/>
|
||||
<field name="collector_id" ref="base.user_admin"/>
|
||||
<field name="collection_date" eval="(DateTime.now() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="sample_type_product_id" ref="lims_management.sample_type_citrate_tube"/>
|
||||
<field name="state">analyzed</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,13 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import analysis_parameter
|
||||
from . import product_template_parameter
|
||||
from . import parameter_range
|
||||
from . import product
|
||||
from . import partner
|
||||
from . import sale_order
|
||||
from . import stock_lot
|
||||
from . import rejection_reason
|
||||
from . import lims_test
|
||||
from . import lims_result
|
||||
from . import res_config_settings
|
||||
from . import lims_config
|
||||
from . import product
|
||||
from . import analysis_range
|
|
@ -1,144 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class LimsAnalysisParameter(models.Model):
|
||||
_name = 'lims.analysis.parameter'
|
||||
_description = 'Catálogo de Parámetros de Laboratorio'
|
||||
_order = 'name'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Nombre',
|
||||
required=True,
|
||||
help='Nombre descriptivo del parámetro (ej: Hemoglobina)'
|
||||
)
|
||||
|
||||
code = fields.Char(
|
||||
string='Código',
|
||||
required=True,
|
||||
help='Código único del parámetro (ej: HGB)'
|
||||
)
|
||||
|
||||
value_type = fields.Selection([
|
||||
('numeric', 'Numérico'),
|
||||
('text', 'Texto'),
|
||||
('boolean', 'Sí/No'),
|
||||
('selection', 'Selección')
|
||||
],
|
||||
string='Tipo de Valor',
|
||||
required=True,
|
||||
default='numeric',
|
||||
help='Tipo de dato que acepta este parámetro'
|
||||
)
|
||||
|
||||
unit = fields.Char(
|
||||
string='Unidad de Medida',
|
||||
help='Unidad de medida del parámetro (ej: g/dL, mg/dL, %)'
|
||||
)
|
||||
|
||||
selection_values = fields.Text(
|
||||
string='Valores de Selección',
|
||||
help='Para tipo "Selección", ingrese los valores posibles separados por comas'
|
||||
)
|
||||
|
||||
description = fields.Text(
|
||||
string='Descripción',
|
||||
help='Descripción detallada del parámetro y su significado clínico'
|
||||
)
|
||||
|
||||
active = fields.Boolean(
|
||||
string='Activo',
|
||||
default=True,
|
||||
help='Si está desmarcado, el parámetro no estará disponible para nuevas configuraciones'
|
||||
)
|
||||
|
||||
category_id = fields.Many2one(
|
||||
'product.category',
|
||||
string='Categoría',
|
||||
domain="[('parent_id.name', '=', 'Análisis de Laboratorio')]",
|
||||
help='Categoría del parámetro para agrupar en reportes'
|
||||
)
|
||||
|
||||
# Relaciones
|
||||
template_parameter_ids = fields.One2many(
|
||||
'product.template.parameter',
|
||||
'parameter_id',
|
||||
string='Análisis que usan este parámetro'
|
||||
)
|
||||
|
||||
range_ids = fields.One2many(
|
||||
'lims.parameter.range',
|
||||
'parameter_id',
|
||||
string='Rangos de Referencia'
|
||||
)
|
||||
|
||||
# Campos computados
|
||||
analysis_count = fields.Integer(
|
||||
string='Cantidad de Análisis',
|
||||
compute='_compute_analysis_count',
|
||||
store=True
|
||||
)
|
||||
|
||||
@api.depends('template_parameter_ids')
|
||||
def _compute_analysis_count(self):
|
||||
for record in self:
|
||||
record.analysis_count = len(record.template_parameter_ids)
|
||||
|
||||
@api.constrains('code')
|
||||
def _check_code_unique(self):
|
||||
for record in self:
|
||||
if self.search_count([
|
||||
('code', '=', record.code),
|
||||
('id', '!=', record.id)
|
||||
]) > 0:
|
||||
raise ValidationError(f'El código "{record.code}" ya existe. Los códigos deben ser únicos.')
|
||||
|
||||
@api.constrains('value_type', 'selection_values')
|
||||
def _check_selection_values(self):
|
||||
for record in self:
|
||||
if record.value_type == 'selection' and not record.selection_values:
|
||||
raise ValidationError('Debe especificar los valores de selección para parámetros de tipo "Selección".')
|
||||
|
||||
@api.constrains('value_type', 'unit')
|
||||
def _check_numeric_unit(self):
|
||||
for record in self:
|
||||
if record.value_type == 'numeric' and not record.unit:
|
||||
raise ValidationError('Los parámetros numéricos deben tener una unidad de medida.')
|
||||
|
||||
def get_selection_list(self):
|
||||
"""Devuelve la lista de valores de selección como una lista de Python"""
|
||||
self.ensure_one()
|
||||
if self.value_type == 'selection' and self.selection_values:
|
||||
return [val.strip() for val in self.selection_values.split(',') if val.strip()]
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# Convertir código a mayúsculas
|
||||
if 'code' in vals:
|
||||
vals['code'] = vals['code'].upper()
|
||||
return super(LimsAnalysisParameter, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
# Convertir código a mayúsculas
|
||||
if 'code' in vals:
|
||||
vals['code'] = vals['code'].upper()
|
||||
return super(LimsAnalysisParameter, self).write(vals)
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = f"[{record.code}] {record.name}"
|
||||
if record.unit:
|
||||
name += f" ({record.unit})"
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||
args = args or []
|
||||
if name:
|
||||
args = ['|', ('code', operator, name), ('name', operator, name)] + args
|
||||
return self._search(args, limit=limit, access_rights_uid=name_get_uid)
|
26
lims_management/models/analysis_range.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class LimsAnalysisRange(models.Model):
|
||||
_name = 'lims.analysis.range'
|
||||
_description = 'Rangos de Referencia para Análisis Clínicos'
|
||||
|
||||
analysis_id = fields.Many2one(
|
||||
'product.template',
|
||||
string="Análisis",
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
gender = fields.Selection([
|
||||
('male', 'Masculino'),
|
||||
('female', 'Femenino'),
|
||||
('both', 'Ambos')
|
||||
], string="Género", default='both')
|
||||
|
||||
age_min = fields.Integer(string="Edad Mínima", default=0)
|
||||
age_max = fields.Integer(string="Edad Máxima", default=99)
|
||||
|
||||
min_value = fields.Float(string="Valor Mínimo")
|
||||
max_value = fields.Float(string="Valor Máximo")
|
||||
|
||||
unit_of_measure = fields.Char(string="Unidad de Medida")
|
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class LimsConfig(models.TransientModel):
|
||||
_name = 'lims.config.settings'
|
||||
_inherit = 'res.config.settings'
|
||||
_description = 'Configuración del Laboratorio'
|
||||
|
||||
auto_resample_on_rejection = fields.Boolean(
|
||||
string='Re-muestreo Automático al Rechazar',
|
||||
help='Si está activo, se generará automáticamente una nueva muestra cuando se rechace una existente',
|
||||
config_parameter='lims_management.auto_resample_on_rejection',
|
||||
default=True
|
||||
)
|
||||
|
||||
resample_state = fields.Selection([
|
||||
('pending_collection', 'Pendiente de Recolección'),
|
||||
('collected', 'Recolectada'),
|
||||
], string='Estado Inicial para Re-muestras',
|
||||
help='Estado en el que se crearán las nuevas muestras generadas por re-muestreo',
|
||||
config_parameter='lims_management.resample_state',
|
||||
default='pending_collection'
|
||||
)
|
||||
|
||||
auto_notify_resample = fields.Boolean(
|
||||
string='Notificar Re-muestreo Automático',
|
||||
help='Enviar notificación al recepcionista cuando se genera una nueva muestra por re-muestreo',
|
||||
config_parameter='lims_management.auto_notify_resample',
|
||||
default=True
|
||||
)
|
||||
|
||||
resample_prefix = fields.Char(
|
||||
string='Prefijo para Re-muestras',
|
||||
help='Prefijo que se añadirá al código de las muestras generadas por re-muestreo (ej: RE-)',
|
||||
config_parameter='lims_management.resample_prefix',
|
||||
default='RE-'
|
||||
)
|
||||
|
||||
max_resample_attempts = fields.Integer(
|
||||
string='Máximo de Re-muestreos',
|
||||
help='Número máximo de veces que se puede re-muestrear una muestra (0 = sin límite)',
|
||||
config_parameter='lims_management.max_resample_attempts',
|
||||
default=3
|
||||
)
|
|
@ -1,537 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LimsResult(models.Model):
|
||||
_name = 'lims.result'
|
||||
_description = 'Resultado de Prueba de Laboratorio'
|
||||
_rec_name = 'display_name'
|
||||
_order = 'test_id, sequence'
|
||||
|
||||
display_name = fields.Char(
|
||||
string='Nombre',
|
||||
compute='_compute_display_name',
|
||||
store=True
|
||||
)
|
||||
|
||||
test_id = fields.Many2one(
|
||||
'lims.test',
|
||||
string='Prueba',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
# Campo relacionado para acceder a la muestra sin duplicar datos
|
||||
test_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra',
|
||||
related='test_id.sample_id',
|
||||
readonly=True,
|
||||
store=True # Para poder buscar y filtrar
|
||||
)
|
||||
|
||||
# Campo relacionado para mostrar el estado sin duplicar
|
||||
test_sample_state = fields.Selection(
|
||||
string='Estado de Muestra',
|
||||
related='test_sample_id.state',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Cambio de parameter_name a parameter_id
|
||||
parameter_id = fields.Many2one(
|
||||
'lims.analysis.parameter',
|
||||
string='Parámetro',
|
||||
required=True,
|
||||
ondelete='restrict'
|
||||
)
|
||||
|
||||
# Mantener parameter_name como campo related para compatibilidad
|
||||
parameter_name = fields.Char(
|
||||
string='Nombre del Parámetro',
|
||||
related='parameter_id.name',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_code = fields.Char(
|
||||
string='Código',
|
||||
related='parameter_id.code',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='Secuencia',
|
||||
default=10
|
||||
)
|
||||
|
||||
# Campos relacionados del parámetro
|
||||
parameter_value_type = fields.Selection(
|
||||
related='parameter_id.value_type',
|
||||
string='Tipo de Valor',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_unit = fields.Char(
|
||||
related='parameter_id.unit',
|
||||
string='Unidad',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Valores del resultado
|
||||
value_numeric = fields.Float(
|
||||
string='Valor Numérico'
|
||||
)
|
||||
|
||||
value_text = fields.Char(
|
||||
string='Valor de Texto'
|
||||
)
|
||||
|
||||
value_selection = fields.Char(
|
||||
string='Valor de Selección',
|
||||
help='Ingrese el valor o las primeras letras. Ej: P para Positivo, N para Negativo'
|
||||
)
|
||||
|
||||
# Campo para mostrar las opciones disponibles
|
||||
selection_options_display = fields.Char(
|
||||
string='Opciones disponibles',
|
||||
compute='_compute_selection_options_display',
|
||||
help='Opciones válidas para este parámetro'
|
||||
)
|
||||
|
||||
value_boolean = fields.Boolean(
|
||||
string='Valor Sí/No'
|
||||
)
|
||||
|
||||
# Campo unificado para mostrar el valor
|
||||
value_display = fields.Char(
|
||||
string='Valor',
|
||||
compute='_compute_value_display',
|
||||
store=True
|
||||
)
|
||||
|
||||
# Campos computados para validación de rangos
|
||||
applicable_range_id = fields.Many2one(
|
||||
'lims.parameter.range',
|
||||
compute='_compute_applicable_range',
|
||||
string='Rango Aplicable',
|
||||
store=False
|
||||
)
|
||||
|
||||
is_out_of_range = fields.Boolean(
|
||||
string='Fuera de Rango',
|
||||
compute='_compute_is_out_of_range',
|
||||
store=True
|
||||
)
|
||||
|
||||
is_critical = fields.Boolean(
|
||||
string='Valor Crítico',
|
||||
compute='_compute_is_out_of_range',
|
||||
store=True
|
||||
)
|
||||
|
||||
notes = fields.Text(
|
||||
string='Notas del Técnico'
|
||||
)
|
||||
|
||||
# Información del paciente (para cálculo de rangos)
|
||||
patient_id = fields.Many2one(
|
||||
related='test_id.patient_id',
|
||||
string='Paciente',
|
||||
store=True
|
||||
)
|
||||
|
||||
test_date = fields.Datetime(
|
||||
related='test_id.create_date',
|
||||
string='Fecha de la Prueba',
|
||||
store=True
|
||||
)
|
||||
|
||||
result_status = fields.Selection([
|
||||
('normal', 'Normal'),
|
||||
('abnormal', 'Anormal'),
|
||||
('critical', 'Crítico')
|
||||
], string='Estado', compute='_compute_result_status', store=True)
|
||||
|
||||
@api.depends('test_id', 'parameter_name')
|
||||
def _compute_display_name(self):
|
||||
"""Calcula el nombre a mostrar."""
|
||||
for record in self:
|
||||
if record.test_id and record.parameter_name:
|
||||
record.display_name = f"{record.test_id.name} - {record.parameter_name}"
|
||||
else:
|
||||
record.display_name = record.parameter_name or _('Nuevo')
|
||||
|
||||
@api.depends('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
|
||||
def _compute_value_display(self):
|
||||
"""Calcula el valor a mostrar según el tipo de dato."""
|
||||
for record in self:
|
||||
if record.parameter_value_type == 'numeric':
|
||||
if record.value_numeric is not False:
|
||||
record.value_display = f"{record.value_numeric} {record.parameter_unit or ''}"
|
||||
else:
|
||||
record.value_display = ''
|
||||
elif record.parameter_value_type == 'text':
|
||||
record.value_display = record.value_text or ''
|
||||
elif record.parameter_value_type == 'selection':
|
||||
record.value_display = record.value_selection or ''
|
||||
elif record.parameter_value_type == 'boolean':
|
||||
record.value_display = 'Sí' if record.value_boolean else 'No'
|
||||
else:
|
||||
record.value_display = ''
|
||||
|
||||
@api.depends('parameter_id', 'patient_id', 'test_date')
|
||||
def _compute_applicable_range(self):
|
||||
"""Determina el rango de referencia aplicable según el paciente."""
|
||||
for record in self:
|
||||
if not record.parameter_id or not record.patient_id:
|
||||
record.applicable_range_id = False
|
||||
continue
|
||||
|
||||
# Calcular edad del paciente en la fecha del test
|
||||
if record.test_date:
|
||||
age = record.patient_id.get_age_at_date(record.test_date.date())
|
||||
else:
|
||||
age = record.patient_id.age
|
||||
|
||||
# Buscar rango más específico
|
||||
domain = [
|
||||
('parameter_id', '=', record.parameter_id.id),
|
||||
('age_min', '<=', age),
|
||||
('age_max', '>=', age),
|
||||
'|',
|
||||
('gender', '=', record.patient_id.gender),
|
||||
('gender', '=', 'both')
|
||||
]
|
||||
|
||||
# Considerar embarazo si aplica
|
||||
if record.patient_id.gender == 'female' and record.patient_id.is_pregnant:
|
||||
domain.append(('pregnant', '=', True))
|
||||
|
||||
# Ordenar para obtener el más específico primero
|
||||
ranges = self.env['lims.parameter.range'].search(
|
||||
domain,
|
||||
order='gender desc, pregnant desc',
|
||||
limit=1
|
||||
)
|
||||
|
||||
record.applicable_range_id = ranges[0] if ranges else False
|
||||
|
||||
@api.depends('value_numeric', 'applicable_range_id', 'parameter_value_type')
|
||||
def _compute_is_out_of_range(self):
|
||||
"""Determina si el valor está fuera del rango normal y si es crítico."""
|
||||
for record in self:
|
||||
record.is_out_of_range = False
|
||||
record.is_critical = False
|
||||
|
||||
# Solo aplica para valores numéricos
|
||||
if record.parameter_value_type != 'numeric' or record.value_numeric is False:
|
||||
continue
|
||||
|
||||
if not record.applicable_range_id:
|
||||
continue
|
||||
|
||||
range_obj = record.applicable_range_id
|
||||
status = range_obj.get_value_status(record.value_numeric)
|
||||
|
||||
record.is_out_of_range = (status != 'normal')
|
||||
record.is_critical = (status == 'critical')
|
||||
|
||||
@api.depends('parameter_id', 'value_numeric', 'is_out_of_range', 'is_critical', 'parameter_value_type')
|
||||
def _compute_result_status(self):
|
||||
"""Calcula el estado visual del resultado."""
|
||||
for record in self:
|
||||
if record.parameter_value_type != 'numeric':
|
||||
record.result_status = 'normal'
|
||||
elif record.is_critical:
|
||||
record.result_status = 'critical'
|
||||
elif record.is_out_of_range:
|
||||
record.result_status = 'abnormal'
|
||||
else:
|
||||
record.result_status = 'normal'
|
||||
|
||||
@api.constrains('value_numeric', 'value_text', 'value_selection', 'value_boolean', 'parameter_value_type')
|
||||
def _check_value_type(self):
|
||||
"""Asegura que el valor ingresado corresponda al tipo de parámetro."""
|
||||
# Skip validation if we're in initialization context
|
||||
if self.env.context.get('skip_value_validation'):
|
||||
return
|
||||
|
||||
for record in self:
|
||||
if not record.parameter_id:
|
||||
continue
|
||||
|
||||
value_type = record.parameter_value_type
|
||||
has_value = False
|
||||
|
||||
if value_type == 'numeric':
|
||||
has_value = record.value_numeric not in [False, 0.0]
|
||||
if record.value_text or record.value_selection:
|
||||
raise ValidationError(
|
||||
_('Para parámetros numéricos solo se debe ingresar el valor numérico.')
|
||||
)
|
||||
elif value_type == 'text':
|
||||
has_value = bool(record.value_text)
|
||||
if (record.value_numeric not in [False, 0.0]) or record.value_selection or record.value_boolean:
|
||||
raise ValidationError(
|
||||
_('Para parámetros de texto solo se debe ingresar el valor de texto.')
|
||||
)
|
||||
elif value_type == 'selection':
|
||||
has_value = bool(record.value_selection)
|
||||
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_boolean:
|
||||
raise ValidationError(
|
||||
_('Para parámetros de selección solo se debe elegir una opción.')
|
||||
)
|
||||
# Validar que el valor seleccionado sea válido
|
||||
if has_value and record.parameter_id:
|
||||
valid_options = record.parameter_id.get_selection_list()
|
||||
if valid_options and record.value_selection not in valid_options:
|
||||
# Intentar autocompletar antes de rechazar
|
||||
autocompleted = record._validate_and_autocomplete_selection(record.value_selection)
|
||||
if autocompleted not in valid_options:
|
||||
raise ValidationError(
|
||||
_('El valor "%s" no es una opción válida. Opciones disponibles: %s') %
|
||||
(record.value_selection, ', '.join(valid_options))
|
||||
)
|
||||
elif value_type == 'boolean':
|
||||
has_value = True # Boolean siempre tiene valor (True o False)
|
||||
if (record.value_numeric not in [False, 0.0]) or record.value_text or record.value_selection:
|
||||
raise ValidationError(
|
||||
_('Para parámetros Sí/No solo se debe marcar el checkbox.')
|
||||
)
|
||||
|
||||
# Solo requerir valor si la prueba existe y no está en borrador
|
||||
if not has_value and record.parameter_id and record.test_id and record.test_id.state != 'draft':
|
||||
raise ValidationError(
|
||||
_('Debe ingresar un valor para el resultado del parámetro %s.') % record.parameter_name
|
||||
)
|
||||
|
||||
@api.onchange('parameter_id')
|
||||
def _onchange_parameter_id(self):
|
||||
"""Limpia los valores cuando se cambia el parámetro."""
|
||||
if self.parameter_id:
|
||||
# Limpiar todos los valores
|
||||
self.value_numeric = False
|
||||
self.value_text = False
|
||||
self.value_selection = False
|
||||
self.value_boolean = False
|
||||
|
||||
# Si es selección, obtener las opciones
|
||||
if self.parameter_value_type == 'selection' and self.parameter_id.selection_values:
|
||||
# Esto se usará en las vistas para mostrar las opciones dinámicamente
|
||||
pass
|
||||
|
||||
@api.depends('parameter_id', 'parameter_id.selection_values')
|
||||
def _compute_selection_options_display(self):
|
||||
"""Calcula las opciones disponibles para mostrar al usuario."""
|
||||
for record in self:
|
||||
if record.parameter_id and record.parameter_value_type == 'selection':
|
||||
options = record.parameter_id.get_selection_list()
|
||||
if options:
|
||||
record.selection_options_display = ' | '.join(options)
|
||||
else:
|
||||
record.selection_options_display = 'Sin opciones definidas'
|
||||
else:
|
||||
record.selection_options_display = False
|
||||
|
||||
@api.onchange('value_selection')
|
||||
def _onchange_value_selection(self):
|
||||
"""Autocompleta el valor de selección basado en coincidencia parcial."""
|
||||
if self.value_selection and self.parameter_id and self.parameter_value_type == 'selection':
|
||||
# Obtener las opciones disponibles
|
||||
options = self.parameter_id.get_selection_list()
|
||||
if options:
|
||||
# Convertir el valor ingresado a mayúsculas para comparación
|
||||
input_upper = self.value_selection.upper().strip()
|
||||
|
||||
# Buscar coincidencias
|
||||
matches = []
|
||||
for option in options:
|
||||
option_upper = option.upper()
|
||||
if option_upper.startswith(input_upper):
|
||||
matches.append(option)
|
||||
|
||||
# Si hay exactamente una coincidencia, autocompletar
|
||||
if len(matches) == 1:
|
||||
self.value_selection = matches[0]
|
||||
elif len(matches) == 0:
|
||||
# Si no hay coincidencias directas, buscar coincidencias parciales
|
||||
for option in options:
|
||||
if input_upper in option.upper():
|
||||
matches.append(option)
|
||||
|
||||
# Si hay una sola coincidencia parcial, autocompletar
|
||||
if len(matches) == 1:
|
||||
self.value_selection = matches[0]
|
||||
|
||||
@api.onchange('value_numeric', 'is_critical')
|
||||
def _onchange_critical_value(self):
|
||||
"""Autocompleta las notas cuando el valor es crítico."""
|
||||
if self.is_critical and self.parameter_value_type == 'numeric' and self.value_numeric:
|
||||
# Diccionario de notas médicas para parámetros críticos
|
||||
CRITICAL_NOTES = {
|
||||
'glucosa': {
|
||||
'high': 'Valor elevado de glucosa. Posible prediabetes o diabetes. Se recomienda repetir la prueba en ayunas y consultar con endocrinología.',
|
||||
'low': 'Hipoglucemia detectada. Riesgo de síntomas neuroglucogénicos. Evaluar causas: medicamentos, insuficiencia hepática o endocrinopatías.'
|
||||
},
|
||||
'hemoglobina': {
|
||||
'high': 'Policitemia. Evaluar posibles causas: deshidratación, tabaquismo, cardiopatía o policitemia vera.',
|
||||
'low': 'Anemia severa. Investigar origen: deficiencia de hierro, pérdida sanguínea, hemólisis o enfermedad crónica.'
|
||||
},
|
||||
'hematocrito': {
|
||||
'high': 'Hemoconcentración. Correlacionar con hemoglobina. Descartar deshidratación o policitemia.',
|
||||
'low': 'Valor compatible con anemia. Evaluar junto con hemoglobina e índices eritrocitarios.'
|
||||
},
|
||||
'leucocitos': {
|
||||
'high': 'Leucocitosis marcada. Descartar proceso infeccioso, inflamatorio o hematológico.',
|
||||
'low': 'Leucopenia severa. Riesgo de infecciones. Evaluar causas: viral, medicamentosa o hematológica.'
|
||||
},
|
||||
'plaquetas': {
|
||||
'high': 'Trombocitosis. Riesgo trombótico. Descartar causa primaria vs reactiva.',
|
||||
'low': 'Trombocitopenia severa. Riesgo de sangrado. Evaluar PTI, hiperesplenismo o supresión medular.'
|
||||
},
|
||||
'neutrofilos': {
|
||||
'high': 'Neutrofilia. Sugiere infección bacteriana o proceso inflamatorio agudo.',
|
||||
'low': 'Neutropenia. Alto riesgo de infección bacteriana. Evaluar urgentemente.'
|
||||
},
|
||||
'linfocitos': {
|
||||
'high': 'Linfocitosis. Considerar infección viral o proceso linfoproliferativo.',
|
||||
'low': 'Linfopenia. Evaluar inmunodeficiencia o efecto de corticoides.'
|
||||
},
|
||||
'colesterol total': {
|
||||
'high': 'Hipercolesterolemia. Riesgo cardiovascular elevado. Iniciar medidas dietéticas y evaluar tratamiento con estatinas.',
|
||||
'low': 'Hipocolesterolemia. Evaluar malnutrición, hipertiroidismo o enfermedad hepática.'
|
||||
},
|
||||
'trigliceridos': {
|
||||
'high': 'Hipertrigliceridemia severa. Riesgo de pancreatitis aguda. Considerar tratamiento farmacológico urgente.',
|
||||
'low': 'Valor bajo, generalmente sin significado patológico.'
|
||||
},
|
||||
'hdl': {
|
||||
'high': 'HDL elevado, factor protector cardiovascular.',
|
||||
'low': 'HDL bajo. Factor de riesgo cardiovascular. Recomendar ejercicio y cambios en estilo de vida.'
|
||||
},
|
||||
'ldl': {
|
||||
'high': 'LDL elevado. Alto riesgo aterogénico. Evaluar inicio de estatinas según riesgo global.',
|
||||
'low': 'LDL bajo, generalmente favorable.'
|
||||
},
|
||||
'glucosa en sangre': {
|
||||
'high': 'Hiperglucemia. Si en ayunas >126 mg/dL sugiere diabetes. Confirmar con segunda muestra.',
|
||||
'low': 'Hipoglucemia. Evaluar síntomas y causas. Riesgo neurológico si <50 mg/dL.'
|
||||
}
|
||||
}
|
||||
|
||||
# Solo autocompletar si no hay notas previas o están vacías
|
||||
if not self.notes or self.notes.strip() == '':
|
||||
note = self._get_critical_note(CRITICAL_NOTES)
|
||||
if note:
|
||||
self.notes = note
|
||||
|
||||
def _get_critical_note(self, critical_notes_dict):
|
||||
"""Obtiene la nota apropiada para un resultado crítico."""
|
||||
if not self.parameter_id or not self.parameter_name:
|
||||
return False
|
||||
|
||||
param_lower = self.parameter_name.lower()
|
||||
|
||||
# Buscar el parámetro en el diccionario
|
||||
for key in critical_notes_dict:
|
||||
if key in param_lower:
|
||||
# Obtener rangos del rango aplicable si existe
|
||||
normal_min = normal_max = None
|
||||
if self.applicable_range_id:
|
||||
normal_min = self.applicable_range_id.normal_min
|
||||
normal_max = self.applicable_range_id.normal_max
|
||||
|
||||
if normal_max and self.value_numeric > normal_max:
|
||||
return critical_notes_dict[key].get('high', f'Valor crítico alto para {self.parameter_name}. Requiere evaluación médica inmediata.')
|
||||
elif normal_min and self.value_numeric < normal_min:
|
||||
return critical_notes_dict[key].get('low', f'Valor crítico bajo para {self.parameter_name}. Requiere evaluación médica inmediata.')
|
||||
|
||||
# Nota genérica si no se encuentra el parámetro
|
||||
if self.applicable_range_id:
|
||||
normal_min = self.applicable_range_id.normal_min
|
||||
normal_max = self.applicable_range_id.normal_max
|
||||
|
||||
if normal_max and self.value_numeric > normal_max:
|
||||
return f'Valor significativamente elevado. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
|
||||
elif normal_min and self.value_numeric < normal_min:
|
||||
return f'Valor significativamente bajo. Rango normal: {normal_min}-{normal_max}. Se recomienda evaluación médica.'
|
||||
|
||||
return 'Valor fuera de rango normal. Requiere interpretación clínica.'
|
||||
|
||||
def _validate_and_autocomplete_selection(self, value):
|
||||
"""Valida y autocompleta el valor de selección.
|
||||
|
||||
Esta función es llamada antes de guardar para asegurar que el valor
|
||||
sea válido y esté completo.
|
||||
"""
|
||||
if not value or not self.parameter_id or self.parameter_value_type != 'selection':
|
||||
return value
|
||||
|
||||
options = self.parameter_id.get_selection_list()
|
||||
if not options:
|
||||
return value
|
||||
|
||||
# Convertir a mayúsculas para comparación
|
||||
value_upper = value.upper().strip()
|
||||
|
||||
# Buscar coincidencias exactas primero
|
||||
for option in options:
|
||||
if option.upper() == value_upper:
|
||||
return option
|
||||
|
||||
# Buscar coincidencias que empiecen con el valor
|
||||
matches = []
|
||||
for option in options:
|
||||
if option.upper().startswith(value_upper):
|
||||
matches.append(option)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
elif len(matches) > 1:
|
||||
# Si hay múltiples coincidencias, intentar ser más específico
|
||||
# Preferir la coincidencia más corta
|
||||
shortest = min(matches, key=len)
|
||||
return shortest
|
||||
|
||||
# Si no hay coincidencias por inicio, buscar contenido
|
||||
for option in options:
|
||||
if value_upper in option.upper():
|
||||
matches.append(option)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
elif len(matches) > 1:
|
||||
# Retornar la primera coincidencia
|
||||
return matches[0]
|
||||
|
||||
# Si no hay ninguna coincidencia, retornar el valor original
|
||||
# La validación en @api.constrains se encargará de rechazarlo
|
||||
return value
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""Override create para autocompletar valores de selección."""
|
||||
if 'value_selection' in vals and vals.get('value_selection'):
|
||||
# Necesitamos el parameter_id para validar
|
||||
if 'parameter_id' in vals:
|
||||
parameter = self.env['lims.analysis.parameter'].browse(vals['parameter_id'])
|
||||
if parameter.value_type == 'selection':
|
||||
# Crear un registro temporal para usar el método
|
||||
temp_record = self.new({'parameter_id': parameter.id, 'parameter_value_type': 'selection'})
|
||||
vals['value_selection'] = temp_record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||
return super(LimsResult, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para autocompletar valores de selección."""
|
||||
if 'value_selection' in vals and vals.get('value_selection'):
|
||||
for record in self:
|
||||
if record.parameter_value_type == 'selection':
|
||||
vals['value_selection'] = record._validate_and_autocomplete_selection(vals['value_selection'])
|
||||
break # Solo necesitamos procesar una vez
|
||||
return super(LimsResult, self).write(vals)
|
|
@ -1,533 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LimsTest(models.Model):
|
||||
_name = 'lims.test'
|
||||
_description = 'Prueba de Laboratorio'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_rec_name = 'name'
|
||||
_order = 'create_date desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Código de Prueba',
|
||||
required=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default='Nuevo'
|
||||
)
|
||||
|
||||
sale_order_line_id = fields.Many2one(
|
||||
'sale.order.line',
|
||||
string='Línea de Orden',
|
||||
required=True,
|
||||
ondelete='restrict'
|
||||
)
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Orden de Venta',
|
||||
related='sale_order_line_id.order_id',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
patient_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Paciente',
|
||||
related='sale_order_line_id.order_id.partner_id',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Análisis',
|
||||
related='sale_order_line_id.product_id',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra',
|
||||
domain="[('is_lab_sample', '=', True), ('patient_id', '=', patient_id), ('state', 'in', ['collected', 'in_analysis'])]",
|
||||
tracking=True
|
||||
)
|
||||
|
||||
sample_state = fields.Selection(
|
||||
related='sample_id.state',
|
||||
string='Estado de Muestra',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Borrador'),
|
||||
('in_process', 'En Proceso'),
|
||||
('result_entered', 'Resultado Ingresado'),
|
||||
('validated', 'Validado'),
|
||||
('cancelled', 'Cancelado')
|
||||
], string='Estado', default='draft', tracking=True)
|
||||
|
||||
validator_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Validador',
|
||||
readonly=True,
|
||||
tracking=True
|
||||
)
|
||||
|
||||
validation_date = fields.Datetime(
|
||||
string='Fecha de Validación',
|
||||
readonly=True,
|
||||
tracking=True
|
||||
)
|
||||
|
||||
technician_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Técnico',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True
|
||||
)
|
||||
|
||||
require_validation = fields.Boolean(
|
||||
string='Requiere Validación',
|
||||
compute='_compute_require_validation',
|
||||
store=True
|
||||
)
|
||||
|
||||
result_ids = fields.One2many(
|
||||
'lims.result',
|
||||
'test_id',
|
||||
string='Resultados'
|
||||
)
|
||||
|
||||
notes = fields.Text(
|
||||
string='Observaciones'
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Compañía',
|
||||
required=True,
|
||||
default=lambda self: self.env.company
|
||||
)
|
||||
|
||||
# Campos para dashboards demográficos
|
||||
patient_gender = fields.Selection(
|
||||
related='patient_id.gender',
|
||||
string='Género del Paciente',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
patient_age_range = fields.Selection(
|
||||
related='patient_id.age_range',
|
||||
string='Rango de Edad',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_require_validation(self):
|
||||
"""Calcula si la prueba requiere validación basado en configuración."""
|
||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||
require_validation = IrConfig.get_param('lims_management.require_validation', 'True')
|
||||
for record in self:
|
||||
record.require_validation = require_validation == 'True'
|
||||
|
||||
@api.onchange('sale_order_line_id')
|
||||
def _onchange_sale_order_line(self):
|
||||
"""Update sample domain when order line changes"""
|
||||
if self.sale_order_line_id:
|
||||
# Try to find a suitable sample from the order
|
||||
order = self.sale_order_line_id.order_id
|
||||
product = self.sale_order_line_id.product_id
|
||||
|
||||
if order.is_lab_request and product.required_sample_type_id:
|
||||
# Find samples for this patient with the required sample type
|
||||
suitable_samples = self.env['stock.lot'].search([
|
||||
('is_lab_sample', '=', True),
|
||||
('patient_id', '=', order.partner_id.id),
|
||||
('sample_type_product_id', '=', product.required_sample_type_id.id),
|
||||
('state', 'in', ['collected', 'in_analysis'])
|
||||
])
|
||||
|
||||
if suitable_samples:
|
||||
# If only one sample, select it automatically
|
||||
if len(suitable_samples) == 1:
|
||||
self.sample_id = suitable_samples[0]
|
||||
# Update domain to show only suitable samples
|
||||
return {
|
||||
'domain': {
|
||||
'sample_id': [
|
||||
('id', 'in', suitable_samples.ids)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _generate_test_results(self):
|
||||
"""Genera automáticamente las líneas de resultado basadas en los parámetros configurados del análisis."""
|
||||
for test in self:
|
||||
if test.result_ids:
|
||||
# Si ya tiene resultados, no generar nuevos
|
||||
continue
|
||||
|
||||
# Obtener el product.template del análisis
|
||||
product_tmpl = test.product_id.product_tmpl_id
|
||||
|
||||
# Buscar los parámetros configurados para este análisis
|
||||
template_parameters = self.env['product.template.parameter'].search([
|
||||
('product_tmpl_id', '=', product_tmpl.id)
|
||||
], order='sequence, id')
|
||||
|
||||
# Crear una línea de resultado por cada parámetro
|
||||
for param_config in template_parameters:
|
||||
# Preparar las notas/instrucciones
|
||||
notes = param_config.instructions or ''
|
||||
|
||||
# Si es un parámetro de tipo selection, agregar instrucciones de autocompletado
|
||||
if param_config.parameter_value_type == 'selection':
|
||||
selection_values = param_config.parameter_id.selection_values
|
||||
if selection_values:
|
||||
options = [v.strip() for v in selection_values.split(',')]
|
||||
if options:
|
||||
# Generar instrucciones automáticas
|
||||
auto_instructions = "Opciones: " + ", ".join(options) + ". "
|
||||
auto_instructions += "Puede escribir las iniciales o parte del texto. "
|
||||
|
||||
# Agregar ejemplos específicos
|
||||
examples = []
|
||||
for opt in options[:3]: # Mostrar ejemplos para las primeras 3 opciones
|
||||
if opt:
|
||||
initial = opt[0].upper()
|
||||
examples.append(f"{initial}={opt}")
|
||||
|
||||
if examples:
|
||||
auto_instructions += "Ej: " + ", ".join(examples)
|
||||
|
||||
# Combinar con instrucciones existentes
|
||||
if notes:
|
||||
notes = auto_instructions + "\n" + notes
|
||||
else:
|
||||
notes = auto_instructions
|
||||
|
||||
result_vals = {
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_config.parameter_id.id,
|
||||
'sequence': param_config.sequence,
|
||||
'notes': notes
|
||||
}
|
||||
|
||||
# Inicializar valores según el tipo
|
||||
if param_config.parameter_value_type == 'boolean':
|
||||
result_vals['value_boolean'] = False
|
||||
|
||||
self.env['lims.result'].create(result_vals)
|
||||
|
||||
if template_parameters:
|
||||
_logger.info(f"Generados {len(template_parameters)} resultados para la prueba {test.name}")
|
||||
else:
|
||||
_logger.warning(f"No se encontraron parámetros configurados para el análisis {product_tmpl.name}")
|
||||
|
||||
def action_start_process(self):
|
||||
"""Inicia el proceso de análisis."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para iniciar el proceso de análisis. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'draft':
|
||||
raise UserError(_('Solo se pueden procesar pruebas en estado borrador.'))
|
||||
if not self.sample_id:
|
||||
raise UserError(_('Debe asignar una muestra antes de iniciar el proceso.'))
|
||||
|
||||
self.write({
|
||||
'state': 'in_process',
|
||||
'technician_id': self.env.user.id
|
||||
})
|
||||
|
||||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Prueba iniciada por %s') % self.env.user.name,
|
||||
subject=_('Proceso Iniciado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Actualizar estado de la muestra si es necesario
|
||||
if self.sample_id and self.sample_id.state == 'collected':
|
||||
self.sample_id.write({'state': 'in_process'})
|
||||
self.sample_id.message_post(
|
||||
body=_('Muestra en análisis para la prueba %s') % self.name,
|
||||
subject=_('Estado actualizado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_enter_results(self):
|
||||
"""Marca como resultados ingresados."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para ingresar resultados. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'in_process':
|
||||
raise UserError(_('Solo se pueden ingresar resultados en pruebas en proceso.'))
|
||||
|
||||
if not self.result_ids:
|
||||
raise UserError(_('Debe ingresar al menos un resultado.'))
|
||||
|
||||
# Verificar que todos los resultados tengan valores ingresados
|
||||
empty_results = self.result_ids.filtered(
|
||||
lambda r: not r.value_text and not r.value_numeric and not r.value_selection and not r.value_boolean and r.parameter_id.value_type != 'boolean'
|
||||
)
|
||||
if empty_results:
|
||||
params = ', '.join(empty_results.mapped('parameter_id.name'))
|
||||
raise UserError(_('Los siguientes parámetros no tienen resultados ingresados: %s') % params)
|
||||
|
||||
# Si no requiere validación, pasar directamente a validado
|
||||
if not self.require_validation:
|
||||
self.write({
|
||||
'state': 'validated',
|
||||
'validator_id': self.env.user.id,
|
||||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados y auto-validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados'),
|
||||
message_type='notification'
|
||||
)
|
||||
else:
|
||||
self.state = 'result_entered'
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Ingresados'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_validate(self):
|
||||
"""Valida los resultados (solo administradores)."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo administradores
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No tiene permisos para validar resultados. Solo administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state != 'result_entered':
|
||||
raise UserError(_('Solo se pueden validar pruebas con resultados ingresados.'))
|
||||
|
||||
# Verificar que todos los resultados críticos tengan observaciones si están fuera de rango
|
||||
critical_results = []
|
||||
for result in self.result_ids:
|
||||
if result.is_critical: # Usar el campo is_critical del resultado, no del parámetro
|
||||
if not result.notes:
|
||||
critical_results.append(result.parameter_id.name)
|
||||
|
||||
if critical_results:
|
||||
raise UserError(_('Los siguientes parámetros críticos están fuera de rango y requieren observaciones: %s') % ', '.join(critical_results))
|
||||
|
||||
self.write({
|
||||
'state': 'validated',
|
||||
'validator_id': self.env.user.id,
|
||||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Log en el chatter con más detalles
|
||||
out_of_range_count = len(self.result_ids.filtered('is_out_of_range'))
|
||||
body = _('Resultados validados por %s') % self.env.user.name
|
||||
if out_of_range_count:
|
||||
body += _('<br/>%d parámetros fuera de rango') % out_of_range_count
|
||||
|
||||
self.message_post(
|
||||
body=body,
|
||||
subject=_('Resultados Validados'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Actualizar estado de la muestra si todas las pruebas están validadas
|
||||
if self.sample_id:
|
||||
all_tests = self.env['lims.test'].search([
|
||||
('sample_id', '=', self.sample_id.id),
|
||||
('state', '!=', 'cancelled')
|
||||
])
|
||||
if all(test.state == 'validated' for test in all_tests):
|
||||
self.sample_id.write({'state': 'analyzed'})
|
||||
self.sample_id.message_post(
|
||||
body=_('Todas las pruebas de la muestra han sido validadas'),
|
||||
subject=_('Análisis completado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancela la prueba."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: técnicos y administradores pueden cancelar
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para cancelar pruebas. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state == 'validated':
|
||||
# Solo administradores pueden cancelar pruebas validadas
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No se pueden cancelar pruebas validadas. Solo administradores pueden realizar esta acción.'))
|
||||
|
||||
old_state = self.state
|
||||
self.state = 'cancelled'
|
||||
|
||||
# Log en el chatter con el estado anterior
|
||||
self.message_post(
|
||||
body=_('Prueba cancelada por %s (estado anterior: %s)') % (self.env.user.name, dict(self._fields['state'].selection).get(old_state)),
|
||||
subject=_('Prueba Cancelada'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_regenerate_results(self):
|
||||
"""Regenera los resultados basados en la configuración actual del análisis."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo técnicos y administradores
|
||||
if not (self.env.user.has_group('lims_management.group_lims_technician') or
|
||||
self.env.user.has_group('lims_management.group_lims_admin')):
|
||||
raise UserError(_('No tiene permisos para regenerar resultados. Solo técnicos y administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state not in ['draft', 'in_process']:
|
||||
raise UserError(_('Solo se pueden regenerar resultados en pruebas en borrador o en proceso.'))
|
||||
|
||||
# Confirmar con el usuario
|
||||
if self.result_ids:
|
||||
# En producción, aquí se mostraría un wizard de confirmación
|
||||
# Por ahora, eliminamos los resultados existentes
|
||||
self.result_ids.unlink()
|
||||
|
||||
# Regenerar
|
||||
self._generate_test_results()
|
||||
|
||||
self.message_post(
|
||||
body=_('Resultados regenerados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Regenerados'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_draft(self):
|
||||
"""Regresa a borrador."""
|
||||
self.ensure_one()
|
||||
|
||||
# Verificar permisos: solo administradores pueden regresar a borrador
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('No tiene permisos para regresar pruebas a borrador. Solo administradores pueden realizar esta acción.'))
|
||||
|
||||
if self.state not in ['cancelled']:
|
||||
raise UserError(_('Solo se pueden regresar a borrador pruebas canceladas.'))
|
||||
|
||||
self.state = 'draft'
|
||||
|
||||
self.message_post(
|
||||
body=_('Prueba regresada a borrador por %s') % self.env.user.name,
|
||||
subject=_('Estado Restaurado'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@api.constrains('state')
|
||||
def _check_state_transition(self):
|
||||
"""Valida que las transiciones de estado sean válidas"""
|
||||
for record in self:
|
||||
# Definir transiciones válidas
|
||||
valid_transitions = {
|
||||
'draft': ['in_process', 'cancelled'],
|
||||
'in_process': ['result_entered', 'cancelled'],
|
||||
'result_entered': ['validated', 'cancelled'],
|
||||
'validated': ['cancelled'], # Solo admin puede cancelar validados
|
||||
'cancelled': ['draft'] # Solo admin puede regresar a draft
|
||||
}
|
||||
|
||||
# Si es un registro nuevo, no hay transición que validar
|
||||
if not record._origin.id:
|
||||
continue
|
||||
|
||||
old_state = record._origin.state
|
||||
new_state = record.state
|
||||
|
||||
# Si el estado no cambió, no hay nada que validar
|
||||
if old_state == new_state:
|
||||
continue
|
||||
|
||||
# Verificar si la transición es válida
|
||||
if old_state in valid_transitions:
|
||||
if new_state not in valid_transitions[old_state]:
|
||||
raise ValidationError(
|
||||
_('Transición de estado no válida: No se puede cambiar de "%s" a "%s"') %
|
||||
(dict(self._fields['state'].selection).get(old_state),
|
||||
dict(self._fields['state'].selection).get(new_state))
|
||||
)
|
||||
|
||||
@api.constrains('sample_id', 'state')
|
||||
def _check_sample_state(self):
|
||||
"""Valida que la muestra esté en un estado apropiado para la prueba"""
|
||||
for record in self:
|
||||
if record.sample_id and record.state in ['in_process', 'result_entered']:
|
||||
# La muestra debe estar al menos recolectada
|
||||
if record.sample_id.state in ['pending_collection', 'cancelled']:
|
||||
raise ValidationError(
|
||||
_('No se puede procesar una prueba con una muestra en estado "%s"') %
|
||||
dict(record.sample_id._fields['state'].selection).get(record.sample_id.state)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""Override create para validaciones adicionales y generación de secuencia"""
|
||||
# Generar código único si no se proporciona
|
||||
if vals.get('name', 'Nuevo') == 'Nuevo':
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
|
||||
|
||||
# Si se está creando con un estado diferente a draft, verificar permisos
|
||||
if vals.get('state') and vals['state'] != 'draft':
|
||||
if not self.env.user.has_group('lims_management.group_lims_admin'):
|
||||
raise UserError(_('Solo administradores pueden crear pruebas en estado diferente a borrador'))
|
||||
|
||||
test = super().create(vals)
|
||||
# Generar resultados automáticamente
|
||||
test._generate_test_results()
|
||||
return test
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write para auditoría adicional"""
|
||||
# Si se está cambiando el estado, registrar más detalles
|
||||
if 'state' in vals:
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
# El write real se hace en el super()
|
||||
|
||||
result = super().write(vals)
|
||||
|
||||
# Registrar cambios importantes después del write
|
||||
if 'sample_id' in vals:
|
||||
for record in self:
|
||||
if vals.get('sample_id'):
|
||||
sample = self.env['stock.lot'].browse(vals['sample_id'])
|
||||
record.message_post(
|
||||
body=_('Muestra asignada: %s') % sample.name,
|
||||
subject=_('Muestra Asignada'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return result
|
|
@ -1,234 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class LimsParameterRange(models.Model):
|
||||
_name = 'lims.parameter.range'
|
||||
_description = 'Rangos de Referencia por Parámetro'
|
||||
_order = 'parameter_id, gender desc, age_min'
|
||||
_rec_name = 'name'
|
||||
|
||||
parameter_id = fields.Many2one(
|
||||
'lims.analysis.parameter',
|
||||
string='Parámetro',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
help='Parámetro al que aplica este rango de referencia'
|
||||
)
|
||||
|
||||
name = fields.Char(
|
||||
string='Descripción',
|
||||
compute='_compute_name',
|
||||
store=True,
|
||||
help='Descripción automática del rango'
|
||||
)
|
||||
|
||||
# Condiciones
|
||||
gender = fields.Selection([
|
||||
('male', 'Masculino'),
|
||||
('female', 'Femenino'),
|
||||
('both', 'Ambos')
|
||||
],
|
||||
string='Género',
|
||||
default='both',
|
||||
required=True,
|
||||
help='Género al que aplica este rango'
|
||||
)
|
||||
|
||||
age_min = fields.Integer(
|
||||
string='Edad Mínima',
|
||||
default=0,
|
||||
help='Edad mínima en años (inclusive)'
|
||||
)
|
||||
|
||||
age_max = fields.Integer(
|
||||
string='Edad Máxima',
|
||||
default=150,
|
||||
help='Edad máxima en años (inclusive)'
|
||||
)
|
||||
|
||||
pregnant = fields.Boolean(
|
||||
string='Embarazada',
|
||||
default=False,
|
||||
help='Marcar si este rango es específico para mujeres embarazadas'
|
||||
)
|
||||
|
||||
# Valores de referencia
|
||||
normal_min = fields.Float(
|
||||
string='Valor Normal Mínimo',
|
||||
help='Límite inferior del rango normal'
|
||||
)
|
||||
|
||||
normal_max = fields.Float(
|
||||
string='Valor Normal Máximo',
|
||||
help='Límite superior del rango normal'
|
||||
)
|
||||
|
||||
critical_min = fields.Float(
|
||||
string='Valor Crítico Mínimo',
|
||||
help='Por debajo de este valor es crítico'
|
||||
)
|
||||
|
||||
critical_max = fields.Float(
|
||||
string='Valor Crítico Máximo',
|
||||
help='Por encima de este valor es crítico'
|
||||
)
|
||||
|
||||
# Información adicional
|
||||
interpretation = fields.Text(
|
||||
string='Interpretación',
|
||||
help='Guía de interpretación clínica para este rango'
|
||||
)
|
||||
|
||||
# Campos relacionados para facilitar búsquedas
|
||||
parameter_name = fields.Char(
|
||||
related='parameter_id.name',
|
||||
string='Nombre del Parámetro',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_code = fields.Char(
|
||||
related='parameter_id.code',
|
||||
string='Código del Parámetro',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_unit = fields.Char(
|
||||
related='parameter_id.unit',
|
||||
string='Unidad',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
reference_text = fields.Char(
|
||||
string='Texto de Referencia',
|
||||
compute='_compute_reference_text',
|
||||
store=False,
|
||||
help='Texto formateado del rango de referencia'
|
||||
)
|
||||
|
||||
@api.depends('normal_min', 'normal_max', 'parameter_unit')
|
||||
def _compute_reference_text(self):
|
||||
"""Computa el texto de referencia basado en los valores min/max y unidad"""
|
||||
for record in self:
|
||||
if record.normal_min is not False and record.normal_max is not False:
|
||||
unit = record.parameter_unit or ''
|
||||
# Formatear los números para evitar decimales innecesarios
|
||||
min_val = f"{record.normal_min:.2f}".rstrip('0').rstrip('.')
|
||||
max_val = f"{record.normal_max:.2f}".rstrip('0').rstrip('.')
|
||||
record.reference_text = f"{min_val} - {max_val} {unit}".strip()
|
||||
else:
|
||||
record.reference_text = "N/A"
|
||||
|
||||
@api.depends('parameter_id', 'gender', 'age_min', 'age_max', 'pregnant')
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
if not record.parameter_id:
|
||||
record.name = 'Nuevo rango'
|
||||
continue
|
||||
|
||||
parts = [record.parameter_id.name]
|
||||
|
||||
# Agregar género si no es ambos
|
||||
if record.gender != 'both':
|
||||
gender_name = dict(self._fields['gender'].selection).get(record.gender, '')
|
||||
parts.append(gender_name)
|
||||
|
||||
# Agregar rango de edad
|
||||
if record.age_min == 0 and record.age_max == 150:
|
||||
parts.append('Todas las edades')
|
||||
else:
|
||||
parts.append(f"{record.age_min}-{record.age_max} años")
|
||||
|
||||
# Agregar indicador de embarazo
|
||||
if record.pregnant:
|
||||
parts.append('Embarazada')
|
||||
|
||||
record.name = ' - '.join(parts)
|
||||
|
||||
@api.constrains('age_min', 'age_max')
|
||||
def _check_age_range(self):
|
||||
for record in self:
|
||||
if record.age_min < 0:
|
||||
raise ValidationError('La edad mínima no puede ser negativa.')
|
||||
if record.age_max < record.age_min:
|
||||
raise ValidationError('La edad máxima debe ser mayor o igual a la edad mínima.')
|
||||
if record.age_max > 150:
|
||||
raise ValidationError('La edad máxima no puede ser mayor a 150 años.')
|
||||
|
||||
@api.constrains('normal_min', 'normal_max')
|
||||
def _check_normal_range(self):
|
||||
for record in self:
|
||||
if record.normal_min and record.normal_max and record.normal_min > record.normal_max:
|
||||
raise ValidationError('El valor normal mínimo debe ser menor o igual al valor normal máximo.')
|
||||
|
||||
@api.constrains('critical_min', 'critical_max', 'normal_min', 'normal_max')
|
||||
def _check_critical_range(self):
|
||||
for record in self:
|
||||
# Validar que crítico mínimo sea menor que normal mínimo
|
||||
if record.critical_min and record.normal_min and record.critical_min > record.normal_min:
|
||||
raise ValidationError('El valor crítico mínimo debe ser menor o igual al valor normal mínimo.')
|
||||
|
||||
# Validar que crítico máximo sea mayor que normal máximo
|
||||
if record.critical_max and record.normal_max and record.critical_max < record.normal_max:
|
||||
raise ValidationError('El valor crítico máximo debe ser mayor o igual al valor normal máximo.')
|
||||
|
||||
@api.constrains('gender', 'pregnant')
|
||||
def _check_pregnant_gender(self):
|
||||
for record in self:
|
||||
if record.pregnant and record.gender == 'male':
|
||||
raise ValidationError('No se puede marcar "Embarazada" para rangos masculinos.')
|
||||
|
||||
@api.constrains('parameter_id', 'gender', 'age_min', 'age_max', 'pregnant')
|
||||
def _check_unique_range(self):
|
||||
for record in self:
|
||||
# Buscar rangos duplicados
|
||||
domain = [
|
||||
('parameter_id', '=', record.parameter_id.id),
|
||||
('gender', '=', record.gender),
|
||||
('age_min', '=', record.age_min),
|
||||
('age_max', '=', record.age_max),
|
||||
('pregnant', '=', record.pregnant),
|
||||
('id', '!=', record.id)
|
||||
]
|
||||
|
||||
if self.search_count(domain) > 0:
|
||||
raise ValidationError('Ya existe un rango con estas mismas condiciones para este parámetro.')
|
||||
|
||||
def is_value_normal(self, value):
|
||||
"""Verifica si un valor está dentro del rango normal"""
|
||||
self.ensure_one()
|
||||
if not value or not self.normal_min or not self.normal_max:
|
||||
return True
|
||||
return self.normal_min <= value <= self.normal_max
|
||||
|
||||
def is_value_critical(self, value):
|
||||
"""Verifica si un valor está en rango crítico"""
|
||||
self.ensure_one()
|
||||
if not value:
|
||||
return False
|
||||
|
||||
# Crítico por debajo
|
||||
if self.critical_min and value < self.critical_min:
|
||||
return True
|
||||
|
||||
# Crítico por encima
|
||||
if self.critical_max and value > self.critical_max:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_value_status(self, value):
|
||||
"""Devuelve el estado del valor: 'normal', 'abnormal', 'critical'"""
|
||||
self.ensure_one()
|
||||
if not value:
|
||||
return 'normal'
|
||||
|
||||
if self.is_value_critical(value):
|
||||
return 'critical'
|
||||
elif not self.is_value_normal(value):
|
||||
return 'abnormal'
|
||||
else:
|
||||
return 'normal'
|
|
@ -1,8 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
@ -20,30 +17,6 @@ class ResPartner(models.Model):
|
|||
('female', 'Femenino'),
|
||||
('other', 'Otro')
|
||||
], string="Género")
|
||||
|
||||
# Nuevos campos para el cálculo de rangos
|
||||
age = fields.Integer(
|
||||
string="Edad",
|
||||
compute='_compute_age',
|
||||
store=False,
|
||||
help="Edad calculada en años basada en la fecha de nacimiento"
|
||||
)
|
||||
|
||||
age_range = fields.Selection([
|
||||
('0-10', '0-10 años'),
|
||||
('11-20', '11-20 años'),
|
||||
('21-30', '21-30 años'),
|
||||
('31-40', '31-40 años'),
|
||||
('41-50', '41-50 años'),
|
||||
('51-60', '51-60 años'),
|
||||
('61-70', '61-70 años'),
|
||||
('71+', 'Más de 70 años')
|
||||
], string="Rango de Edad", compute='_compute_age_range', store=True)
|
||||
|
||||
is_pregnant = fields.Boolean(
|
||||
string="Embarazada",
|
||||
help="Marcar si la paciente está embarazada (solo aplica para género femenino)"
|
||||
)
|
||||
|
||||
is_doctor = fields.Boolean(string="Es Médico")
|
||||
doctor_license = fields.Char(string="Licencia Médica", copy=False)
|
||||
|
@ -52,53 +25,6 @@ class ResPartner(models.Model):
|
|||
('patient_identifier_unique', 'unique(patient_identifier)', 'El identificador del paciente debe ser único.'),
|
||||
('doctor_license_unique', 'unique(doctor_license)', 'La licencia médica debe ser única.')
|
||||
]
|
||||
|
||||
@api.depends('birthdate_date')
|
||||
def _compute_age(self):
|
||||
"""Calcula la edad en años basada en la fecha de nacimiento"""
|
||||
today = date.today()
|
||||
for partner in self:
|
||||
if partner.birthdate_date:
|
||||
# Calcular diferencia usando relativedelta para precisión
|
||||
delta = relativedelta(today, partner.birthdate_date)
|
||||
partner.age = delta.years
|
||||
else:
|
||||
partner.age = 0
|
||||
|
||||
@api.depends('birthdate_date')
|
||||
def _compute_age_range(self):
|
||||
"""Calcula el rango de edad basado en la edad"""
|
||||
for partner in self:
|
||||
if partner.birthdate_date:
|
||||
today = date.today()
|
||||
delta = relativedelta(today, partner.birthdate_date)
|
||||
age = delta.years
|
||||
|
||||
if age <= 10:
|
||||
partner.age_range = '0-10'
|
||||
elif age <= 20:
|
||||
partner.age_range = '11-20'
|
||||
elif age <= 30:
|
||||
partner.age_range = '21-30'
|
||||
elif age <= 40:
|
||||
partner.age_range = '31-40'
|
||||
elif age <= 50:
|
||||
partner.age_range = '41-50'
|
||||
elif age <= 60:
|
||||
partner.age_range = '51-60'
|
||||
elif age <= 70:
|
||||
partner.age_range = '61-70'
|
||||
else:
|
||||
partner.age_range = '71+'
|
||||
else:
|
||||
partner.age_range = False
|
||||
|
||||
@api.constrains('is_pregnant', 'gender')
|
||||
def _check_pregnant_gender(self):
|
||||
"""Valida que solo pacientes de género femenino puedan estar embarazadas"""
|
||||
for partner in self:
|
||||
if partner.is_pregnant and partner.gender != 'female':
|
||||
raise ValidationError('Solo las pacientes de género femenino pueden estar marcadas como embarazadas.')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
@ -106,25 +32,3 @@ class ResPartner(models.Model):
|
|||
if vals.get('is_patient') and not vals.get('patient_identifier'):
|
||||
vals['patient_identifier'] = self.env['ir.sequence'].next_by_code('res.partner.patient_identifier')
|
||||
return super(ResPartner, self).create(vals_list)
|
||||
|
||||
def get_age_at_date(self, target_date=None):
|
||||
"""
|
||||
Calcula la edad del paciente en una fecha específica.
|
||||
|
||||
:param target_date: Fecha en la que calcular la edad. Si es None, usa la fecha actual.
|
||||
:return: Edad en años
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.birthdate_date:
|
||||
return 0
|
||||
|
||||
if not target_date:
|
||||
target_date = date.today()
|
||||
elif isinstance(target_date, str):
|
||||
target_date = fields.Date.from_string(target_date)
|
||||
|
||||
if target_date < self.birthdate_date:
|
||||
return 0
|
||||
|
||||
delta = relativedelta(target_date, self.birthdate_date)
|
||||
return delta.years
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import models, fields
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
@ -22,38 +21,8 @@ class ProductTemplate(models.Model):
|
|||
string="Especificaciones Técnicas"
|
||||
)
|
||||
|
||||
parameter_ids = fields.One2many(
|
||||
'product.template.parameter',
|
||||
'product_tmpl_id',
|
||||
string="Parámetros del Análisis",
|
||||
help="Parámetros que se medirán en este análisis"
|
||||
value_range_ids = fields.One2many(
|
||||
'lims.analysis.range',
|
||||
'analysis_id',
|
||||
string="Rangos de Referencia"
|
||||
)
|
||||
|
||||
is_sample_type = fields.Boolean(
|
||||
string="Es Tipo de Muestra",
|
||||
help="Marcar si este producto representa un tipo de contenedor de muestra de laboratorio."
|
||||
)
|
||||
|
||||
required_sample_type_id = fields.Many2one(
|
||||
'product.template',
|
||||
string='Tipo de Muestra Requerida',
|
||||
domain="[('is_sample_type', '=', True)]",
|
||||
help="Tipo de muestra/contenedor requerido para realizar este análisis"
|
||||
)
|
||||
|
||||
sample_volume_ml = fields.Float(
|
||||
string='Volumen Requerido (ml)',
|
||||
help="Volumen de muestra requerido en mililitros para realizar este análisis"
|
||||
)
|
||||
|
||||
@api.constrains('required_sample_type_id', 'is_analysis')
|
||||
def _check_sample_type_for_analysis(self):
|
||||
for product in self:
|
||||
if product.required_sample_type_id and not product.is_analysis:
|
||||
raise ValidationError("Solo los productos marcados como 'Es un Análisis Clínico' pueden tener un tipo de muestra requerida.")
|
||||
|
||||
@api.constrains('sample_volume_ml', 'is_analysis')
|
||||
def _check_volume_for_analysis(self):
|
||||
for product in self:
|
||||
if product.sample_volume_ml and not product.is_analysis:
|
||||
raise ValidationError("Solo los productos marcados como 'Es un Análisis Clínico' pueden tener un volumen requerido.")
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ProductTemplateParameter(models.Model):
|
||||
_name = 'product.template.parameter'
|
||||
_description = 'Parámetros por Análisis'
|
||||
_order = 'product_tmpl_id, sequence, id'
|
||||
_rec_name = 'parameter_id'
|
||||
|
||||
product_tmpl_id = fields.Many2one(
|
||||
'product.template',
|
||||
string='Análisis',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
domain=[('is_analysis', '=', True)],
|
||||
help='Análisis al que pertenece este parámetro'
|
||||
)
|
||||
|
||||
parameter_id = fields.Many2one(
|
||||
'lims.analysis.parameter',
|
||||
string='Parámetro',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
help='Parámetro de laboratorio'
|
||||
)
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='Secuencia',
|
||||
default=10,
|
||||
help='Orden en que aparecerá el parámetro en los resultados'
|
||||
)
|
||||
|
||||
required = fields.Boolean(
|
||||
string='Obligatorio',
|
||||
default=True,
|
||||
help='Si está marcado, este parámetro debe tener un valor en los resultados'
|
||||
)
|
||||
|
||||
instructions = fields.Text(
|
||||
string='Instrucciones específicas',
|
||||
help='Instrucciones especiales para este parámetro en este análisis'
|
||||
)
|
||||
|
||||
# Campos relacionados para facilitar búsquedas y vistas
|
||||
parameter_name = fields.Char(
|
||||
related='parameter_id.name',
|
||||
string='Nombre del Parámetro',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_code = fields.Char(
|
||||
related='parameter_id.code',
|
||||
string='Código',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_value_type = fields.Selection(
|
||||
related='parameter_id.value_type',
|
||||
string='Tipo de Valor',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
parameter_unit = fields.Char(
|
||||
related='parameter_id.unit',
|
||||
string='Unidad',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_param_per_analysis',
|
||||
'UNIQUE(product_tmpl_id, parameter_id)',
|
||||
'El parámetro ya está configurado para este análisis. Cada parámetro solo puede aparecer una vez por análisis.')
|
||||
]
|
||||
|
||||
@api.constrains('sequence')
|
||||
def _check_sequence(self):
|
||||
for record in self:
|
||||
if record.sequence < 0:
|
||||
raise ValidationError('La secuencia debe ser un número positivo.')
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = f"{record.product_tmpl_id.name} - [{record.parameter_code}] {record.parameter_name}"
|
||||
if record.parameter_unit:
|
||||
name += f" ({record.parameter_unit})"
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# Si no se especifica secuencia, asignar la siguiente disponible
|
||||
if 'sequence' not in vals and 'product_tmpl_id' in vals:
|
||||
max_sequence = self.search([
|
||||
('product_tmpl_id', '=', vals['product_tmpl_id'])
|
||||
], order='sequence desc', limit=1).sequence
|
||||
vals['sequence'] = (max_sequence or 0) + 10
|
||||
return super(ProductTemplateParameter, self).create(vals)
|
||||
|
||||
def copy_data(self, default=None):
|
||||
default = dict(default or {})
|
||||
# Al duplicar, incrementar la secuencia
|
||||
default['sequence'] = self.sequence + 10
|
||||
return super(ProductTemplateParameter, self).copy_data(default)
|
|
@ -1,61 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
class LimsRejectionReason(models.Model):
|
||||
_name = 'lims.rejection.reason'
|
||||
_description = 'Motivo de Rechazo de Muestra'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Motivo',
|
||||
required=True
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Código',
|
||||
required=True,
|
||||
help="Código único para identificar el motivo"
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Descripción',
|
||||
help="Descripción detallada del motivo de rechazo"
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Activo',
|
||||
default=True
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Secuencia',
|
||||
default=10,
|
||||
help="Orden de aparición en las listas"
|
||||
)
|
||||
requires_new_sample = fields.Boolean(
|
||||
string='Requiere Nueva Muestra',
|
||||
default=True,
|
||||
help="Indica si este tipo de rechazo requiere solicitar una nueva muestra"
|
||||
)
|
||||
severity = fields.Selection([
|
||||
('low', 'Baja'),
|
||||
('medium', 'Media'),
|
||||
('high', 'Alta'),
|
||||
('critical', 'Crítica')
|
||||
], string='Severidad', default='medium',
|
||||
help="Severidad del problema que causa el rechazo")
|
||||
|
||||
# Statistics
|
||||
rejection_count = fields.Integer(
|
||||
string='Cantidad de Rechazos',
|
||||
compute='_compute_rejection_count',
|
||||
help="Número de muestras rechazadas con este motivo"
|
||||
)
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_rejection_count(self):
|
||||
for record in self:
|
||||
record.rejection_count = self.env['stock.lot'].search_count([
|
||||
('rejection_reason_id', '=', record.id),
|
||||
('state', '=', 'rejected')
|
||||
])
|
||||
|
||||
_sql_constraints = [
|
||||
('code_uniq', 'unique (code)', 'El código del motivo de rechazo debe ser único!'),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
lims_require_validation = fields.Boolean(
|
||||
string='Requerir Validación de Resultados',
|
||||
help='Si está activado, los resultados de las pruebas deben ser validados por un administrador antes de considerarse finales.',
|
||||
config_parameter='lims_management.require_validation',
|
||||
default=True
|
||||
)
|
||||
|
||||
lims_auto_generate_tests = fields.Boolean(
|
||||
string='Generar Pruebas Automáticamente',
|
||||
help='Si está activado, se generarán automáticamente registros de pruebas (lims.test) cuando se confirme una orden de laboratorio.',
|
||||
config_parameter='lims_management.auto_generate_tests',
|
||||
default=False
|
||||
)
|
|
@ -1,394 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
is_lab_request = fields.Boolean(
|
||||
string="Es Orden de Laboratorio",
|
||||
default=False,
|
||||
copy=False,
|
||||
help="Campo técnico para identificar si la orden de venta es una solicitud de laboratorio."
|
||||
)
|
||||
|
||||
doctor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Médico Referente",
|
||||
domain="[('is_doctor', '=', True)]",
|
||||
help="El médico que refirió al paciente para esta solicitud de laboratorio."
|
||||
)
|
||||
|
||||
generated_sample_ids = fields.Many2many(
|
||||
'stock.lot',
|
||||
'sale_order_stock_lot_rel',
|
||||
'order_id',
|
||||
'lot_id',
|
||||
string='Muestras Generadas',
|
||||
domain="[('is_lab_sample', '=', True)]",
|
||||
readonly=True,
|
||||
help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden"
|
||||
)
|
||||
|
||||
all_sample_ids = fields.Many2many(
|
||||
'stock.lot',
|
||||
string='Todas las Muestras (inc. Re-muestras)',
|
||||
compute='_compute_all_samples',
|
||||
help="Todas las muestras relacionadas con esta orden, incluyendo re-muestras"
|
||||
)
|
||||
|
||||
@api.depends('generated_sample_ids', 'generated_sample_ids.child_sample_ids')
|
||||
def _compute_all_samples(self):
|
||||
"""Compute all samples including resamples"""
|
||||
for order in self:
|
||||
all_samples = order.generated_sample_ids
|
||||
# Add all resamples recursively
|
||||
resamples = self.env['stock.lot']
|
||||
for sample in order.generated_sample_ids:
|
||||
resamples |= self._get_all_resamples(sample)
|
||||
order.all_sample_ids = all_samples | resamples
|
||||
|
||||
def _get_all_resamples(self, sample):
|
||||
"""Recursively get all resamples of a sample"""
|
||||
resamples = sample.child_sample_ids
|
||||
for resample in sample.child_sample_ids:
|
||||
resamples |= self._get_all_resamples(resample)
|
||||
return resamples
|
||||
|
||||
def action_confirm(self):
|
||||
"""Override to generate laboratory samples and tests automatically"""
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
|
||||
# Generate samples and tests only for laboratory requests
|
||||
for order in self.filtered('is_lab_request'):
|
||||
try:
|
||||
order._generate_lab_samples()
|
||||
order._generate_lab_tests()
|
||||
except Exception as e:
|
||||
_logger.error(f"Error generating samples/tests for order {order.name}: {str(e)}")
|
||||
# Continue with order confirmation even if generation fails
|
||||
# But notify the user
|
||||
order.message_post(
|
||||
body=_("Error al generar muestras/pruebas automáticamente: %s. "
|
||||
"Por favor, genere las muestras y pruebas manualmente.") % str(e),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def _generate_lab_samples(self):
|
||||
"""Generate laboratory samples based on the analyses in the order"""
|
||||
self.ensure_one()
|
||||
_logger.info(f"Generating laboratory samples for order {self.name}")
|
||||
|
||||
# Group analyses by sample type
|
||||
sample_groups = self._group_analyses_by_sample_type()
|
||||
|
||||
if not sample_groups:
|
||||
_logger.warning(f"No analyses with sample types found in order {self.name}")
|
||||
return
|
||||
|
||||
# Create samples for each group
|
||||
created_samples = self.env['stock.lot']
|
||||
|
||||
for sample_type_id, group_data in sample_groups.items():
|
||||
sample = self._create_sample_for_group(group_data)
|
||||
if sample:
|
||||
created_samples |= sample
|
||||
|
||||
# Link created samples to the order
|
||||
if created_samples:
|
||||
self.generated_sample_ids = [(6, 0, created_samples.ids)]
|
||||
_logger.info(f"Created {len(created_samples)} samples for order {self.name}")
|
||||
|
||||
# Post message with created samples
|
||||
sample_list = "<ul>"
|
||||
for sample in created_samples:
|
||||
sample_list += f"<li>{sample.name} - {sample.sample_type_product_id.name}</li>"
|
||||
sample_list += "</ul>"
|
||||
|
||||
self.message_post(
|
||||
body=_("Muestras generadas automáticamente: %s") % sample_list,
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def _group_analyses_by_sample_type(self):
|
||||
"""Group order lines by required sample type"""
|
||||
groups = {}
|
||||
|
||||
for line in self.order_line:
|
||||
product = line.product_id
|
||||
|
||||
# Skip non-analysis products
|
||||
if not product.is_analysis:
|
||||
continue
|
||||
|
||||
# Check if analysis has a required sample type
|
||||
if not product.required_sample_type_id:
|
||||
_logger.warning(
|
||||
f"Analysis {product.name} has no required sample type defined"
|
||||
)
|
||||
# Post warning message
|
||||
self.message_post(
|
||||
body=_("Advertencia: El análisis '%s' no tiene tipo de muestra definido") % product.name,
|
||||
message_type='notification'
|
||||
)
|
||||
continue
|
||||
|
||||
sample_type = product.required_sample_type_id
|
||||
|
||||
# Initialize group if not exists
|
||||
if sample_type.id not in groups:
|
||||
groups[sample_type.id] = {
|
||||
'sample_type': sample_type,
|
||||
'lines': [],
|
||||
'total_volume': 0.0,
|
||||
'analyses': []
|
||||
}
|
||||
|
||||
# Add line to group
|
||||
groups[sample_type.id]['lines'].append(line)
|
||||
groups[sample_type.id]['analyses'].append(product.name)
|
||||
groups[sample_type.id]['total_volume'] += (product.sample_volume_ml or 0.0) * line.product_uom_qty
|
||||
|
||||
return groups
|
||||
|
||||
def _create_sample_for_group(self, group_data):
|
||||
"""Create a single sample for a group of analyses"""
|
||||
try:
|
||||
sample_type = group_data['sample_type']
|
||||
|
||||
# Generate a unique lot name using sequence
|
||||
sequence = self.env['ir.sequence'].next_by_code('stock.lot.serial')
|
||||
if not sequence:
|
||||
# Fallback to timestamp-based name if no sequence exists
|
||||
import time
|
||||
sequence = 'LAB-' + str(int(time.time()))[-8:]
|
||||
|
||||
# Prepare sample values
|
||||
vals = {
|
||||
'name': sequence, # Add the lot name
|
||||
'product_id': sample_type.product_variant_id.id,
|
||||
'patient_id': self.partner_id.id,
|
||||
'doctor_id': self.doctor_id.id if self.doctor_id else False,
|
||||
'origin': self.name,
|
||||
'sample_type_product_id': sample_type.id,
|
||||
'volume_ml': group_data['total_volume'],
|
||||
'is_lab_sample': True,
|
||||
'state': 'pending_collection',
|
||||
'analysis_names': ', '.join(group_data['analyses'][:3]) +
|
||||
('...' if len(group_data['analyses']) > 3 else '')
|
||||
}
|
||||
|
||||
# Create the sample
|
||||
sample = self.env['stock.lot'].create(vals)
|
||||
|
||||
_logger.info(
|
||||
f"Created sample {sample.name} for {len(group_data['analyses'])} analyses"
|
||||
)
|
||||
|
||||
return sample
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Error creating sample: {str(e)}")
|
||||
raise UserError(
|
||||
_("Error al crear muestra para %s: %s") % (sample_type.name, str(e))
|
||||
)
|
||||
|
||||
def _generate_lab_tests(self):
|
||||
"""Generate laboratory tests for analysis order lines"""
|
||||
self.ensure_one()
|
||||
_logger.info(f"Generating laboratory tests for order {self.name}")
|
||||
|
||||
# Get the test model
|
||||
TestModel = self.env['lims.test']
|
||||
created_tests = TestModel.browse()
|
||||
|
||||
# Create a test for each analysis line
|
||||
for line in self.order_line:
|
||||
if not line.product_id.is_analysis:
|
||||
continue
|
||||
|
||||
# Find appropriate sample for this analysis
|
||||
sample = self._find_sample_for_analysis(line.product_id)
|
||||
|
||||
if not sample:
|
||||
_logger.warning(
|
||||
f"No sample found for analysis {line.product_id.name} in order {self.name}"
|
||||
)
|
||||
self.message_post(
|
||||
body=_("Advertencia: No se encontró muestra para el análisis '%s'") % line.product_id.name,
|
||||
message_type='notification'
|
||||
)
|
||||
continue
|
||||
|
||||
# Create the test
|
||||
try:
|
||||
test = TestModel.create({
|
||||
'sale_order_line_id': line.id,
|
||||
'sample_id': sample.id,
|
||||
})
|
||||
created_tests |= test
|
||||
_logger.info(f"Created test {test.name} for analysis {line.product_id.name}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Error creating test for {line.product_id.name}: {str(e)}")
|
||||
self.message_post(
|
||||
body=_("Error al crear prueba para '%s': %s") % (line.product_id.name, str(e)),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Post message with created tests
|
||||
if created_tests:
|
||||
test_list = "<ul>"
|
||||
for test in created_tests:
|
||||
test_list += f"<li>{test.name} - {test.product_id.name}</li>"
|
||||
test_list += "</ul>"
|
||||
|
||||
self.message_post(
|
||||
body=_("Pruebas generadas automáticamente: %s") % test_list,
|
||||
message_type='notification'
|
||||
)
|
||||
_logger.info(f"Created {len(created_tests)} tests for order {self.name}")
|
||||
|
||||
def _find_sample_for_analysis(self, product):
|
||||
"""Find the appropriate sample for an analysis product"""
|
||||
# Check if the analysis has a required sample type
|
||||
if not product.required_sample_type_id:
|
||||
return False
|
||||
|
||||
# Find a generated sample with matching sample type
|
||||
for sample in self.generated_sample_ids:
|
||||
if sample.sample_type_product_id.id == product.required_sample_type_id.id:
|
||||
return sample
|
||||
|
||||
return False
|
||||
|
||||
def action_cancel(self):
|
||||
"""Override para cancelar automáticamente muestras y pruebas asociadas cuando se cancela una orden de laboratorio"""
|
||||
# Primero llamar al método padre
|
||||
res = super(SaleOrder, self).action_cancel()
|
||||
|
||||
# Si es una orden de laboratorio, cancelar muestras y pruebas asociadas
|
||||
if self.is_lab_request:
|
||||
# Cancelar muestras que estén en estados cancelables
|
||||
cancelable_sample_states = ['pending_collection', 'collected', 'received', 'in_process']
|
||||
samples_to_cancel = self.generated_sample_ids.filtered(
|
||||
lambda s: s.state in cancelable_sample_states
|
||||
)
|
||||
|
||||
if samples_to_cancel:
|
||||
# Cancelar las muestras
|
||||
samples_to_cancel.action_cancel()
|
||||
|
||||
# Registrar en el chatter de cada muestra
|
||||
for sample in samples_to_cancel:
|
||||
sample.message_post(
|
||||
body=_("Muestra cancelada automáticamente debido a la cancelación de la orden %s") % self.name,
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Buscar y cancelar pruebas asociadas a estas muestras
|
||||
tests_to_cancel = self.env['lims.test'].search([
|
||||
('sample_id', 'in', samples_to_cancel.ids),
|
||||
('state', 'not in', ['validated', 'cancelled'])
|
||||
])
|
||||
|
||||
if tests_to_cancel:
|
||||
for test in tests_to_cancel:
|
||||
test.action_cancel()
|
||||
test.message_post(
|
||||
body=_("Prueba cancelada automáticamente debido a la cancelación de la orden %s") % self.name,
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Registrar en el chatter de la orden
|
||||
message = _("Se cancelaron automáticamente:<br/>")
|
||||
message += _("- %d muestras<br/>") % len(samples_to_cancel)
|
||||
if tests_to_cancel:
|
||||
message += _("- %d pruebas de laboratorio") % len(tests_to_cancel)
|
||||
|
||||
self.message_post(
|
||||
body=message,
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
_logger.info(f"Cancelled {len(samples_to_cancel)} samples and {len(tests_to_cancel)} tests for order {self.name}")
|
||||
|
||||
return res
|
||||
|
||||
def action_print_sample_labels(self):
|
||||
"""Imprimir etiquetas de todas las muestras activas (incluyendo re-muestras)"""
|
||||
self.ensure_one()
|
||||
|
||||
# Obtener todas las muestras activas (no rechazadas ni canceladas)
|
||||
active_samples = self.all_sample_ids.filtered(
|
||||
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||
)
|
||||
|
||||
if not active_samples:
|
||||
raise UserError(_('No hay muestras activas para imprimir. Todas las muestras están rechazadas, canceladas o desechadas.'))
|
||||
|
||||
# Asegurar que todas las muestras tengan código de barras
|
||||
active_samples._ensure_barcode()
|
||||
|
||||
# Obtener el reporte
|
||||
report = self.env.ref('lims_management.action_report_sample_label')
|
||||
|
||||
# Retornar la acción de imprimir el reporte para las muestras activas
|
||||
return report.report_action(active_samples)
|
||||
|
||||
# Fields for lab results report
|
||||
can_print_results = fields.Boolean(
|
||||
string="Puede Imprimir Resultados",
|
||||
compute='_compute_can_print_results',
|
||||
help="Indica si todas las pruebas están validadas y se puede imprimir el informe"
|
||||
)
|
||||
|
||||
lab_test_ids = fields.One2many(
|
||||
'lims.test',
|
||||
'sale_order_id',
|
||||
string="Pruebas de Laboratorio",
|
||||
readonly=True,
|
||||
help="Todas las pruebas de laboratorio asociadas a esta orden"
|
||||
)
|
||||
|
||||
referring_doctor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Médico Solicitante",
|
||||
related='doctor_id',
|
||||
readonly=True,
|
||||
help="Médico que solicitó los análisis"
|
||||
)
|
||||
|
||||
lab_notes = fields.Text(
|
||||
string="Observaciones del Laboratorio",
|
||||
help="Observaciones generales sobre la orden o los resultados"
|
||||
)
|
||||
|
||||
@api.depends('lab_test_ids.state')
|
||||
def _compute_can_print_results(self):
|
||||
"""Compute if results can be printed (all tests validated)"""
|
||||
for order in self:
|
||||
tests = order.lab_test_ids
|
||||
order.can_print_results = (
|
||||
tests and
|
||||
all(test.state == 'validated' for test in tests)
|
||||
)
|
||||
|
||||
def action_print_lab_results(self):
|
||||
"""Generate and print lab results report"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verify all tests are validated
|
||||
if not self.can_print_results:
|
||||
raise UserError(_("No se puede imprimir el informe: hay pruebas sin validar"))
|
||||
|
||||
# Ensure this is a lab request
|
||||
if not self.is_lab_request:
|
||||
raise UserError(_("Esta no es una orden de laboratorio"))
|
||||
|
||||
# Generate the report
|
||||
return self.env.ref('lims_management.action_report_lab_results').report_action(self)
|
|
@ -1,602 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
class StockLot(models.Model):
|
||||
_name = 'stock.lot'
|
||||
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
|
||||
|
||||
is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio')
|
||||
|
||||
barcode = fields.Char(
|
||||
string='Código de Barras',
|
||||
compute='_compute_barcode',
|
||||
store=True,
|
||||
readonly=True,
|
||||
help="Código de barras único para la muestra en formato YYMMDDNNNNNNC"
|
||||
)
|
||||
|
||||
patient_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Paciente',
|
||||
domain="[('is_patient', '=', True)]"
|
||||
)
|
||||
|
||||
request_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Orden de Laboratorio',
|
||||
domain="[('is_lab_request', '=', True)]"
|
||||
)
|
||||
|
||||
collection_date = fields.Datetime(string='Fecha de Recolección')
|
||||
|
||||
container_type = fields.Selection([
|
||||
('serum_tube', 'Tubo de Suero'),
|
||||
('edta_tube', 'Tubo EDTA'),
|
||||
('swab', 'Hisopo'),
|
||||
('urine', 'Contenedor de Orina'),
|
||||
('other', 'Otro')
|
||||
], string='Tipo de Contenedor (Obsoleto)', help='Campo obsoleto, use sample_type_product_id en su lugar')
|
||||
|
||||
sample_type_product_id = fields.Many2one(
|
||||
'product.template',
|
||||
string='Tipo de Muestra',
|
||||
domain="[('is_sample_type', '=', True)]",
|
||||
help="Producto que representa el tipo de contenedor/muestra"
|
||||
)
|
||||
|
||||
collector_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Recolectado por',
|
||||
default=lambda self: self.env.user
|
||||
)
|
||||
|
||||
doctor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Médico Referente',
|
||||
domain="[('is_doctor', '=', True)]",
|
||||
help="Médico que ordenó los análisis"
|
||||
)
|
||||
|
||||
origin = fields.Char(
|
||||
string='Origen',
|
||||
help="Referencia a la orden de laboratorio que generó esta muestra"
|
||||
)
|
||||
|
||||
volume_ml = fields.Float(
|
||||
string='Volumen (ml)',
|
||||
help="Volumen total de muestra requerido"
|
||||
)
|
||||
|
||||
analysis_names = fields.Char(
|
||||
string='Análisis',
|
||||
help="Lista de análisis que se realizarán con esta muestra"
|
||||
)
|
||||
|
||||
state = fields.Selection([
|
||||
('pending_collection', 'Pendiente de Recolección'),
|
||||
('collected', 'Recolectada'),
|
||||
('received', 'Recibida en Laboratorio'),
|
||||
('in_process', 'En Proceso'),
|
||||
('analyzed', 'Analizada'),
|
||||
('stored', 'Almacenada'),
|
||||
('disposed', 'Desechada'),
|
||||
('cancelled', 'Cancelada'),
|
||||
('rejected', 'Rechazada')
|
||||
], string='Estado', default='collected', tracking=True)
|
||||
|
||||
# Rejection fields
|
||||
rejection_reason_id = fields.Many2one(
|
||||
'lims.rejection.reason',
|
||||
string='Motivo de Rechazo',
|
||||
tracking=True
|
||||
)
|
||||
rejection_notes = fields.Text(
|
||||
string='Notas de Rechazo',
|
||||
help="Información adicional sobre el rechazo"
|
||||
)
|
||||
rejected_by = fields.Many2one(
|
||||
'res.users',
|
||||
string='Rechazado por',
|
||||
readonly=True
|
||||
)
|
||||
rejection_date = fields.Datetime(
|
||||
string='Fecha de Rechazo',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Re-sampling fields
|
||||
parent_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra Original',
|
||||
help='Muestra original de la cual esta es un re-muestreo',
|
||||
domain="[('is_lab_sample', '=', True)]"
|
||||
)
|
||||
child_sample_ids = fields.One2many(
|
||||
'stock.lot',
|
||||
'parent_sample_id',
|
||||
string='Re-muestras',
|
||||
help='Muestras generadas como re-muestreo de esta'
|
||||
)
|
||||
resample_count = fields.Integer(
|
||||
string='Número de Re-muestreo',
|
||||
help='Indica cuántas veces se ha re-muestreado esta muestra',
|
||||
compute='_compute_resample_count',
|
||||
store=True
|
||||
)
|
||||
is_resample = fields.Boolean(
|
||||
string='Es Re-muestra',
|
||||
compute='_compute_is_resample',
|
||||
store=True
|
||||
)
|
||||
root_sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra Original (Raíz)',
|
||||
compute='_compute_root_sample',
|
||||
store=True,
|
||||
help='Muestra original de la cadena de re-muestreos'
|
||||
)
|
||||
resample_chain_count = fields.Integer(
|
||||
string='Re-muestreos en Cadena',
|
||||
compute='_compute_resample_chain_count',
|
||||
help='Número total de re-muestreos en toda la cadena'
|
||||
)
|
||||
|
||||
def action_collect(self):
|
||||
"""Mark sample(s) as collected"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
|
||||
record.message_post(
|
||||
body='Muestra recolectada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recolectada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_receive(self):
|
||||
"""Mark sample(s) as received in laboratory"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'received'})
|
||||
record.message_post(
|
||||
body='Muestra recibida en laboratorio por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recibida',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_start_analysis(self):
|
||||
"""Start analysis process"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'in_process'})
|
||||
record.message_post(
|
||||
body='Análisis iniciado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: En Proceso',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_complete_analysis(self):
|
||||
"""Mark analysis as completed"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'analyzed'})
|
||||
record.message_post(
|
||||
body='Análisis completado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Analizada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_store(self):
|
||||
"""Store the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'stored'})
|
||||
record.message_post(
|
||||
body='Muestra almacenada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Almacenada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_dispose(self):
|
||||
"""Dispose of the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'disposed'})
|
||||
record.message_post(
|
||||
body='Muestra desechada por %s. Motivo de disposición registrado.' % self.env.user.name,
|
||||
subject='Estado actualizado: Desechada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancel the sample(s)"""
|
||||
for record in self:
|
||||
old_state = record.state
|
||||
record.write({'state': 'cancelled'})
|
||||
record.message_post(
|
||||
body='Muestra cancelada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Cancelada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_open_rejection_wizard(self):
|
||||
"""Open the rejection wizard"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Rechazar Muestra',
|
||||
'res_model': 'lims.sample.rejection.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_sample_id': self.id,
|
||||
}
|
||||
}
|
||||
|
||||
def action_reject(self, create_resample=None):
|
||||
"""Reject the sample - to be called from wizard
|
||||
|
||||
Args:
|
||||
create_resample: Boolean to force resample creation. If None, uses system config
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state == 'completed':
|
||||
raise ValueError('No se puede rechazar una muestra ya completada')
|
||||
|
||||
# This method is called from the wizard, so rejection fields should already be set
|
||||
self.write({
|
||||
'state': 'rejected',
|
||||
'rejected_by': self.env.user.id,
|
||||
'rejection_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
reason_name = self.rejection_reason_id.name if self.rejection_reason_id else 'Sin especificar'
|
||||
notes = self.rejection_notes or ''
|
||||
|
||||
body = f'Muestra rechazada por {self.env.user.name}<br/>Motivo: {reason_name}'
|
||||
if notes:
|
||||
body += f'<br/>Notas: {notes}'
|
||||
|
||||
self.message_post(
|
||||
body=body,
|
||||
subject='Estado actualizado: Rechazada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Notify related sale order if exists
|
||||
if self.request_id:
|
||||
self.request_id.message_post(
|
||||
body=f'La muestra {self.name} ha sido rechazada. Motivo: {reason_name}',
|
||||
subject='Muestra Rechazada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Determine if we should create a resample
|
||||
should_create_resample = False
|
||||
|
||||
if create_resample is not None:
|
||||
# Explicit value from wizard
|
||||
should_create_resample = create_resample
|
||||
else:
|
||||
# Check system configuration
|
||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
||||
should_create_resample = auto_resample
|
||||
|
||||
if should_create_resample:
|
||||
try:
|
||||
# Create resample automatically
|
||||
resample_action = self.action_create_resample()
|
||||
self.message_post(
|
||||
body=_('Re-muestra generada automáticamente debido al rechazo'),
|
||||
subject='Re-muestreo Automático',
|
||||
message_type='notification'
|
||||
)
|
||||
except UserError as e:
|
||||
# If resample creation fails (e.g., max attempts reached), log it
|
||||
self.message_post(
|
||||
body=_('No se pudo generar re-muestra automática: %s') % str(e),
|
||||
subject='Error en Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
@api.onchange('sample_type_product_id')
|
||||
def _onchange_sample_type_product_id(self):
|
||||
"""Synchronize container_type when sample_type_product_id changes"""
|
||||
if self.sample_type_product_id:
|
||||
# Try to map product name to legacy container type
|
||||
product_name = self.sample_type_product_id.name.lower()
|
||||
if 'suero' in product_name or 'serum' in product_name:
|
||||
self.container_type = 'serum_tube'
|
||||
elif 'edta' in product_name:
|
||||
self.container_type = 'edta_tube'
|
||||
elif 'hisopo' in product_name or 'swab' in product_name:
|
||||
self.container_type = 'swab'
|
||||
elif 'orina' in product_name or 'urine' in product_name:
|
||||
self.container_type = 'urine'
|
||||
else:
|
||||
self.container_type = 'other'
|
||||
|
||||
def get_container_name(self):
|
||||
"""Get container name from product or legacy field"""
|
||||
if self.sample_type_product_id:
|
||||
return self.sample_type_product_id.name
|
||||
elif self.container_type:
|
||||
return dict(self._fields['container_type'].selection).get(self.container_type)
|
||||
return 'Unknown'
|
||||
|
||||
@api.depends('is_lab_sample', 'create_date')
|
||||
def _compute_barcode(self):
|
||||
"""Generate unique barcode for laboratory samples"""
|
||||
for record in self:
|
||||
if record.is_lab_sample and not record.barcode:
|
||||
record.barcode = record._generate_unique_barcode()
|
||||
elif not record.is_lab_sample:
|
||||
record.barcode = False
|
||||
|
||||
def _generate_unique_barcode(self):
|
||||
"""Generate a unique barcode in format YYMMDDNNNNNNC
|
||||
YY: Year (2 digits)
|
||||
MM: Month (2 digits)
|
||||
DD: Day (2 digits)
|
||||
NNNNNN: Sequential number (6 digits)
|
||||
C: Check digit
|
||||
"""
|
||||
self.ensure_one()
|
||||
now = datetime.now()
|
||||
date_prefix = now.strftime('%y%m%d')
|
||||
|
||||
# Get the highest sequence number for today
|
||||
domain = [
|
||||
('is_lab_sample', '=', True),
|
||||
('barcode', 'like', date_prefix + '%'),
|
||||
('id', '!=', self.id)
|
||||
]
|
||||
|
||||
max_barcode = self.search(domain, order='barcode desc', limit=1)
|
||||
|
||||
if max_barcode and max_barcode.barcode:
|
||||
# Extract sequence number from existing barcode
|
||||
try:
|
||||
sequence = int(max_barcode.barcode[6:12]) + 1
|
||||
except:
|
||||
sequence = 1
|
||||
else:
|
||||
sequence = 1
|
||||
|
||||
# Ensure we don't exceed 6 digits
|
||||
if sequence > 999999:
|
||||
# Add prefix based on sample type to allow more barcodes
|
||||
prefix_map = {
|
||||
'suero': '1',
|
||||
'edta': '2',
|
||||
'orina': '3',
|
||||
'hisopo': '4',
|
||||
'other': '9'
|
||||
}
|
||||
|
||||
type_prefix = '9' # default
|
||||
if self.sample_type_product_id:
|
||||
name_lower = self.sample_type_product_id.name.lower()
|
||||
for key, val in prefix_map.items():
|
||||
if key in name_lower:
|
||||
type_prefix = val
|
||||
break
|
||||
|
||||
sequence = int(type_prefix + str(sequence % 100000).zfill(5))
|
||||
|
||||
# Format sequence with leading zeros
|
||||
sequence_str = str(sequence).zfill(6)
|
||||
|
||||
# Calculate check digit using Luhn algorithm
|
||||
barcode_without_check = date_prefix + sequence_str
|
||||
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
|
||||
|
||||
final_barcode = barcode_without_check + str(check_digit)
|
||||
|
||||
# Verify uniqueness
|
||||
existing = self.search([
|
||||
('barcode', '=', final_barcode),
|
||||
('id', '!=', self.id)
|
||||
], limit=1)
|
||||
|
||||
if existing:
|
||||
# If collision, add random component and retry
|
||||
sequence = sequence * 10 + random.randint(0, 9)
|
||||
sequence_str = str(sequence % 1000000).zfill(6)
|
||||
barcode_without_check = date_prefix + sequence_str
|
||||
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
|
||||
final_barcode = barcode_without_check + str(check_digit)
|
||||
|
||||
return final_barcode
|
||||
|
||||
def _calculate_luhn_check_digit(self, number_str):
|
||||
"""Calculate Luhn check digit for barcode validation"""
|
||||
digits = [int(d) for d in number_str]
|
||||
odd_sum = sum(digits[-1::-2])
|
||||
even_sum = sum([sum(divmod(2 * d, 10)) for d in digits[-2::-2]])
|
||||
total = odd_sum + even_sum
|
||||
return (10 - (total % 10)) % 10
|
||||
|
||||
def _ensure_barcode(self):
|
||||
"""Ensure all lab samples have a barcode"""
|
||||
for record in self:
|
||||
if record.is_lab_sample and not record.barcode:
|
||||
record.barcode = record._generate_unique_barcode()
|
||||
return True
|
||||
|
||||
@api.depends('parent_sample_id')
|
||||
def _compute_is_resample(self):
|
||||
"""Compute if this sample is a resample"""
|
||||
for record in self:
|
||||
record.is_resample = bool(record.parent_sample_id)
|
||||
|
||||
@api.depends('child_sample_ids')
|
||||
def _compute_resample_count(self):
|
||||
"""Compute the number of times this sample has been resampled"""
|
||||
for record in self:
|
||||
record.resample_count = len(record.child_sample_ids)
|
||||
|
||||
@api.depends('parent_sample_id')
|
||||
def _compute_root_sample(self):
|
||||
"""Compute the root sample of the resample chain"""
|
||||
for record in self:
|
||||
root = record
|
||||
while root.parent_sample_id:
|
||||
root = root.parent_sample_id
|
||||
record.root_sample_id = root if root != record else False
|
||||
|
||||
@api.depends('parent_sample_id', 'child_sample_ids')
|
||||
def _compute_resample_chain_count(self):
|
||||
"""Compute total resamples in the entire chain"""
|
||||
for record in self:
|
||||
# Find root sample
|
||||
root = record
|
||||
while root.parent_sample_id:
|
||||
root = root.parent_sample_id
|
||||
# Count all resamples from root
|
||||
record.resample_chain_count = self._count_all_resamples_in_chain(root)
|
||||
|
||||
def action_create_resample(self):
|
||||
"""Create a new sample as a resample of the current one"""
|
||||
self.ensure_one()
|
||||
|
||||
# Determine the parent sample for the new resample
|
||||
# If current sample is already a resample, use its parent
|
||||
# Otherwise, use the current sample as parent
|
||||
parent_for_resample = self.parent_sample_id if self.parent_sample_id else self
|
||||
|
||||
# Check if there's already an active resample for the parent
|
||||
active_resamples = parent_for_resample.child_sample_ids.filtered(
|
||||
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
|
||||
)
|
||||
if active_resamples:
|
||||
raise UserError(_('La muestra %s ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') %
|
||||
(parent_for_resample.name, ', '.join(active_resamples.mapped('name'))))
|
||||
|
||||
# Get configuration
|
||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
||||
initial_state = IrConfig.get_param('lims_management.resample_state', 'pending_collection')
|
||||
prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-')
|
||||
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
|
||||
|
||||
# Find the original sample (root of the resample chain)
|
||||
original_sample = parent_for_resample
|
||||
while original_sample.parent_sample_id:
|
||||
original_sample = original_sample.parent_sample_id
|
||||
|
||||
# Count all resamples in the chain
|
||||
total_resamples = self._count_all_resamples_in_chain(original_sample)
|
||||
|
||||
# Check maximum resample attempts based on the entire chain
|
||||
if max_attempts > 0 and total_resamples >= max_attempts:
|
||||
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta cadena de muestras.') % max_attempts)
|
||||
|
||||
# Calculate resample number for naming (based on parent's resample count)
|
||||
resample_number = len(parent_for_resample.child_sample_ids) + 1
|
||||
|
||||
# Prepare values for new sample
|
||||
vals = {
|
||||
'name': f"{prefix}{parent_for_resample.name}-{resample_number}",
|
||||
'product_id': self.product_id.id,
|
||||
'patient_id': self.patient_id.id,
|
||||
'doctor_id': self.doctor_id.id,
|
||||
'origin': self.origin,
|
||||
'sample_type_product_id': self.sample_type_product_id.id,
|
||||
'volume_ml': self.volume_ml,
|
||||
'is_lab_sample': True,
|
||||
'state': initial_state,
|
||||
'analysis_names': self.analysis_names,
|
||||
'parent_sample_id': parent_for_resample.id, # Always use the determined parent
|
||||
'request_id': self.request_id.id if self.request_id else False,
|
||||
}
|
||||
|
||||
# Create the resample
|
||||
resample = self.create(vals)
|
||||
|
||||
# Post message in all relevant samples
|
||||
self.message_post(
|
||||
body=_('Re-muestra creada: %s') % resample.name,
|
||||
subject='Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
if self != parent_for_resample:
|
||||
# If we're creating from a resample, also notify the parent
|
||||
parent_for_resample.message_post(
|
||||
body=_('Nueva re-muestra creada: %s (debido al rechazo de %s)') % (resample.name, self.name),
|
||||
subject='Re-muestreo',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
resample.message_post(
|
||||
body=_('Esta es una re-muestra de: %s<br/>Creada debido al rechazo de: %s<br/>Motivo: %s') %
|
||||
(parent_for_resample.name, self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'),
|
||||
subject='Re-muestra creada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Notify receptionist if configured
|
||||
auto_notify = IrConfig.get_param('lims_management.auto_notify_resample', 'True') == 'True'
|
||||
if auto_notify:
|
||||
self._notify_resample_created(resample)
|
||||
|
||||
# If there's a related order, update it
|
||||
if self.request_id:
|
||||
self.request_id.message_post(
|
||||
body=_('Se ha creado una re-muestra (%s) para la muestra rechazada %s') % (resample.name, self.name),
|
||||
subject='Re-muestra creada',
|
||||
message_type='notification'
|
||||
)
|
||||
# Add the new sample to the order's generated samples
|
||||
self.request_id.generated_sample_ids = [(4, resample.id)]
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Re-muestra Creada',
|
||||
'res_model': 'stock.lot',
|
||||
'res_id': resample.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _count_all_resamples_in_chain(self, root_sample):
|
||||
"""Count all resamples in the entire chain starting from root"""
|
||||
count = 0
|
||||
samples_to_check = [root_sample]
|
||||
|
||||
while samples_to_check:
|
||||
sample = samples_to_check.pop(0)
|
||||
# Add all child samples to the check list
|
||||
for child in sample.child_sample_ids:
|
||||
count += 1
|
||||
samples_to_check.append(child)
|
||||
|
||||
return count
|
||||
|
||||
def _notify_resample_created(self, resample):
|
||||
"""Notify receptionist users about the created resample"""
|
||||
# Find receptionist users
|
||||
receptionist_group = self.env.ref('lims_management.group_lims_receptionist', raise_if_not_found=False)
|
||||
if receptionist_group:
|
||||
receptionist_users = receptionist_group.users
|
||||
|
||||
# Get the model id for stock.lot
|
||||
model_id = self.env['ir.model'].search([('model', '=', 'stock.lot')], limit=1).id
|
||||
|
||||
# Create activities for receptionists
|
||||
for user in receptionist_users:
|
||||
self.env['mail.activity'].create({
|
||||
'res_model': 'stock.lot',
|
||||
'res_model_id': model_id, # Campo obligatorio
|
||||
'res_id': resample.id,
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
||||
'summary': _('Nueva re-muestra pendiente de recolección'),
|
||||
'note': _('Se ha generado una re-muestra (%s) que requiere recolección. Muestra original: %s') %
|
||||
(resample.name, self.name),
|
||||
'user_id': user.id,
|
||||
'date_deadline': fields.Date.today(),
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
|
@ -1,89 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Formato de papel para etiquetas - DEBE IR PRIMERO -->
|
||||
<record id="paperformat_sample_label" model="report.paperformat">
|
||||
<field name="name">Formato Etiqueta Muestra</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_height">50</field>
|
||||
<field name="page_width">100</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">2</field>
|
||||
<field name="margin_bottom">2</field>
|
||||
<field name="margin_left">2</field>
|
||||
<field name="margin_right">2</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">0</field>
|
||||
<field name="dpi">200</field>
|
||||
</record>
|
||||
|
||||
<!-- Definir el reporte - DESPUÉS del paperformat -->
|
||||
<record id="action_report_sample_label" model="ir.actions.report">
|
||||
<field name="name">Etiquetas de Muestras</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">lims_management.report_sample_label</field>
|
||||
<field name="report_file">lims_management.report_sample_label</field>
|
||||
<field name="print_report_name">'Etiquetas - ' + object.name</field>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="lims_management.paperformat_sample_label"/>
|
||||
<field name="attachment_use" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Template del reporte -->
|
||||
<template id="report_sample_label">
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-set="body_classname">o_report_qweb_pdf</t>
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<div style="width: 96mm; height: 46mm; border: 1px solid #ccc; padding: 2mm; margin: 2mm; font-family: 'DejaVu Sans', Arial, sans-serif; display: inline-block; vertical-align: top; page-break-inside: avoid; overflow: hidden;">
|
||||
<!-- Encabezado -->
|
||||
<div style="text-align: center; margin-bottom: 2mm;">
|
||||
<h4 style="margin: 0; font-size: 14px; font-family: 'DejaVu Sans', Arial, sans-serif;">LABORATORIO CLÍNICO</h4>
|
||||
</div>
|
||||
|
||||
<!-- Información del paciente -->
|
||||
<div style="font-size: 11px; margin-bottom: 2mm; font-family: 'DejaVu Sans', Arial, sans-serif;">
|
||||
<div><strong>Paciente:</strong> <span t-field="o.patient_id.name"/></div>
|
||||
<div><strong>ID:</strong> <span t-field="o.patient_id.vat" t-if="o.patient_id.vat"/>
|
||||
<span t-else="">Sin ID</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información de la muestra -->
|
||||
<div style="font-size: 10px; margin-bottom: 3mm; font-family: 'DejaVu Sans', Arial, sans-serif;">
|
||||
<div><strong>Orden:</strong> <span t-field="o.origin"/></div>
|
||||
<div><strong>Tipo:</strong> <span t-esc="o.get_container_name()"/></div>
|
||||
<div><strong>Fecha:</strong> <span t-field="o.collection_date" t-options='{"widget": "date"}'/></div>
|
||||
</div>
|
||||
|
||||
<!-- Código de barras -->
|
||||
<div style="text-align: center; margin-top: 2mm;">
|
||||
<t t-set="barcode_value" t-value="o.barcode if o.barcode else o.name"/>
|
||||
<t t-if="barcode_value">
|
||||
<!-- Usar sintaxis específica de Odoo para código de barras -->
|
||||
<div style="overflow: hidden; height: 55px;">
|
||||
<span t-field="o.barcode"
|
||||
t-options="{'widget': 'barcode', 'type': 'Code128', 'width': 220, 'height': 45, 'humanreadable': 1}"
|
||||
style="display: block;"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div style="border: 1px solid #ccc; width: 220px; height: 45px; margin: 0 auto; display: flex; align-items: center; justify-content: center;">
|
||||
<span style="color: #666;">Sin código de barras</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Análisis a realizar (si caben) -->
|
||||
<div style="font-size: 9px; margin-top: 1mm; font-family: 'DejaVu Sans', Arial, sans-serif;" t-if="o.analysis_names">
|
||||
<div><strong>Análisis:</strong> <span t-field="o.analysis_names"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,274 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Template principal del reporte -->
|
||||
<template id="report_lab_results">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-if="o.is_lab_request">
|
||||
<t t-call="lims_management.report_lab_results_document" t-lang="o.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Documento individual -->
|
||||
<template id="report_lab_results_document">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<!-- Estilos CSS -->
|
||||
<style>
|
||||
.lab-header {
|
||||
border-bottom: 2px solid #337ab7;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.patient-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.results-table {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.results-table th {
|
||||
background-color: #e9ecef;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.results-table td {
|
||||
padding: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.result-out-of-range {
|
||||
color: #d9534f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.result-critical {
|
||||
background-color: #f2dede;
|
||||
color: #a94442;
|
||||
font-weight: bold;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.result-normal {
|
||||
color: #5cb85c;
|
||||
}
|
||||
.test-header {
|
||||
background-color: #337ab7;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.observations {
|
||||
background-color: #fcf8e3;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
border-left: 4px solid #faebcc;
|
||||
}
|
||||
.validation-info {
|
||||
margin-top: 40px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.signature-line {
|
||||
border-bottom: 1px solid #000;
|
||||
width: 250px;
|
||||
margin-top: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Encabezado del laboratorio -->
|
||||
<div class="lab-header">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<h2>LABORATORIO CLÍNICO</h2>
|
||||
<h3><t t-esc="o.company_id.name"/></h3>
|
||||
<p>
|
||||
<t t-if="o.company_id.street"><t t-esc="o.company_id.street"/><br/></t>
|
||||
<t t-if="o.company_id.city"><t t-esc="o.company_id.city"/>, </t>
|
||||
<t t-if="o.company_id.state_id"><t t-esc="o.company_id.state_id.name"/><br/></t>
|
||||
<t t-if="o.company_id.phone">Tel: <t t-esc="o.company_id.phone"/></t>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-4 text-right">
|
||||
<img t-if="o.company_id.logo" t-att-src="image_data_uri(o.company_id.logo)"
|
||||
style="max-height: 100px; max-width: 200px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información del paciente y orden -->
|
||||
<div class="patient-info">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h4>DATOS DEL PACIENTE</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Nombre:</strong></td>
|
||||
<td><t t-esc="o.partner_id.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Identificación:</strong></td>
|
||||
<td><t t-esc="o.partner_id.vat or 'N/A'"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Edad:</strong></td>
|
||||
<td>
|
||||
<t t-if="o.partner_id.birthdate_date">
|
||||
<t t-esc="o.partner_id.age"/> años
|
||||
</t>
|
||||
<t t-else="">N/A</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sexo:</strong></td>
|
||||
<td>
|
||||
<t t-if="o.partner_id.gender == 'male'">Masculino</t>
|
||||
<t t-elif="o.partner_id.gender == 'female'">Femenino</t>
|
||||
<t t-else="">No especificado</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h4>DATOS DE LA ORDEN</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Número de Orden:</strong></td>
|
||||
<td><t t-esc="o.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Fecha de Solicitud:</strong></td>
|
||||
<td><t t-esc="o.date_order" t-options='{"widget": "date"}'/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Médico Solicitante:</strong></td>
|
||||
<td><t t-esc="o.referring_doctor_id.name or 'N/A'"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Estado:</strong></td>
|
||||
<td>Resultados Validados</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resultados de análisis -->
|
||||
<h3 class="text-center" style="margin: 30px 0;">INFORME DE RESULTADOS</h3>
|
||||
|
||||
<!-- Iterar por cada prueba validada -->
|
||||
<t t-set="validated_tests" t-value="o.lab_test_ids.filtered(lambda t: t.state == 'validated')"/>
|
||||
<t t-foreach="validated_tests" t-as="test">
|
||||
<div class="test-section">
|
||||
<!-- Encabezado del análisis -->
|
||||
<h4 class="test-header">
|
||||
<t t-esc="test.product_id.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Tabla de resultados -->
|
||||
<table class="table results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">PARÁMETRO</th>
|
||||
<th width="20%" class="text-center">RESULTADO</th>
|
||||
<th width="15%" class="text-center">UNIDAD</th>
|
||||
<th width="35%" class="text-center">VALOR DE REFERENCIA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="test.result_ids" t-as="result">
|
||||
<tr>
|
||||
<td><t t-esc="result.parameter_id.name"/></td>
|
||||
<td class="text-center">
|
||||
<span t-attf-class="#{result.is_critical and 'result-critical' or result.is_out_of_range and 'result-out-of-range' or 'result-normal'}">
|
||||
<t t-esc="result.value_display"/>
|
||||
<t t-if="result.is_critical"> **</t>
|
||||
<t t-elif="result.is_out_of_range"> *</t>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="result.parameter_id.unit or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="result.applicable_range_id">
|
||||
<t t-if="result.parameter_id.value_type == 'numeric'">
|
||||
<t t-esc="result.applicable_range_id.normal_min"/> - <t t-esc="result.applicable_range_id.normal_max"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="result.applicable_range_id.reference_text or 'N/A'"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">N/A</t>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Mostrar notas si existen -->
|
||||
<t t-if="result.notes">
|
||||
<tr>
|
||||
<td colspan="4" style="padding-left: 30px; font-style: italic;">
|
||||
<strong>Nota:</strong> <t t-esc="result.notes"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Comentarios de la prueba -->
|
||||
<t t-if="test.notes">
|
||||
<div class="observations">
|
||||
<strong>Observaciones:</strong> <t t-esc="test.notes"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Leyenda de símbolos -->
|
||||
<div style="margin-top: 30px; font-size: 12px;">
|
||||
<p><strong>*</strong> Valor fuera del rango normal</p>
|
||||
<p><strong>**</strong> Valor crítico que requiere atención inmediata</p>
|
||||
</div>
|
||||
|
||||
<!-- Comentarios generales de la orden -->
|
||||
<t t-if="o.lab_notes">
|
||||
<div class="observations" style="margin-top: 30px;">
|
||||
<h5>OBSERVACIONES GENERALES</h5>
|
||||
<p><t t-esc="o.lab_notes"/></p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Información de validación -->
|
||||
<div class="validation-info">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<p><strong>Fecha de Validación:</strong>
|
||||
<t t-if="validated_tests">
|
||||
<t t-esc="validated_tests[0].validation_date" t-options='{"widget": "datetime"}'/>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6 text-center">
|
||||
<t t-if="validated_tests and validated_tests[0].validator_id">
|
||||
<div class="signature-line"></div>
|
||||
<p style="margin-top: 5px;">
|
||||
<strong><t t-esc="validated_tests[0].validator_id.name"/></strong><br/>
|
||||
Responsable del Laboratorio
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nota al pie -->
|
||||
<div style="margin-top: 50px; font-size: 10px; text-align: center; color: #666;">
|
||||
<p>Este informe es confidencial y está dirigido exclusivamente al paciente y/o médico tratante.</p>
|
||||
<p>Los resultados se relacionan únicamente con las muestras analizadas.</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Paper Format para el reporte de resultados -->
|
||||
<record id="paperformat_lab_results" model="report.paperformat">
|
||||
<field name="name">Formato Resultados de Laboratorio</field>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">40</field>
|
||||
<field name="margin_bottom">25</field>
|
||||
<field name="margin_left">10</field>
|
||||
<field name="margin_right">10</field>
|
||||
<field name="header_spacing">35</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- Acción del reporte -->
|
||||
<record id="action_report_lab_results" model="ir.actions.report">
|
||||
<field name="name">Informe de Resultados</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">lims_management.report_lab_results</field>
|
||||
<field name="report_file">lims_management.report_lab_results</field>
|
||||
<field name="print_report_name">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="paperformat_id" ref="paperformat_lab_results"/>
|
||||
<field name="attachment">'Resultados_Lab_' + object.name + '.pdf'</field>
|
||||
<field name="attachment_use">True</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,26 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_lims_analysis_parameter_user,lims.analysis.parameter.user,model_lims_analysis_parameter,base.group_user,1,0,0,0
|
||||
access_lims_analysis_parameter_manager,lims.analysis.parameter.manager,model_lims_analysis_parameter,group_lims_admin,1,1,1,1
|
||||
access_product_template_parameter_user,product.template.parameter.user,model_product_template_parameter,base.group_user,1,0,0,0
|
||||
access_product_template_parameter_manager,product.template.parameter.manager,model_product_template_parameter,group_lims_admin,1,1,1,1
|
||||
access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_range,base.group_user,1,0,0,0
|
||||
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,1,1,1,1
|
||||
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_line_receptionist,sale.order.line.receptionist,sale.model_sale_order_line,group_lims_receptionist,1,1,1,0
|
||||
access_sale_order_technician,sale.order.technician,sale.model_sale_order,group_lims_technician,1,0,0,0
|
||||
access_sale_order_line_technician,sale.order.line.technician,sale.model_sale_order_line,group_lims_technician,1,0,0,0
|
||||
access_sale_order_admin,sale.order.admin,sale.model_sale_order,group_lims_admin,1,1,1,1
|
||||
access_sale_order_line_admin,sale.order.line.admin,sale.model_sale_order_line,group_lims_admin,1,1,1,1
|
||||
access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
|
||||
access_lims_test_receptionist,lims.test.receptionist,model_lims_test,group_lims_receptionist,1,0,0,0
|
||||
access_lims_test_technician,lims.test.technician,model_lims_test,group_lims_technician,1,1,1,0
|
||||
access_lims_test_admin,lims.test.admin,model_lims_test,group_lims_admin,1,1,1,1
|
||||
access_lims_result_receptionist,lims.result.receptionist,model_lims_result,group_lims_receptionist,1,0,0,0
|
||||
access_lims_result_technician,lims.result.technician,model_lims_result,group_lims_technician,1,1,1,0
|
||||
access_lims_result_admin,lims.result.admin,model_lims_result,group_lims_admin,1,1,1,1
|
||||
access_lims_rejection_reason_user,lims.rejection.reason.user,model_lims_rejection_reason,base.group_user,1,0,0,0
|
||||
access_lims_rejection_reason_technician,lims.rejection.reason.technician,model_lims_rejection_reason,group_lims_technician,1,0,0,0
|
||||
access_lims_rejection_reason_admin,lims.rejection.reason.admin,model_lims_rejection_reason,group_lims_admin,1,1,1,1
|
||||
access_lims_sample_rejection_wizard_user,lims.sample.rejection.wizard.user,model_lims_sample_rejection_wizard,base.group_user,1,1,1,1
|
||||
access_lims_sample_rejection_wizard_technician,lims.sample.rejection.wizard.technician,model_lims_sample_rejection_wizard,group_lims_technician,1,1,1,1
|
||||
access_lims_config_settings_admin,lims.config.settings.admin,model_lims_config_settings,group_lims_admin,1,1,1,1
|
||||
access_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,base.group_user,1,1,1,1
|
||||
|
|
|
|
@ -33,81 +33,5 @@
|
|||
El usuario tiene acceso completo al módulo LIMS, incluyendo la validación de resultados, configuración y reportes.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Reglas de registro para lims.test -->
|
||||
|
||||
<!-- Recepcionistas: Solo pueden ver pruebas, no editarlas -->
|
||||
<record id="lims_test_receptionist_read_rule" model="ir.rule">
|
||||
<field name="name">Recepcionista: Solo lectura en pruebas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_receptionist'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Técnicos: Pueden editar solo pruebas no validadas -->
|
||||
<record id="lims_test_technician_write_rule" model="ir.rule">
|
||||
<field name="name">Técnico: Editar solo pruebas no validadas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_technician'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[('state', '!=', 'validated')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Administradores: Acceso completo (sin restricciones) -->
|
||||
<record id="lims_test_admin_all_rule" model="ir.rule">
|
||||
<field name="name">Administrador: Acceso completo a pruebas</field>
|
||||
<field name="model_id" ref="model_lims_test"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_admin'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Reglas de registro para lims.result -->
|
||||
|
||||
<!-- Recepcionistas: Solo pueden ver resultados -->
|
||||
<record id="lims_result_receptionist_read_rule" model="ir.rule">
|
||||
<field name="name">Recepcionista: Solo lectura en resultados</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_receptionist'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Técnicos: Pueden editar resultados de pruebas no validadas -->
|
||||
<record id="lims_result_technician_write_rule" model="ir.rule">
|
||||
<field name="name">Técnico: Editar resultados de pruebas no validadas</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_technician'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[('test_id.state', '!=', 'validated')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Administradores: Acceso completo a resultados -->
|
||||
<record id="lims_result_admin_all_rule" model="ir.rule">
|
||||
<field name="name">Administrador: Acceso completo a resultados</field>
|
||||
<field name="model_id" ref="model_lims_result"/>
|
||||
<field name="groups" eval="[(4, ref('group_lims_admin'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
Before Width: | Height: | Size: 1.4 MiB |
|
@ -1,21 +0,0 @@
|
|||
/* Estilos para pruebas de laboratorio LIMS */
|
||||
|
||||
/* Resaltar valores fuera de rango con decoration-danger */
|
||||
.o_list_view .o_data_row td[name="value_numeric"].text-danger,
|
||||
.o_list_view .o_data_row td[name="value_numeric"] .text-danger {
|
||||
color: #dc3545 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Asegurar que funcione con el decoration-danger de Odoo 18 */
|
||||
.o_list_renderer tbody tr td.o_list_number.text-danger,
|
||||
.o_list_renderer tbody tr td .o_field_number.text-danger {
|
||||
color: #dc3545 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Para campos en vista formulario también */
|
||||
.o_form_sheet .o_field_widget[name="value_numeric"].text-danger input {
|
||||
color: #dc3545 !important;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
# Tests del Módulo LIMS
|
||||
|
||||
Este directorio contiene los tests automatizados para el módulo `lims_management`, específicamente para el sistema de catálogo de parámetros.
|
||||
|
||||
## Estructura de Tests
|
||||
|
||||
### 1. test_analysis_parameter.py
|
||||
Tests para el modelo `lims.analysis.parameter`:
|
||||
- Creación de parámetros con diferentes tipos de valores
|
||||
- Validaciones de campos requeridos
|
||||
- Prevención de códigos duplicados
|
||||
- Relaciones con rangos y análisis
|
||||
|
||||
### 2. test_parameter_range.py
|
||||
Tests para el modelo `lims.parameter.range`:
|
||||
- Creación de rangos de referencia
|
||||
- Validaciones de valores mínimos y máximos
|
||||
- Rangos específicos por género y edad
|
||||
- Búsqueda de rangos aplicables según características del paciente
|
||||
|
||||
### 3. test_result_parameter_integration.py
|
||||
Tests de integración entre resultados y parámetros:
|
||||
- Asignación de parámetros a resultados
|
||||
- Selección automática de rangos aplicables
|
||||
- Detección de valores fuera de rango y críticos
|
||||
- Formato de visualización de resultados
|
||||
|
||||
### 4. test_auto_result_generation.py
|
||||
Tests para la generación automática de resultados:
|
||||
- Creación automática al generar pruebas
|
||||
- Herencia de secuencia desde la configuración
|
||||
- Rendimiento en creación masiva
|
||||
|
||||
## Ejecución de Tests
|
||||
|
||||
### Usando Odoo Test Framework
|
||||
```bash
|
||||
# Desde el servidor Odoo
|
||||
python3 -m odoo.cli.server -d lims_demo --test-enable --test-tags lims_management
|
||||
```
|
||||
|
||||
### Usando el Script Simplificado
|
||||
```bash
|
||||
# Copiar script al contenedor
|
||||
docker cp test/test_parameters_simple.py lims_odoo:/tmp/
|
||||
|
||||
# Ejecutar tests
|
||||
docker-compose exec odoo python3 /tmp/test_parameters_simple.py
|
||||
```
|
||||
|
||||
## Cobertura de Tests
|
||||
|
||||
Los tests cubren:
|
||||
|
||||
1. **Validaciones del Modelo**
|
||||
- Campos requeridos según tipo de parámetro
|
||||
- Restricciones de unicidad
|
||||
- Validaciones de rangos
|
||||
|
||||
2. **Lógica de Negocio**
|
||||
- Generación automática de resultados
|
||||
- Búsqueda de rangos aplicables
|
||||
- Cálculo de estados (fuera de rango, crítico)
|
||||
|
||||
3. **Integración**
|
||||
- Flujo completo desde orden hasta resultados
|
||||
- Compatibilidad con el sistema existente
|
||||
|
||||
## Datos de Prueba
|
||||
|
||||
Los tests utilizan:
|
||||
- Parámetros de demostración del archivo `parameter_demo.xml`
|
||||
- Rangos de referencia de `parameter_range_demo.xml`
|
||||
- Análisis configurados en `analysis_parameter_config_demo.xml`
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
- Los tests se ejecutan en transacciones que se revierten automáticamente
|
||||
- No afectan los datos de producción o demostración
|
||||
- Requieren que el módulo esté instalado con datos demo
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import test_analysis_parameter
|
||||
from . import test_parameter_range
|
||||
from . import test_result_parameter_integration
|
||||
from . import test_auto_result_generation
|
||||
from . import test_order_cancel_cascade
|
|
@ -1,175 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests para el modelo lims.analysis.parameter
|
||||
"""
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestAnalysisParameter(TransactionCase):
|
||||
"""Tests para el catálogo de parámetros de análisis"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.Parameter = self.env['lims.analysis.parameter']
|
||||
|
||||
def test_create_numeric_parameter(self):
|
||||
"""Test crear parámetro numérico con validaciones"""
|
||||
# Crear parámetro numérico válido
|
||||
param = self.Parameter.create({
|
||||
'code': 'TEST001',
|
||||
'name': 'Test Parameter',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'mg/dL',
|
||||
'description': 'Test numeric parameter'
|
||||
})
|
||||
|
||||
self.assertEqual(param.code, 'TEST001')
|
||||
self.assertEqual(param.value_type, 'numeric')
|
||||
self.assertEqual(param.unit, 'mg/dL')
|
||||
|
||||
def test_numeric_parameter_requires_unit(self):
|
||||
"""Test que parámetros numéricos requieren unidad"""
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Parameter.create({
|
||||
'code': 'TEST002',
|
||||
'name': 'Test Parameter No Unit',
|
||||
'value_type': 'numeric',
|
||||
# Sin unit - debe fallar
|
||||
})
|
||||
self.assertIn('unidad de medida', str(e.exception))
|
||||
|
||||
def test_create_selection_parameter(self):
|
||||
"""Test crear parámetro de selección con opciones"""
|
||||
param = self.Parameter.create({
|
||||
'code': 'TEST003',
|
||||
'name': 'Test Selection',
|
||||
'value_type': 'selection',
|
||||
'selection_values': 'Positivo,Negativo,Indeterminado'
|
||||
})
|
||||
|
||||
self.assertEqual(param.value_type, 'selection')
|
||||
self.assertEqual(param.selection_values, 'Positivo,Negativo,Indeterminado')
|
||||
|
||||
def test_selection_parameter_requires_values(self):
|
||||
"""Test que parámetros de selección requieren valores"""
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Parameter.create({
|
||||
'code': 'TEST004',
|
||||
'name': 'Test Selection No Values',
|
||||
'value_type': 'selection',
|
||||
# Sin selection_values - debe fallar
|
||||
})
|
||||
self.assertIn('valores de selección', str(e.exception))
|
||||
|
||||
def test_duplicate_code_not_allowed(self):
|
||||
"""Test que no se permiten códigos duplicados"""
|
||||
# Crear primer parámetro
|
||||
self.Parameter.create({
|
||||
'code': 'DUP001',
|
||||
'name': 'Original Parameter',
|
||||
'value_type': 'text'
|
||||
})
|
||||
|
||||
# Intentar crear duplicado
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Parameter.create({
|
||||
'code': 'DUP001',
|
||||
'name': 'Duplicate Parameter',
|
||||
'value_type': 'text'
|
||||
})
|
||||
self.assertIn('ya existe', str(e.exception))
|
||||
|
||||
def test_boolean_parameter(self):
|
||||
"""Test crear parámetro booleano"""
|
||||
param = self.Parameter.create({
|
||||
'code': 'BOOL001',
|
||||
'name': 'Test Boolean',
|
||||
'value_type': 'boolean',
|
||||
'description': 'Boolean parameter'
|
||||
})
|
||||
|
||||
self.assertEqual(param.value_type, 'boolean')
|
||||
self.assertFalse(param.unit) # Boolean no debe tener unidad
|
||||
|
||||
def test_text_parameter(self):
|
||||
"""Test crear parámetro de texto"""
|
||||
param = self.Parameter.create({
|
||||
'code': 'TEXT001',
|
||||
'name': 'Test Text',
|
||||
'value_type': 'text',
|
||||
'description': 'Text parameter'
|
||||
})
|
||||
|
||||
self.assertEqual(param.value_type, 'text')
|
||||
self.assertFalse(param.unit) # Text no debe tener unidad
|
||||
self.assertFalse(param.selection_values) # Text no debe tener valores de selección
|
||||
|
||||
def test_parameter_name_display(self):
|
||||
"""Test nombre mostrado del parámetro"""
|
||||
# Con unidad
|
||||
param1 = self.Parameter.create({
|
||||
'code': 'DISP001',
|
||||
'name': 'Glucosa',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'mg/dL'
|
||||
})
|
||||
self.assertEqual(param1.display_name, 'Glucosa (mg/dL)')
|
||||
|
||||
# Sin unidad
|
||||
param2 = self.Parameter.create({
|
||||
'code': 'DISP002',
|
||||
'name': 'Cultivo',
|
||||
'value_type': 'text'
|
||||
})
|
||||
self.assertEqual(param2.display_name, 'Cultivo')
|
||||
|
||||
def test_parameter_ranges_relationship(self):
|
||||
"""Test relación con rangos de referencia"""
|
||||
param = self.Parameter.create({
|
||||
'code': 'RANGE001',
|
||||
'name': 'Test with Ranges',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'U/L'
|
||||
})
|
||||
|
||||
# Crear rango para este parámetro
|
||||
range1 = self.env['lims.parameter.range'].create({
|
||||
'parameter_id': param.id,
|
||||
'name': 'Adult Male',
|
||||
'gender': 'male',
|
||||
'age_min': 18,
|
||||
'age_max': 65,
|
||||
'normal_min': 10.0,
|
||||
'normal_max': 50.0
|
||||
})
|
||||
|
||||
self.assertEqual(len(param.range_ids), 1)
|
||||
self.assertEqual(param.range_ids[0], range1)
|
||||
|
||||
def test_parameter_analysis_relationship(self):
|
||||
"""Test relación con análisis a través de product.template.parameter"""
|
||||
param = self.Parameter.create({
|
||||
'code': 'ANAL001',
|
||||
'name': 'Test Analysis Link',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'mmol/L'
|
||||
})
|
||||
|
||||
# Crear producto análisis
|
||||
analysis = self.env['product.template'].create({
|
||||
'name': 'Test Analysis',
|
||||
'type': 'service',
|
||||
'is_analysis': True,
|
||||
'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id,
|
||||
})
|
||||
|
||||
# Crear configuración parámetro-análisis
|
||||
config = self.env['product.template.parameter'].create({
|
||||
'product_tmpl_id': analysis.id,
|
||||
'parameter_id': param.id,
|
||||
'sequence': 10
|
||||
})
|
||||
|
||||
self.assertEqual(len(param.analysis_config_ids), 1)
|
||||
self.assertEqual(param.analysis_config_ids[0], config)
|
|
@ -1,283 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests para la generación automática de resultados basada en parámetros
|
||||
"""
|
||||
from odoo.tests import TransactionCase
|
||||
from datetime import date
|
||||
|
||||
|
||||
class TestAutoResultGeneration(TransactionCase):
|
||||
"""Tests para la generación automática de resultados al crear pruebas"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Modelos
|
||||
self.Test = self.env['lims.test']
|
||||
self.Sample = self.env['stock.lot']
|
||||
self.Order = self.env['sale.order']
|
||||
self.Parameter = self.env['lims.analysis.parameter']
|
||||
self.TemplateParam = self.env['product.template.parameter']
|
||||
self.Product = self.env['product.template']
|
||||
self.Partner = self.env['res.partner']
|
||||
|
||||
# Crear paciente
|
||||
self.patient = self.Partner.create({
|
||||
'name': 'Patient for Auto Generation',
|
||||
'is_patient': True,
|
||||
'gender': 'male',
|
||||
'birth_date': date(1985, 3, 15)
|
||||
})
|
||||
|
||||
# Crear doctor
|
||||
self.doctor = self.Partner.create({
|
||||
'name': 'Dr. Test',
|
||||
'is_doctor': True
|
||||
})
|
||||
|
||||
# Crear parámetros
|
||||
self.param1 = self.Parameter.create({
|
||||
'code': 'AUTO1',
|
||||
'name': 'Parameter Auto 1',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'mg/dL'
|
||||
})
|
||||
|
||||
self.param2 = self.Parameter.create({
|
||||
'code': 'AUTO2',
|
||||
'name': 'Parameter Auto 2',
|
||||
'value_type': 'selection',
|
||||
'selection_values': 'Normal,Anormal'
|
||||
})
|
||||
|
||||
self.param3 = self.Parameter.create({
|
||||
'code': 'AUTO3',
|
||||
'name': 'Parameter Auto 3',
|
||||
'value_type': 'text'
|
||||
})
|
||||
|
||||
# Crear análisis con parámetros configurados
|
||||
self.analysis_multi = self.Product.create({
|
||||
'name': 'Multi-Parameter Analysis',
|
||||
'type': 'service',
|
||||
'is_analysis': True,
|
||||
'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id,
|
||||
'sample_type_id': self.env.ref('lims_management.sample_type_blood').id,
|
||||
})
|
||||
|
||||
# Configurar parámetros en el análisis
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': self.analysis_multi.id,
|
||||
'parameter_id': self.param1.id,
|
||||
'sequence': 10
|
||||
})
|
||||
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': self.analysis_multi.id,
|
||||
'parameter_id': self.param2.id,
|
||||
'sequence': 20
|
||||
})
|
||||
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': self.analysis_multi.id,
|
||||
'parameter_id': self.param3.id,
|
||||
'sequence': 30
|
||||
})
|
||||
|
||||
# Crear análisis sin parámetros
|
||||
self.analysis_empty = self.Product.create({
|
||||
'name': 'Empty Analysis',
|
||||
'type': 'service',
|
||||
'is_analysis': True,
|
||||
'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id,
|
||||
})
|
||||
|
||||
def test_auto_generate_results_on_test_creation(self):
|
||||
"""Test generación automática de resultados al crear una prueba"""
|
||||
# Crear orden y muestra
|
||||
order = self.Order.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis_multi.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
order.action_confirm()
|
||||
|
||||
# Generar muestra
|
||||
order.action_generate_samples()
|
||||
sample = order.lab_sample_ids[0]
|
||||
|
||||
# La prueba debe haberse creado automáticamente con los resultados
|
||||
self.assertEqual(len(sample.test_ids), 1)
|
||||
test = sample.test_ids[0]
|
||||
|
||||
# Verificar que se generaron todos los resultados
|
||||
self.assertEqual(len(test.result_ids), 3)
|
||||
|
||||
# Verificar que cada resultado tiene el parámetro correcto
|
||||
param_ids = test.result_ids.mapped('parameter_id')
|
||||
self.assertIn(self.param1, param_ids)
|
||||
self.assertIn(self.param2, param_ids)
|
||||
self.assertIn(self.param3, param_ids)
|
||||
|
||||
# Verificar orden de secuencia
|
||||
results_sorted = test.result_ids.sorted('sequence')
|
||||
self.assertEqual(results_sorted[0].parameter_id, self.param1)
|
||||
self.assertEqual(results_sorted[1].parameter_id, self.param2)
|
||||
self.assertEqual(results_sorted[2].parameter_id, self.param3)
|
||||
|
||||
def test_no_results_for_analysis_without_parameters(self):
|
||||
"""Test que no se generan resultados para análisis sin parámetros"""
|
||||
# Crear orden con análisis sin parámetros
|
||||
order = self.Order.create({
|
||||
'partner_id': self.patient.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis_empty.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
order.action_confirm()
|
||||
order.action_generate_samples()
|
||||
|
||||
sample = order.lab_sample_ids[0]
|
||||
test = sample.test_ids[0]
|
||||
|
||||
# No debe haber resultados
|
||||
self.assertEqual(len(test.result_ids), 0)
|
||||
|
||||
def test_manual_test_creation_generates_results(self):
|
||||
"""Test generación de resultados al crear prueba manualmente"""
|
||||
# Crear muestra manual
|
||||
sample = self.Sample.create({
|
||||
'name': 'SAMPLE-MANUAL-001',
|
||||
'is_lab_sample': True,
|
||||
'patient_id': self.patient.id,
|
||||
'sample_state': 'collected'
|
||||
})
|
||||
|
||||
# Crear prueba manualmente
|
||||
test = self.Test.create({
|
||||
'sample_id': sample.id,
|
||||
'patient_id': self.patient.id,
|
||||
'product_id': self.analysis_multi.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Verificar generación automática
|
||||
self.assertEqual(len(test.result_ids), 3)
|
||||
|
||||
def test_results_inherit_correct_sequence(self):
|
||||
"""Test que los resultados heredan la secuencia correcta"""
|
||||
# Crear análisis con secuencias específicas
|
||||
analysis = self.Product.create({
|
||||
'name': 'Sequence Test Analysis',
|
||||
'type': 'service',
|
||||
'is_analysis': True,
|
||||
'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id,
|
||||
})
|
||||
|
||||
# Configurar con secuencias no consecutivas
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': analysis.id,
|
||||
'parameter_id': self.param1.id,
|
||||
'sequence': 100
|
||||
})
|
||||
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': analysis.id,
|
||||
'parameter_id': self.param2.id,
|
||||
'sequence': 50
|
||||
})
|
||||
|
||||
self.TemplateParam.create({
|
||||
'product_tmpl_id': analysis.id,
|
||||
'parameter_id': self.param3.id,
|
||||
'sequence': 75
|
||||
})
|
||||
|
||||
# Crear prueba
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient.id,
|
||||
'product_id': analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Verificar orden: param2 (50), param3 (75), param1 (100)
|
||||
results_sorted = test.result_ids.sorted('sequence')
|
||||
self.assertEqual(results_sorted[0].parameter_id, self.param2)
|
||||
self.assertEqual(results_sorted[0].sequence, 50)
|
||||
self.assertEqual(results_sorted[1].parameter_id, self.param3)
|
||||
self.assertEqual(results_sorted[1].sequence, 75)
|
||||
self.assertEqual(results_sorted[2].parameter_id, self.param1)
|
||||
self.assertEqual(results_sorted[2].sequence, 100)
|
||||
|
||||
def test_bulk_test_creation_performance(self):
|
||||
"""Test rendimiento de creación masiva de pruebas"""
|
||||
# Crear múltiples órdenes
|
||||
orders = []
|
||||
for i in range(5):
|
||||
order = self.Order.create({
|
||||
'partner_id': self.patient.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis_multi.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
order.action_confirm()
|
||||
orders.append(order)
|
||||
|
||||
# Generar muestras en lote
|
||||
for order in orders:
|
||||
order.action_generate_samples()
|
||||
|
||||
# Verificar que todas las pruebas tienen resultados
|
||||
total_tests = 0
|
||||
total_results = 0
|
||||
|
||||
for order in orders:
|
||||
for sample in order.lab_sample_ids:
|
||||
for test in sample.test_ids:
|
||||
total_tests += 1
|
||||
total_results += len(test.result_ids)
|
||||
|
||||
self.assertEqual(total_tests, 5)
|
||||
self.assertEqual(total_results, 15) # 5 tests * 3 parameters each
|
||||
|
||||
def test_result_generation_with_mixed_analyses(self):
|
||||
"""Test generación con análisis mixtos (con y sin parámetros)"""
|
||||
# Crear orden con múltiples análisis
|
||||
order = self.Order.create({
|
||||
'partner_id': self.patient.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'product_id': self.analysis_multi.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': self.analysis_empty.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})
|
||||
]
|
||||
})
|
||||
order.action_confirm()
|
||||
order.action_generate_samples()
|
||||
|
||||
# Verificar resultados por prueba
|
||||
tests_with_results = 0
|
||||
tests_without_results = 0
|
||||
|
||||
for sample in order.lab_sample_ids:
|
||||
for test in sample.test_ids:
|
||||
if test.result_ids:
|
||||
tests_with_results += 1
|
||||
else:
|
||||
tests_without_results += 1
|
||||
|
||||
self.assertEqual(tests_with_results, 1) # Solo analysis_multi
|
||||
self.assertEqual(tests_without_results, 1) # Solo analysis_empty
|
|
@ -1,263 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test para verificar la cancelación en cascada de muestras y pruebas
|
||||
cuando se cancela una orden de laboratorio
|
||||
"""
|
||||
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestOrderCancelCascade(TransactionCase):
|
||||
"""Test de cancelación en cascada de órdenes de laboratorio"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Obtener modelos
|
||||
self.Partner = self.env['res.partner']
|
||||
self.Product = self.env['product.product']
|
||||
self.SaleOrder = self.env['sale.order']
|
||||
self.StockLot = self.env['stock.lot']
|
||||
self.LimsTest = self.env['lims.test']
|
||||
|
||||
# Crear datos de prueba
|
||||
self.patient = self.Partner.create({
|
||||
'name': 'Test Patient Cancel',
|
||||
'is_patient': True,
|
||||
'birthdate_date': '1990-01-01',
|
||||
'gender': 'male'
|
||||
})
|
||||
|
||||
self.doctor = self.Partner.create({
|
||||
'name': 'Test Doctor Cancel',
|
||||
'is_doctor': True
|
||||
})
|
||||
|
||||
# Crear tipo de muestra
|
||||
self.sample_type = self.env['product.template'].create({
|
||||
'name': 'Tubo EDTA Test',
|
||||
'is_sample_type': True,
|
||||
'type': 'service',
|
||||
'categ_id': self.env.ref('product.product_category_all').id
|
||||
})
|
||||
|
||||
# Crear análisis
|
||||
self.analysis = self.env['product.template'].create({
|
||||
'name': 'Hemograma Test Cancel',
|
||||
'is_analysis': True,
|
||||
'type': 'service',
|
||||
'required_sample_type_id': self.sample_type.id,
|
||||
'categ_id': self.env.ref('product.product_category_all').id
|
||||
})
|
||||
|
||||
# Crear parámetro para el análisis
|
||||
self.parameter = self.env['lims.analysis.parameter'].create({
|
||||
'name': 'Hemoglobina Test',
|
||||
'code': 'HGB_TEST',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'g/dL'
|
||||
})
|
||||
|
||||
# Configurar parámetro en el análisis
|
||||
self.env['product.template.parameter'].create({
|
||||
'product_tmpl_id': self.analysis.id,
|
||||
'parameter_id': self.parameter.id,
|
||||
'sequence': 10
|
||||
})
|
||||
|
||||
def test_01_cancel_order_cancels_samples(self):
|
||||
"""Test que al cancelar una orden se cancelan las muestras asociadas"""
|
||||
# Crear orden de laboratorio
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar la orden (debe generar muestras)
|
||||
order.action_confirm()
|
||||
|
||||
# Verificar que se generaron muestras
|
||||
self.assertTrue(order.generated_sample_ids, "No se generaron muestras")
|
||||
samples = order.generated_sample_ids
|
||||
|
||||
# Verificar estado inicial de las muestras
|
||||
for sample in samples:
|
||||
self.assertIn(sample.state, ['pending_collection', 'collected'],
|
||||
f"Estado inicial incorrecto: {sample.state}")
|
||||
|
||||
# Cancelar la orden
|
||||
order.action_cancel()
|
||||
|
||||
# Verificar que las muestras fueron canceladas
|
||||
for sample in samples:
|
||||
self.assertEqual(sample.state, 'cancelled',
|
||||
f"Muestra no fue cancelada: {sample.state}")
|
||||
|
||||
def test_02_cancel_order_cancels_tests(self):
|
||||
"""Test que al cancelar una orden se cancelan las pruebas asociadas"""
|
||||
# Crear orden de laboratorio
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar la orden
|
||||
order.action_confirm()
|
||||
|
||||
# Obtener las pruebas generadas
|
||||
tests = self.LimsTest.search([
|
||||
('sale_order_line_id.order_id', '=', order.id)
|
||||
])
|
||||
self.assertTrue(tests, "No se generaron pruebas")
|
||||
|
||||
# Verificar estado inicial
|
||||
for test in tests:
|
||||
self.assertEqual(test.state, 'draft',
|
||||
f"Estado inicial incorrecto: {test.state}")
|
||||
|
||||
# Iniciar proceso en una prueba
|
||||
if tests:
|
||||
tests[0].write({'sample_id': order.generated_sample_ids[0].id})
|
||||
tests[0].action_start_process()
|
||||
self.assertEqual(tests[0].state, 'in_process')
|
||||
|
||||
# Cancelar la orden
|
||||
order.action_cancel()
|
||||
|
||||
# Verificar que las pruebas fueron canceladas
|
||||
for test in tests:
|
||||
self.assertEqual(test.state, 'cancelled',
|
||||
f"Prueba no fue cancelada: {test.state}")
|
||||
|
||||
def test_03_dont_cancel_completed_samples(self):
|
||||
"""Test que no se cancelan muestras en estados finales"""
|
||||
# Crear orden
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar
|
||||
order.action_confirm()
|
||||
|
||||
# Marcar una muestra como analizada
|
||||
sample = order.generated_sample_ids[0]
|
||||
sample.write({'state': 'analyzed'})
|
||||
|
||||
# Cancelar la orden
|
||||
order.action_cancel()
|
||||
|
||||
# Verificar que la muestra analizada no fue cancelada
|
||||
self.assertEqual(sample.state, 'analyzed',
|
||||
"Muestra analizada fue cancelada incorrectamente")
|
||||
|
||||
def test_04_dont_cancel_validated_tests(self):
|
||||
"""Test que no se cancelan pruebas validadas"""
|
||||
# Crear orden
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar
|
||||
order.action_confirm()
|
||||
|
||||
# Obtener prueba y marcarla como validada
|
||||
test = self.LimsTest.search([
|
||||
('sale_order_line_id.order_id', '=', order.id)
|
||||
], limit=1)
|
||||
|
||||
if test:
|
||||
test.write({
|
||||
'state': 'validated',
|
||||
'sample_id': order.generated_sample_ids[0].id
|
||||
})
|
||||
|
||||
# Cancelar la orden
|
||||
order.action_cancel()
|
||||
|
||||
# Verificar que la prueba validada no fue cancelada
|
||||
self.assertEqual(test.state, 'validated',
|
||||
"Prueba validada fue cancelada incorrectamente")
|
||||
|
||||
def test_05_chatter_messages_created(self):
|
||||
"""Test que se crean mensajes en el chatter"""
|
||||
# Crear orden
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'doctor_id': self.doctor.id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar
|
||||
order.action_confirm()
|
||||
|
||||
# Obtener conteo inicial de mensajes
|
||||
initial_order_messages = len(order.message_ids)
|
||||
sample = order.generated_sample_ids[0]
|
||||
initial_sample_messages = len(sample.message_ids)
|
||||
|
||||
# Cancelar
|
||||
order.action_cancel()
|
||||
|
||||
# Verificar que se agregaron mensajes
|
||||
self.assertGreater(len(order.message_ids), initial_order_messages,
|
||||
"No se agregó mensaje en la orden")
|
||||
self.assertGreater(len(sample.message_ids), initial_sample_messages,
|
||||
"No se agregó mensaje en la muestra")
|
||||
|
||||
# Verificar contenido del mensaje
|
||||
last_order_msg = order.message_ids[0].body
|
||||
self.assertIn("cancelaron automáticamente", last_order_msg,
|
||||
"Mensaje de orden no contiene texto esperado")
|
||||
|
||||
def test_06_non_lab_order_not_affected(self):
|
||||
"""Test que órdenes normales no son afectadas"""
|
||||
# Crear orden normal (no de laboratorio)
|
||||
order = self.SaleOrder.create({
|
||||
'partner_id': self.patient.id,
|
||||
'is_lab_request': False, # NO es orden de laboratorio
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1.0
|
||||
})]
|
||||
})
|
||||
|
||||
# Confirmar
|
||||
order.action_confirm()
|
||||
|
||||
# No deberían generarse muestras
|
||||
self.assertFalse(order.generated_sample_ids,
|
||||
"Se generaron muestras en orden normal")
|
||||
|
||||
# Cancelar - no debería causar error
|
||||
order.action_cancel()
|
||||
self.assertEqual(order.state, 'cancel')
|
|
@ -1,249 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests para el modelo lims.parameter.range
|
||||
"""
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestParameterRange(TransactionCase):
|
||||
"""Tests para rangos de referencia de parámetros"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.Range = self.env['lims.parameter.range']
|
||||
self.Parameter = self.env['lims.analysis.parameter']
|
||||
|
||||
# Crear parámetro de prueba
|
||||
self.test_param = self.Parameter.create({
|
||||
'code': 'HGB_TEST',
|
||||
'name': 'Hemoglobina Test',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'g/dL'
|
||||
})
|
||||
|
||||
def test_create_basic_range(self):
|
||||
"""Test crear rango básico"""
|
||||
range_obj = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Adulto General',
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
})
|
||||
|
||||
self.assertEqual(range_obj.parameter_id, self.test_param)
|
||||
self.assertEqual(range_obj.normal_min, 12.0)
|
||||
self.assertEqual(range_obj.normal_max, 16.0)
|
||||
self.assertFalse(range_obj.gender) # Sin género específico
|
||||
|
||||
def test_range_validation_min_max(self):
|
||||
"""Test validación que min < max"""
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Rango Inválido',
|
||||
'normal_min': 20.0,
|
||||
'normal_max': 10.0 # Max menor que min
|
||||
})
|
||||
self.assertIn('menor o igual', str(e.exception))
|
||||
|
||||
def test_range_validation_age(self):
|
||||
"""Test validación de rangos de edad"""
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Rango Edad Inválida',
|
||||
'age_min': 65,
|
||||
'age_max': 18, # Max menor que min
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
})
|
||||
self.assertIn('edad', str(e.exception))
|
||||
|
||||
def test_critical_values_validation(self):
|
||||
"""Test validación de valores críticos"""
|
||||
# Crítico min debe ser menor que normal min
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Crítico Inválido',
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0,
|
||||
'critical_min': 13.0 # Mayor que normal_min
|
||||
})
|
||||
self.assertIn('crítico mínimo', str(e.exception))
|
||||
|
||||
# Crítico max debe ser mayor que normal max
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Crítico Inválido 2',
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0,
|
||||
'critical_max': 15.0 # Menor que normal_max
|
||||
})
|
||||
self.assertIn('crítico máximo', str(e.exception))
|
||||
|
||||
def test_gender_specific_ranges(self):
|
||||
"""Test rangos específicos por género"""
|
||||
# Rango para hombres
|
||||
male_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Hombre Adulto',
|
||||
'gender': 'male',
|
||||
'age_min': 18,
|
||||
'age_max': 65,
|
||||
'normal_min': 14.0,
|
||||
'normal_max': 18.0
|
||||
})
|
||||
|
||||
# Rango para mujeres
|
||||
female_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Mujer Adulta',
|
||||
'gender': 'female',
|
||||
'age_min': 18,
|
||||
'age_max': 65,
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
})
|
||||
|
||||
self.assertEqual(male_range.gender, 'male')
|
||||
self.assertEqual(female_range.gender, 'female')
|
||||
|
||||
def test_pregnancy_specific_range(self):
|
||||
"""Test rangos para embarazadas"""
|
||||
pregnancy_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Embarazada',
|
||||
'gender': 'female',
|
||||
'pregnant': True,
|
||||
'age_min': 15,
|
||||
'age_max': 50,
|
||||
'normal_min': 11.0,
|
||||
'normal_max': 14.0
|
||||
})
|
||||
|
||||
self.assertTrue(pregnancy_range.pregnant)
|
||||
self.assertEqual(pregnancy_range.gender, 'female')
|
||||
|
||||
def test_find_applicable_range(self):
|
||||
"""Test encontrar rango aplicable según características del paciente"""
|
||||
# Crear varios rangos
|
||||
general_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'General',
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
})
|
||||
|
||||
male_adult_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Hombre Adulto',
|
||||
'gender': 'male',
|
||||
'age_min': 18,
|
||||
'age_max': 65,
|
||||
'normal_min': 14.0,
|
||||
'normal_max': 18.0
|
||||
})
|
||||
|
||||
child_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Niño',
|
||||
'age_max': 12,
|
||||
'normal_min': 11.0,
|
||||
'normal_max': 14.0
|
||||
})
|
||||
|
||||
pregnant_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Embarazada',
|
||||
'gender': 'female',
|
||||
'pregnant': True,
|
||||
'normal_min': 11.0,
|
||||
'normal_max': 14.0
|
||||
})
|
||||
|
||||
# Test para hombre adulto de 30 años
|
||||
applicable = self.Range._find_applicable_range(
|
||||
self.test_param.id,
|
||||
gender='male',
|
||||
age=30,
|
||||
is_pregnant=False
|
||||
)
|
||||
self.assertEqual(applicable, male_adult_range)
|
||||
|
||||
# Test para niño de 8 años
|
||||
applicable = self.Range._find_applicable_range(
|
||||
self.test_param.id,
|
||||
gender='male',
|
||||
age=8,
|
||||
is_pregnant=False
|
||||
)
|
||||
self.assertEqual(applicable, child_range)
|
||||
|
||||
# Test para mujer embarazada
|
||||
applicable = self.Range._find_applicable_range(
|
||||
self.test_param.id,
|
||||
gender='female',
|
||||
age=28,
|
||||
is_pregnant=True
|
||||
)
|
||||
self.assertEqual(applicable, pregnant_range)
|
||||
|
||||
# Test para caso sin rango específico (mujer no embarazada)
|
||||
applicable = self.Range._find_applicable_range(
|
||||
self.test_param.id,
|
||||
gender='female',
|
||||
age=35,
|
||||
is_pregnant=False
|
||||
)
|
||||
self.assertEqual(applicable, general_range) # Debe devolver el rango general
|
||||
|
||||
def test_range_overlap_allowed(self):
|
||||
"""Test que se permiten rangos superpuestos"""
|
||||
# Rango 1: 0-18 años
|
||||
range1 = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Pediátrico',
|
||||
'age_max': 18,
|
||||
'normal_min': 11.0,
|
||||
'normal_max': 15.0
|
||||
})
|
||||
|
||||
# Rango 2: 12-65 años (se superpone con rango 1)
|
||||
range2 = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Adolescente-Adulto',
|
||||
'age_min': 12,
|
||||
'age_max': 65,
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
})
|
||||
|
||||
# Ambos rangos deben existir sin error
|
||||
self.assertTrue(range1.exists())
|
||||
self.assertTrue(range2.exists())
|
||||
|
||||
def test_range_description_compute(self):
|
||||
"""Test generación automática de descripción"""
|
||||
# Rango con todas las características
|
||||
full_range = self.Range.create({
|
||||
'parameter_id': self.test_param.id,
|
||||
'name': 'Completo',
|
||||
'gender': 'female',
|
||||
'age_min': 18,
|
||||
'age_max': 45,
|
||||
'pregnant': True,
|
||||
'normal_min': 11.0,
|
||||
'normal_max': 14.0,
|
||||
'critical_min': 8.0,
|
||||
'critical_max': 20.0
|
||||
})
|
||||
|
||||
description = full_range.description
|
||||
self.assertIn('Mujer', description)
|
||||
self.assertIn('18-45 años', description)
|
||||
self.assertIn('Embarazada', description)
|
||||
self.assertIn('11.0 - 14.0', description)
|
||||
self.assertIn('Críticos', description)
|
|
@ -1,291 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests para la integración entre resultados y el catálogo de parámetros
|
||||
"""
|
||||
from odoo.tests import TransactionCase
|
||||
from datetime import date
|
||||
|
||||
|
||||
class TestResultParameterIntegration(TransactionCase):
|
||||
"""Tests para la integración de resultados con parámetros y rangos"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Modelos
|
||||
self.Result = self.env['lims.result']
|
||||
self.Test = self.env['lims.test']
|
||||
self.Parameter = self.env['lims.analysis.parameter']
|
||||
self.Range = self.env['lims.parameter.range']
|
||||
self.Partner = self.env['res.partner']
|
||||
self.Product = self.env['product.template']
|
||||
|
||||
# Crear paciente de prueba
|
||||
self.patient_male = self.Partner.create({
|
||||
'name': 'Test Patient Male',
|
||||
'is_patient': True,
|
||||
'gender': 'male',
|
||||
'birth_date': date(1990, 1, 1) # 34 años aprox
|
||||
})
|
||||
|
||||
self.patient_female_pregnant = self.Partner.create({
|
||||
'name': 'Test Patient Pregnant',
|
||||
'is_patient': True,
|
||||
'gender': 'female',
|
||||
'birth_date': date(1995, 6, 15), # 29 años aprox
|
||||
'is_pregnant': True
|
||||
})
|
||||
|
||||
# Crear parámetro de prueba
|
||||
self.param_glucose = self.Parameter.create({
|
||||
'code': 'GLU_TEST',
|
||||
'name': 'Glucosa Test',
|
||||
'value_type': 'numeric',
|
||||
'unit': 'mg/dL'
|
||||
})
|
||||
|
||||
# Crear rangos de referencia
|
||||
self.range_general = self.Range.create({
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'name': 'General',
|
||||
'normal_min': 70.0,
|
||||
'normal_max': 100.0,
|
||||
'critical_min': 50.0,
|
||||
'critical_max': 200.0
|
||||
})
|
||||
|
||||
self.range_pregnant = self.Range.create({
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'name': 'Embarazada',
|
||||
'gender': 'female',
|
||||
'pregnant': True,
|
||||
'normal_min': 60.0,
|
||||
'normal_max': 95.0,
|
||||
'critical_min': 45.0,
|
||||
'critical_max': 180.0
|
||||
})
|
||||
|
||||
# Crear análisis de prueba
|
||||
self.analysis = self.Product.create({
|
||||
'name': 'Glucosa en Sangre Test',
|
||||
'type': 'service',
|
||||
'is_analysis': True,
|
||||
'categ_id': self.env.ref('lims_management.product_category_clinical_analysis').id,
|
||||
})
|
||||
|
||||
# Configurar parámetro en el análisis
|
||||
self.env['product.template.parameter'].create({
|
||||
'product_tmpl_id': self.analysis.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'sequence': 10
|
||||
})
|
||||
|
||||
def test_result_parameter_assignment(self):
|
||||
"""Test asignación de parámetro a resultado"""
|
||||
# Crear test
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Crear resultado
|
||||
result = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 85.0
|
||||
})
|
||||
|
||||
self.assertEqual(result.parameter_id, self.param_glucose)
|
||||
self.assertEqual(result.value_type, 'numeric')
|
||||
self.assertEqual(result.unit, 'mg/dL')
|
||||
|
||||
def test_applicable_range_selection(self):
|
||||
"""Test selección automática de rango aplicable"""
|
||||
# Test para paciente masculino
|
||||
test_male = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
result_male = self.Result.create({
|
||||
'test_id': test_male.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 85.0
|
||||
})
|
||||
|
||||
# Debe usar el rango general
|
||||
self.assertEqual(result_male.applicable_range_id, self.range_general)
|
||||
self.assertFalse(result_male.is_out_of_range)
|
||||
self.assertFalse(result_male.is_critical)
|
||||
|
||||
# Test para paciente embarazada
|
||||
test_pregnant = self.Test.create({
|
||||
'patient_id': self.patient_female_pregnant.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
result_pregnant = self.Result.create({
|
||||
'test_id': test_pregnant.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 98.0 # Fuera de rango para embarazada
|
||||
})
|
||||
|
||||
# Debe usar el rango para embarazadas
|
||||
self.assertEqual(result_pregnant.applicable_range_id, self.range_pregnant)
|
||||
self.assertTrue(result_pregnant.is_out_of_range)
|
||||
self.assertFalse(result_pregnant.is_critical)
|
||||
|
||||
def test_out_of_range_detection(self):
|
||||
"""Test detección de valores fuera de rango"""
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Valor normal
|
||||
result_normal = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 85.0
|
||||
})
|
||||
self.assertFalse(result_normal.is_out_of_range)
|
||||
self.assertFalse(result_normal.is_critical)
|
||||
|
||||
# Valor alto pero no crítico
|
||||
result_high = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 115.0
|
||||
})
|
||||
self.assertTrue(result_high.is_out_of_range)
|
||||
self.assertFalse(result_high.is_critical)
|
||||
|
||||
# Valor crítico alto
|
||||
result_critical = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 250.0
|
||||
})
|
||||
self.assertTrue(result_critical.is_out_of_range)
|
||||
self.assertTrue(result_critical.is_critical)
|
||||
|
||||
def test_selection_parameter_result(self):
|
||||
"""Test resultado con parámetro de selección"""
|
||||
# Crear parámetro de selección
|
||||
param_culture = self.Parameter.create({
|
||||
'code': 'CULT_TEST',
|
||||
'name': 'Cultivo Test',
|
||||
'value_type': 'selection',
|
||||
'selection_values': 'Negativo,Positivo'
|
||||
})
|
||||
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
result = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_culture.id,
|
||||
'value_selection': 'Positivo'
|
||||
})
|
||||
|
||||
self.assertEqual(result.value_type, 'selection')
|
||||
self.assertEqual(result.value_selection, 'Positivo')
|
||||
self.assertFalse(result.applicable_range_id) # Selection no tiene rangos
|
||||
|
||||
def test_text_parameter_result(self):
|
||||
"""Test resultado con parámetro de texto"""
|
||||
param_observation = self.Parameter.create({
|
||||
'code': 'OBS_TEST',
|
||||
'name': 'Observación Test',
|
||||
'value_type': 'text'
|
||||
})
|
||||
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
result = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_observation.id,
|
||||
'value_text': 'Muestra hemolizada levemente'
|
||||
})
|
||||
|
||||
self.assertEqual(result.value_type, 'text')
|
||||
self.assertEqual(result.value_text, 'Muestra hemolizada levemente')
|
||||
|
||||
def test_boolean_parameter_result(self):
|
||||
"""Test resultado con parámetro booleano"""
|
||||
param_pregnancy = self.Parameter.create({
|
||||
'code': 'PREG_TEST',
|
||||
'name': 'Embarazo Test',
|
||||
'value_type': 'boolean'
|
||||
})
|
||||
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_female_pregnant.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
result = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_pregnancy.id,
|
||||
'value_boolean': True
|
||||
})
|
||||
|
||||
self.assertEqual(result.value_type, 'boolean')
|
||||
self.assertTrue(result.value_boolean)
|
||||
|
||||
def test_formatted_value_display(self):
|
||||
"""Test formato de visualización de valores"""
|
||||
test = self.Test.create({
|
||||
'patient_id': self.patient_male.id,
|
||||
'product_id': self.analysis.product_variant_id.id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Valor numérico
|
||||
result_numeric = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': self.param_glucose.id,
|
||||
'value_numeric': 85.5
|
||||
})
|
||||
self.assertEqual(result_numeric.formatted_value, '85.5 mg/dL')
|
||||
|
||||
# Valor de selección
|
||||
param_selection = self.Parameter.create({
|
||||
'code': 'SEL_FORMAT',
|
||||
'name': 'Selection Format',
|
||||
'value_type': 'selection',
|
||||
'selection_values': 'Opción A,Opción B'
|
||||
})
|
||||
|
||||
result_selection = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_selection.id,
|
||||
'value_selection': 'Opción A'
|
||||
})
|
||||
self.assertEqual(result_selection.formatted_value, 'Opción A')
|
||||
|
||||
# Valor booleano
|
||||
param_bool = self.Parameter.create({
|
||||
'code': 'BOOL_FORMAT',
|
||||
'name': 'Boolean Format',
|
||||
'value_type': 'boolean'
|
||||
})
|
||||
|
||||
result_bool = self.Result.create({
|
||||
'test_id': test.id,
|
||||
'parameter_id': param_bool.id,
|
||||
'value_boolean': True
|
||||
})
|
||||
self.assertEqual(result_bool.formatted_value, 'Sí')
|
|
@ -1,136 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View -->
|
||||
<record id="view_lims_analysis_parameter_form" model="ir.ui.view">
|
||||
<field name="name">lims.analysis.parameter.form</field>
|
||||
<field name="model">lims.analysis.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Parámetro de Análisis">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(lims_management.action_product_template_parameter)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
icon="fa-flask"
|
||||
context="{'search_default_parameter_id': id}">
|
||||
<field name="analysis_count" widget="statinfo" string="Análisis"/>
|
||||
</button>
|
||||
<button name="toggle_active"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options="{'terminology': 'archive'}"/>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archivado" bg_color="bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="code" placeholder="Código" class="oe_inline"/>
|
||||
</h1>
|
||||
<h2>
|
||||
<field name="name" placeholder="Nombre del parámetro" class="oe_inline"/>
|
||||
</h2>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Información General">
|
||||
<field name="value_type"/>
|
||||
<field name="unit" invisible="value_type != 'numeric'"/>
|
||||
<field name="selection_values"
|
||||
invisible="value_type != 'selection'"
|
||||
placeholder="Positivo, Negativo, No concluyente"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</group>
|
||||
<group string="Detalles">
|
||||
<field name="description" widget="text" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Rangos de Referencia" name="ranges">
|
||||
<field name="range_ids" context="{'default_parameter_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="gender"/>
|
||||
<field name="age_min"/>
|
||||
<field name="age_max"/>
|
||||
<field name="pregnant" optional="show"/>
|
||||
<field name="normal_min"/>
|
||||
<field name="normal_max"/>
|
||||
<field name="critical_min" optional="show"/>
|
||||
<field name="critical_max" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Análisis Configurados" name="analysis">
|
||||
<field name="template_parameter_ids">
|
||||
<list>
|
||||
<field name="product_tmpl_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="required"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_lims_analysis_parameter_list" model="ir.ui.view">
|
||||
<field name="name">lims.analysis.parameter.list</field>
|
||||
<field name="model">lims.analysis.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Parámetros de Análisis">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="value_type"/>
|
||||
<field name="unit" optional="show"/>
|
||||
<field name="analysis_count" optional="show"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_lims_analysis_parameter_search" model="ir.ui.view">
|
||||
<field name="name">lims.analysis.parameter.search</field>
|
||||
<field name="model">lims.analysis.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Parámetros">
|
||||
<field name="name" string="Parámetro"
|
||||
filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
|
||||
<field name="code"/>
|
||||
<filter string="Numéricos" name="numeric" domain="[('value_type', '=', 'numeric')]"/>
|
||||
<filter string="Texto" name="text" domain="[('value_type', '=', 'text')]"/>
|
||||
<filter string="Sí/No" name="boolean" domain="[('value_type', '=', 'boolean')]"/>
|
||||
<filter string="Selección" name="selection" domain="[('value_type', '=', 'selection')]"/>
|
||||
<separator/>
|
||||
<filter string="Activos" name="active" domain="[('active', '=', True)]"/>
|
||||
<filter string="Archivados" name="archived" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'value_type'}"/>
|
||||
<filter string="Estado" name="group_active" context="{'group_by': 'active'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_lims_analysis_parameter" model="ir.actions.act_window">
|
||||
<field name="name">Parámetros de Análisis</field>
|
||||
<field name="res_model">lims.analysis.parameter</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_analysis_parameter_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crear nuevo parámetro
|
||||
</p>
|
||||
<p>
|
||||
Los parámetros definen qué valores se pueden registrar en los análisis de laboratorio.
|
||||
Cada parámetro tiene un tipo de dato, unidad de medida y rangos de referencia.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,6 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Vista de Lista para Rangos de Referencia -->
|
||||
<record id="view_lims_analysis_range_tree" model="ir.ui.view">
|
||||
<field name="name">lims.analysis.range.tree</field>
|
||||
<field name="model">lims.analysis.range</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Rangos de Referencia" editable="bottom">
|
||||
<field name="gender"/>
|
||||
<field name="age_min"/>
|
||||
<field name="age_max"/>
|
||||
<field name="min_value"/>
|
||||
<field name="max_value"/>
|
||||
<field name="unit_of_measure"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Hereda la vista de formulario de producto para añadir la pestaña de Análisis -->
|
||||
<record id="view_product_template_form_lims" model="ir.ui.view">
|
||||
<field name="name">product.template.form.lims</field>
|
||||
|
@ -13,28 +29,14 @@
|
|||
<group>
|
||||
<group>
|
||||
<field name="analysis_type"/>
|
||||
<field name="required_sample_type_id"/>
|
||||
<field name="sample_volume_ml" invisible="not required_sample_type_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="technical_specifications"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Parámetros del Análisis"/>
|
||||
<field name="parameter_ids"
|
||||
context="{'default_product_tmpl_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="parameter_id"
|
||||
options="{'no_create': True}"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="parameter_value_type"/>
|
||||
<field name="parameter_unit"/>
|
||||
<field name="required"/>
|
||||
<field name="instructions" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
<separator string="Rangos de Referencia"/>
|
||||
<field name="value_range_ids"
|
||||
view_id="lims_management.view_lims_analysis_range_tree"/>
|
||||
</page>
|
||||
</xpath>
|
||||
<!-- Añade el campo is_analysis cerca del nombre del producto para fácil acceso -->
|
||||
|
@ -43,43 +45,5 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista de Lista para Productos de Análisis -->
|
||||
<record id="view_product_template_analysis_list" model="ir.ui.view">
|
||||
<field name="name">product.template.analysis.list</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='categ_id']" position="after">
|
||||
<field name="is_analysis" optional="show"/>
|
||||
<field name="analysis_type" optional="hide"/>
|
||||
<field name="required_sample_type_id" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista de Lista para Productos tipo Muestra -->
|
||||
<record id="view_product_template_sample_list" model="ir.ui.view">
|
||||
<field name="name">product.template.sample.list</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='categ_id']" position="after">
|
||||
<field name="is_sample_type" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Añadir is_sample_type al formulario de producto -->
|
||||
<record id="view_product_template_form_sample_type" model="ir.ui.view">
|
||||
<field name="name">product.template.form.sample.type</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='is_analysis']" position="after">
|
||||
<field name="is_sample_type"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ================================================================
|
||||
DASHBOARD 1: Estado de Órdenes de Laboratorio
|
||||
================================================================ -->
|
||||
|
||||
<!-- Vista Graph para Estado de Órdenes -->
|
||||
<record id="view_lab_order_dashboard_graph" model="ir.ui.view">
|
||||
<field name="name">sale.order.lab.dashboard.graph</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Estado de Ó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>
|
|
@ -1,55 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Laboratory Configuration Form View -->
|
||||
<record id="view_lims_config_settings_form" model="ir.ui.view">
|
||||
<field name="name">lims.config.settings.form</field>
|
||||
<field name="model">lims.config.settings</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuración del Laboratorio">
|
||||
<header>
|
||||
<button string="Guardar" type="object" name="execute" class="oe_highlight"/>
|
||||
<button string="Cancelar" special="cancel"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="o_form_label">Configuración de Re-muestreo</div>
|
||||
<group>
|
||||
<group name="resample_settings" string="Re-muestreo Automático">
|
||||
<field name="auto_resample_on_rejection"/>
|
||||
<field name="resample_state" invisible="not auto_resample_on_rejection"/>
|
||||
<field name="resample_prefix" invisible="not auto_resample_on_rejection"/>
|
||||
<field name="max_resample_attempts" invisible="not auto_resample_on_rejection"/>
|
||||
</group>
|
||||
<group name="notification_settings" string="Notificaciones">
|
||||
<field name="auto_notify_resample" invisible="not auto_resample_on_rejection"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información">
|
||||
<div class="text-muted">
|
||||
<p>El re-muestreo automático permite generar una nueva muestra cuando se rechaza una existente.</p>
|
||||
<p>Las notificaciones se enviarán a todos los usuarios con rol de Recepcionista.</p>
|
||||
</div>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open laboratory configuration -->
|
||||
<record id="action_lims_config_settings" model="ir.actions.act_window">
|
||||
<field name="name">Configuración del Laboratorio</field>
|
||||
<field name="res_model">lims.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'dialog_size': 'medium'}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu for Laboratory Configuration -->
|
||||
<menuitem id="menu_lims_lab_config"
|
||||
name="Configuración del Laboratorio"
|
||||
parent="lims_management.lims_menu_config"
|
||||
action="action_lims_config_settings"
|
||||
sequence="60"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
</data>
|
||||
</odoo>
|
|
@ -1,164 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Specialized Form View for Bulk Result Entry -->
|
||||
<record id="view_lims_test_result_entry_form" model="ir.ui.view">
|
||||
<field name="name">lims.test.result.entry.form</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Ingreso Rápido de Resultados">
|
||||
<header>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,in_process,result_entered,validated"/>
|
||||
<button name="action_start_process" string="Iniciar Análisis"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_enter_results" string="Guardar Resultados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'in_process'"/>
|
||||
<button name="action_validate" string="Validar Resultados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'result_entered'"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
<h2>
|
||||
<field name="patient_id" readonly="1"/>
|
||||
</h2>
|
||||
<h3>
|
||||
<field name="product_id" readonly="1"/>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="sample_id" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="technician_id" readonly="state != 'in_process'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="create_date" readonly="1"/>
|
||||
<field name="validation_date" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<separator string="Ingreso de Resultados"/>
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
context="{'form_view_ref': 'lims_management.view_lims_result_form'}">
|
||||
<list string="Resultados" editable="bottom" create="0" delete="0">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="parameter_id" readonly="1" force_save="1"/>
|
||||
<field name="parameter_code" readonly="1"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
|
||||
<!-- Entrada rápida de valores -->
|
||||
<field name="value_numeric"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
widget="float"
|
||||
options="{'digits': [16, 4]}"
|
||||
decoration-danger="is_critical"
|
||||
decoration-warning="is_out_of_range and not is_critical"/>
|
||||
<field name="value_text"
|
||||
invisible="parameter_value_type != 'text'"/>
|
||||
<field name="value_selection"
|
||||
invisible="parameter_value_type != 'selection'"
|
||||
widget="selection"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"
|
||||
widget="boolean_toggle"/>
|
||||
|
||||
<!-- Información de referencia -->
|
||||
<field name="parameter_unit"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
readonly="1"/>
|
||||
<field name="applicable_range_id"
|
||||
widget="many2one_tags"
|
||||
readonly="1"
|
||||
options="{'no_open': True}"/>
|
||||
|
||||
<!-- Indicadores -->
|
||||
<field name="result_status"
|
||||
widget="badge"
|
||||
decoration-success="result_status == 'normal'"
|
||||
decoration-warning="result_status == 'abnormal'"
|
||||
decoration-danger="result_status == 'critical'"/>
|
||||
|
||||
<!-- Campos ocultos -->
|
||||
<field name="is_out_of_range" invisible="1"/>
|
||||
<field name="is_critical" invisible="1"/>
|
||||
|
||||
<!-- Notas rápidas -->
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<group string="Observaciones Generales" invisible="state == 'draft'">
|
||||
<field name="notes" nolabel="1"
|
||||
placeholder="Ingrese observaciones generales sobre la prueba..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Quick Result Entry -->
|
||||
<record id="action_lims_result_entry" model="ir.actions.act_window">
|
||||
<field name="name">Ingreso Rápido de Resultados</field>
|
||||
<field name="res_model">lims.test</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="view_lims_test_result_entry_form"/>
|
||||
<field name="search_view_id" ref="view_lims_test_search"/>
|
||||
<field name="domain">[('state', 'in', ['in_process', 'result_entered'])]</field>
|
||||
<field name="context">{'search_default_my_tests': 1, 'search_default_in_process': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay pruebas pendientes de resultados
|
||||
</p>
|
||||
<p>
|
||||
Las pruebas aparecerán aquí cuando estén listas para
|
||||
el ingreso de resultados.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Result Summary Dashboard -->
|
||||
<record id="view_lims_result_pivot" model="ir.ui.view">
|
||||
<field name="name">lims.result.pivot</field>
|
||||
<field name="model">lims.result</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Análisis de Resultados">
|
||||
<field name="parameter_id" type="row"/>
|
||||
<field name="result_status" type="col"/>
|
||||
<field name="test_id" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_lims_result_graph" model="ir.ui.view">
|
||||
<field name="name">lims.result.graph</field>
|
||||
<field name="model">lims.result</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Distribución de Resultados" type="pie">
|
||||
<field name="result_status"/>
|
||||
<field name="test_id" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Result Analysis -->
|
||||
<record id="action_lims_result_analysis" model="ir.actions.act_window">
|
||||
<field name="name">Análisis de Resultados</field>
|
||||
<field name="res_model">lims.result</field>
|
||||
<field name="view_mode">pivot,graph,list</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Análisis estadístico de los resultados de laboratorio.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,169 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for lims.result -->
|
||||
<record id="view_lims_result_form" model="ir.ui.view">
|
||||
<field name="name">lims.result.form</field>
|
||||
<field name="model">lims.result</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Resultado de Análisis">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Información del Test">
|
||||
<field name="test_id" readonly="1"/>
|
||||
<field name="test_sample_id" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="test_sample_state" widget="badge"/>
|
||||
<field name="patient_id" readonly="1"/>
|
||||
<field name="test_date" readonly="1"/>
|
||||
</group>
|
||||
<group string="Parámetro">
|
||||
<field name="parameter_id" readonly="1"/>
|
||||
<field name="parameter_code" readonly="1"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
<field name="parameter_unit" invisible="parameter_value_type != 'numeric'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Valor del Resultado">
|
||||
<group>
|
||||
<field name="value_numeric"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
widget="float"
|
||||
options="{'digits': [16, 4]}"
|
||||
decoration-danger="is_out_of_range"
|
||||
decoration-warning="is_critical"/>
|
||||
<field name="value_text"
|
||||
invisible="parameter_value_type != 'text'"/>
|
||||
<field name="value_selection"
|
||||
invisible="parameter_value_type != 'selection'"
|
||||
widget="selection"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"
|
||||
widget="boolean_toggle"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_out_of_range" readonly="1"/>
|
||||
<field name="is_critical" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Rango de Referencia" invisible="parameter_value_type != 'numeric'">
|
||||
<field name="applicable_range_id" readonly="1">
|
||||
<form>
|
||||
<group>
|
||||
<field name="normal_min"/>
|
||||
<field name="normal_max"/>
|
||||
<field name="critical_min"/>
|
||||
<field name="critical_max"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Observaciones">
|
||||
<field name="notes" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View for lims.result -->
|
||||
<record id="view_lims_result_list" model="ir.ui.view">
|
||||
<field name="name">lims.result.list</field>
|
||||
<field name="model">lims.result</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Resultados de Análisis" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="test_sample_id"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"
|
||||
optional="show"/>
|
||||
<field name="test_sample_state"
|
||||
widget="badge"
|
||||
optional="show"/>
|
||||
<field name="parameter_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="parameter_code" optional="show"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
<field name="value_numeric"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
decoration-danger="is_out_of_range"
|
||||
decoration-warning="is_critical"/>
|
||||
<field name="value_text"
|
||||
invisible="parameter_value_type != 'text'"/>
|
||||
<field name="value_selection"
|
||||
invisible="parameter_value_type != 'selection'"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"
|
||||
widget="boolean_toggle"/>
|
||||
<field name="parameter_unit"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
optional="show"/>
|
||||
<field name="is_out_of_range" invisible="1"/>
|
||||
<field name="is_critical" invisible="1"/>
|
||||
<field name="applicable_range_id" optional="hide"/>
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for lims.result -->
|
||||
<record id="view_lims_result_search" model="ir.ui.view">
|
||||
<field name="name">lims.result.search</field>
|
||||
<field name="model">lims.result</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Resultados">
|
||||
<field name="test_id"/>
|
||||
<field name="test_sample_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="patient_id"/>
|
||||
<separator/>
|
||||
<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)]"/>
|
||||
<separator/>
|
||||
<filter string="Numéricos" name="numeric"
|
||||
domain="[('parameter_value_type', '=', 'numeric')]"/>
|
||||
<filter string="Texto" name="text"
|
||||
domain="[('parameter_value_type', '=', 'text')]"/>
|
||||
<filter string="Selección" name="selection"
|
||||
domain="[('parameter_value_type', '=', 'selection')]"/>
|
||||
<filter string="Sí/No" name="boolean"
|
||||
domain="[('parameter_value_type', '=', 'boolean')]"/>
|
||||
<separator/>
|
||||
<filter string="Muestras Pendientes" name="sample_pending"
|
||||
domain="[('test_sample_state', 'in', ['pending_collection', 'collected'])]"/>
|
||||
<filter string="Muestras en Proceso" name="sample_process"
|
||||
domain="[('test_sample_state', '=', 'in_process')]"/>
|
||||
<filter string="Muestras Completadas" name="sample_completed"
|
||||
domain="[('test_sample_state', '=', 'completed')]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Test" name="group_test" context="{'group_by': 'test_id'}"/>
|
||||
<filter string="Parámetro" name="group_parameter" context="{'group_by': 'parameter_id'}"/>
|
||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
||||
<filter string="Muestra" name="group_sample" context="{'group_by': 'test_sample_id'}"/>
|
||||
<filter string="Estado de Muestra" name="group_sample_state" context="{'group_by': 'test_sample_state'}"/>
|
||||
<filter string="Tipo de Valor" name="group_value_type" context="{'group_by': 'parameter_value_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for lims.result -->
|
||||
<record id="action_lims_result" model="ir.actions.act_window">
|
||||
<field name="name">Resultados de Análisis</field>
|
||||
<field name="res_model">lims.result</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_result_search"/>
|
||||
<field name="context">{'search_default_out_of_range': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay resultados registrados
|
||||
</p>
|
||||
<p>
|
||||
Los resultados se crean automáticamente al generar las pruebas
|
||||
de laboratorio basándose en los parámetros configurados.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,244 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Vista formulario para lims.test -->
|
||||
<record id="view_lims_test_form" model="ir.ui.view">
|
||||
<field name="name">lims.test.form</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Prueba de Laboratorio">
|
||||
<header>
|
||||
<button name="action_start_process" string="Iniciar Proceso"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'draft'"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
<button name="action_enter_results" string="Marcar Resultados Ingresados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'in_process'"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
<button name="action_validate" string="Validar Resultados"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'result_entered' or not require_validation"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
<button name="action_cancel" string="Cancelar"
|
||||
type="object"
|
||||
invisible="state in ['validated', 'cancelled']"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
<button name="action_draft" string="Volver a Borrador"
|
||||
type="object"
|
||||
invisible="state != 'cancelled'"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
<button name="action_regenerate_results" string="Regenerar Resultados"
|
||||
type="object"
|
||||
invisible="state not in ['draft', 'in_process']"
|
||||
confirm="¿Está seguro de regenerar los resultados? Esto eliminará los resultados actuales."
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,in_process,result_entered,validated"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="sale_order_line_id" invisible="1"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="sample_id"
|
||||
options="{'no_create': True}"
|
||||
domain="[('is_lab_sample', '=', True), ('patient_id', '=', patient_id)]"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="technician_id" readonly="state != 'draft'"/>
|
||||
<field name="require_validation" invisible="1"/>
|
||||
<field name="validator_id" readonly="1" invisible="not validator_id"/>
|
||||
<field name="validation_date" readonly="1" invisible="not validation_date"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Resultados" name="results">
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
context="{'default_test_id': id, 'default_patient_id': patient_id, 'default_test_date': create_date}"
|
||||
mode="list">
|
||||
<list string="Resultados" editable="bottom"
|
||||
decoration-danger="is_out_of_range and not is_critical"
|
||||
decoration-warning="is_critical"
|
||||
decoration-success="not is_out_of_range and not is_critical and parameter_value_type == 'numeric'">
|
||||
<field name="sequence" widget="handle" optional="show"/>
|
||||
<field name="parameter_id"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
readonly="1"/>
|
||||
<field name="parameter_code" optional="show" readonly="1"/>
|
||||
<field name="parameter_value_type" invisible="1"/>
|
||||
<!-- Campos de valor con mejores widgets -->
|
||||
<field name="value_numeric"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
widget="float"
|
||||
options="{'digits': [16, 4]}"
|
||||
class="oe_edit_only"/>
|
||||
<field name="value_text"
|
||||
invisible="parameter_value_type != 'text'"
|
||||
class="oe_edit_only"/>
|
||||
<field name="value_selection"
|
||||
invisible="parameter_value_type != 'selection'"
|
||||
placeholder="Ingrese valor o iniciales"
|
||||
class="oe_edit_only"/>
|
||||
<field name="value_boolean"
|
||||
invisible="parameter_value_type != 'boolean'"
|
||||
widget="boolean_toggle"
|
||||
class="oe_edit_only"/>
|
||||
<!-- Unidad y rangos -->
|
||||
<field name="parameter_unit"
|
||||
invisible="parameter_value_type != 'numeric'"
|
||||
optional="show"
|
||||
readonly="1"/>
|
||||
<field name="applicable_range_id"
|
||||
optional="hide"
|
||||
readonly="1"/>
|
||||
<!-- Indicadores de estado -->
|
||||
<field name="is_out_of_range" invisible="1"/>
|
||||
<field name="is_critical" invisible="1"/>
|
||||
<!-- Campo de estado visual -->
|
||||
<field name="result_status"
|
||||
widget="badge"
|
||||
optional="show"
|
||||
decoration-success="result_status == 'normal'"
|
||||
decoration-warning="result_status == 'abnormal'"
|
||||
decoration-danger="result_status == 'critical'"/>
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Observaciones" name="observations">
|
||||
<group>
|
||||
<field name="notes" nolabel="1" placeholder="Agregar observaciones generales de la prueba..."/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Actividades" name="activities">
|
||||
<field name="activity_ids"/>
|
||||
</page>
|
||||
<page string="Historial" name="history">
|
||||
<field name="message_ids" options="{'no_create': True}"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista lista para lims.test -->
|
||||
<record id="view_lims_test_tree" model="ir.ui.view">
|
||||
<field name="name">lims.test.tree</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Pruebas de Laboratorio">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="sample_id"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="technician_id" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'validated'"
|
||||
decoration-warning="state == 'result_entered'"
|
||||
decoration-info="state == 'in_process'"
|
||||
decoration-muted="state == 'cancelled'"/>
|
||||
<field name="create_date" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista kanban para lims.test -->
|
||||
<record id="view_lims_test_kanban" model="ir.ui.view">
|
||||
<field name="name">lims.test.kanban</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="state"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="create_date"/>
|
||||
<templates>
|
||||
<t t-name="kanban-card">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title">
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<div>
|
||||
<i class="fa fa-user" title="Paciente"/>
|
||||
<field name="patient_id"/>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa fa-flask" title="Análisis"/>
|
||||
<field name="product_id"/>
|
||||
</div>
|
||||
<div t-if="record.technician_id.raw_value">
|
||||
<i class="fa fa-user-md" title="Técnico"/>
|
||||
<field name="technician_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<field name="create_date" widget="date"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista búsqueda para lims.test -->
|
||||
<record id="view_lims_test_search" model="ir.ui.view">
|
||||
<field name="name">lims.test.search</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Pruebas">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="sample_id"/>
|
||||
<field name="technician_id"/>
|
||||
<separator/>
|
||||
<filter string="Borrador" name="draft" domain="[('state','=','draft')]"/>
|
||||
<filter string="En Proceso" name="in_process" domain="[('state','=','in_process')]"/>
|
||||
<filter string="Resultado Ingresado" name="result_entered" domain="[('state','=','result_entered')]"/>
|
||||
<filter string="Validado" name="validated" domain="[('state','=','validated')]"/>
|
||||
<separator/>
|
||||
<filter string="Mis Pruebas" name="my_tests" domain="[('technician_id','=',uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Hoy" name="today" domain="[('create_date','>=',(datetime.datetime.now().replace(hour=0, minute=0, second=0)).strftime('%Y-%m-%d %H:%M:%S'))]"/>
|
||||
<group expand="0" string="Agrupar Por">
|
||||
<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="Técnico" name="group_by_technician" context="{'group_by':'technician_id'}"/>
|
||||
<filter string="Fecha" name="group_by_date" context="{'group_by':'create_date:day'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -53,167 +53,6 @@
|
|||
action="action_lims_doctor"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Acción de Ventana para Solicitudes de Laboratorio -->
|
||||
<record id="action_lims_lab_request" model="ir.actions.act_window">
|
||||
<field name="name">Solicitudes de Laboratorio</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('is_lab_request', '=', True)]</field>
|
||||
<field name="context">{'default_is_lab_request': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crea una nueva solicitud de laboratorio
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú para Solicitudes de Laboratorio -->
|
||||
<menuitem
|
||||
id="lims_menu_lab_requests"
|
||||
name="Solicitudes de Laboratorio"
|
||||
parent="lims_menu_root"
|
||||
action="action_lims_lab_request"
|
||||
sequence="15"/>
|
||||
|
||||
<!-- Acción de Ventana para Muestras de Laboratorio -->
|
||||
<record id="action_lims_lab_sample" model="ir.actions.act_window">
|
||||
<field name="name">Muestras de Laboratorio</field>
|
||||
<field name="res_model">stock.lot</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'list', 'view_id': ref('view_lab_sample_list')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('view_lab_sample_form')})]"/>
|
||||
<field name="domain">[('is_lab_sample', '=', True)]</field>
|
||||
<field name="context" eval="{
|
||||
'default_is_lab_sample': True
|
||||
}"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crea una nueva muestra de laboratorio
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú para Muestras de Laboratorio -->
|
||||
<menuitem
|
||||
id="lims_menu_lab_samples"
|
||||
name="Muestras"
|
||||
parent="lims_menu_root"
|
||||
action="action_lims_lab_sample"
|
||||
sequence="16"/>
|
||||
|
||||
<!-- Menú para Muestras Rechazadas -->
|
||||
<menuitem
|
||||
id="lims_menu_lab_samples_rejected"
|
||||
name="Muestras Rechazadas"
|
||||
parent="lims_menu_root"
|
||||
action="action_lab_sample_rejected"
|
||||
sequence="17"/>
|
||||
|
||||
<!-- Submenú de Laboratorio -->
|
||||
<menuitem
|
||||
id="lims_menu_laboratory"
|
||||
name="Laboratorio"
|
||||
parent="lims_menu_root"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Acción para lims.test -->
|
||||
<record id="action_lims_test" model="ir.actions.act_window">
|
||||
<field name="name">Pruebas de Laboratorio</field>
|
||||
<field name="res_model">lims.test</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
<field name="context">{'search_default_my_tests': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crear primera prueba de laboratorio
|
||||
</p>
|
||||
<p>
|
||||
Aquí podrá gestionar las pruebas de laboratorio,
|
||||
ingresar resultados y validarlos.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú para Pruebas -->
|
||||
<menuitem id="menu_lims_tests"
|
||||
name="Pruebas"
|
||||
parent="lims_menu_laboratory"
|
||||
action="action_lims_test"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menú para Ingreso de Resultados -->
|
||||
<menuitem id="menu_lims_result_entry"
|
||||
name="Ingreso de Resultados"
|
||||
parent="lims_menu_laboratory"
|
||||
action="action_lims_result_entry"
|
||||
sequence="25"/>
|
||||
|
||||
<!-- Menú para Resultados -->
|
||||
<menuitem id="menu_lims_result"
|
||||
name="Resultados"
|
||||
parent="lims_menu_laboratory"
|
||||
action="action_lims_result"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Submenú de Dashboards -->
|
||||
<menuitem
|
||||
id="menu_lims_dashboards"
|
||||
name="Dashboards"
|
||||
parent="lims_menu_root"
|
||||
sequence="85"
|
||||
groups="lims_management.group_lims_admin"/>
|
||||
|
||||
<!-- Dashboards individuales -->
|
||||
<menuitem id="menu_lab_order_dashboard"
|
||||
name="Estado de Ó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 -->
|
||||
<menuitem
|
||||
id="lims_menu_reports"
|
||||
name="Reportes"
|
||||
parent="lims_menu_root"
|
||||
sequence="90"/>
|
||||
|
||||
<!-- Menú para Análisis de Resultados en Reportes -->
|
||||
<menuitem id="menu_lims_result_analysis"
|
||||
name="Análisis de Resultados"
|
||||
parent="lims_menu_reports"
|
||||
action="action_lims_result_analysis"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Submenú de Configuración -->
|
||||
<menuitem
|
||||
id="lims_menu_config"
|
||||
|
@ -247,89 +86,5 @@
|
|||
parent="lims_menu_config"
|
||||
action="action_lims_analysis_catalog"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Acción de Ventana para Tipos de Muestra -->
|
||||
<record id="action_lims_sample_type_catalog" model="ir.actions.act_window">
|
||||
<field name="name">Tipos de Muestra</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="domain">[('is_sample_type', '=', True)]</field>
|
||||
<field name="context" eval="{
|
||||
'default_is_sample_type': True,
|
||||
'default_type': 'service'
|
||||
}"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crea un nuevo tipo de muestra
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú para Tipos de Muestra -->
|
||||
<menuitem
|
||||
id="lims_menu_sample_type_catalog"
|
||||
name="Tipos de Muestra"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_sample_type_catalog"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Acción para abrir configuración de laboratorio -->
|
||||
<record id="action_lims_config_settings" model="ir.actions.act_window">
|
||||
<field name="name">Configuración</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'module' : 'lims_management'}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menú de Panel de Parámetros -->
|
||||
<menuitem id="menu_lims_parameter_dashboard"
|
||||
name="Panel de Parámetros"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_parameter_dashboard"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menú de Parámetros de Análisis -->
|
||||
<menuitem id="menu_lims_analysis_parameter"
|
||||
name="Parámetros de Análisis"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_analysis_parameter"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Menú de Rangos de Referencia -->
|
||||
<menuitem id="menu_lims_parameter_range"
|
||||
name="Rangos de Referencia"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_parameter_range"
|
||||
sequence="25"/>
|
||||
|
||||
<!-- Menú de Config. Parámetros-Análisis -->
|
||||
<menuitem id="menu_product_template_parameter_config"
|
||||
name="Config. Parámetros-Análisis"
|
||||
parent="lims_menu_config"
|
||||
action="action_product_template_parameter_config"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Menú de Estadísticas -->
|
||||
<menuitem id="menu_lims_parameter_statistics"
|
||||
name="Estadísticas"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_parameter_statistics"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- Menú de Motivos de Rechazo -->
|
||||
<menuitem id="menu_lims_rejection_reason"
|
||||
name="Motivos de Rechazo"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_rejection_reason"
|
||||
sequence="50"/>
|
||||
|
||||
<!-- Menú de configuración de ajustes -->
|
||||
<menuitem id="menu_lims_config_settings"
|
||||
name="Ajustes"
|
||||
parent="lims_menu_config"
|
||||
action="action_lims_config_settings"
|
||||
sequence="100"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Kanban View for Parameters Dashboard -->
|
||||
<record id="view_lims_analysis_parameter_kanban" model="ir.ui.view">
|
||||
<field name="name">lims.analysis.parameter.kanban</field>
|
||||
<field name="model">lims.analysis.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="value_type"/>
|
||||
<field name="unit"/>
|
||||
<field name="analysis_count"/>
|
||||
<field name="active"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title">
|
||||
<field name="code"/> - <field name="name"/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<div class="text-muted">
|
||||
<span>Tipo: </span>
|
||||
<field name="value_type" widget="badge"/>
|
||||
</div>
|
||||
<div t-if="record.unit.raw_value" class="text-muted">
|
||||
<span>Unidad: </span>
|
||||
<field name="unit"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<span t-if="!record.active.raw_value"
|
||||
class="badge badge-danger">Archivado</span>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="analysis_count" widget="badge"/>
|
||||
<span> análisis</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Graph View for Parameter Usage Statistics -->
|
||||
<record id="view_product_template_parameter_graph" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.graph</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Uso de Parámetros en Análisis" type="bar">
|
||||
<field name="parameter_id"/>
|
||||
<field name="product_tmpl_id" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action for Parameters -->
|
||||
<record id="action_lims_parameter_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Panel de Parámetros</field>
|
||||
<field name="res_model">lims.analysis.parameter</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_analysis_parameter_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay parámetros configurados
|
||||
</p>
|
||||
<p>
|
||||
Configure los parámetros que se utilizarán en los análisis clínicos.
|
||||
Cada parámetro puede tener múltiples rangos de referencia según
|
||||
las características del paciente.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Parameter Statistics Action -->
|
||||
<record id="action_lims_parameter_statistics" model="ir.actions.act_window">
|
||||
<field name="name">Estadísticas de Parámetros</field>
|
||||
<field name="res_model">product.template.parameter</field>
|
||||
<field name="view_mode">graph,pivot,list</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Visualización estadística del uso de parámetros en los diferentes análisis.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Configuration Summary Dashboard -->
|
||||
<record id="view_lims_config_summary_form" model="ir.ui.view">
|
||||
<field name="name">lims.config.summary.form</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="lims_management.res_config_settings_view_form_lims"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//app[@name='lims_management']//block[@name='lims_settings']" position="after">
|
||||
<div class="row mt16" id="lims_configuration_stats">
|
||||
<div class="col-12">
|
||||
<h2>Estadísticas de Configuración</h2>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h4>Parámetros</h4>
|
||||
<p class="text-muted">Total configurados</p>
|
||||
<button name="%(action_lims_analysis_parameter)d"
|
||||
string="Ver Parámetros"
|
||||
type="action"
|
||||
class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h4>Rangos</h4>
|
||||
<p class="text-muted">Rangos de referencia</p>
|
||||
<button name="%(action_lims_parameter_range)d"
|
||||
string="Ver Rangos"
|
||||
type="action"
|
||||
class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h4>Análisis</h4>
|
||||
<p class="text-muted">Con parámetros</p>
|
||||
<button name="%(action_product_template_parameter_config)d"
|
||||
string="Ver Configuración"
|
||||
type="action"
|
||||
class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h4>Estadísticas</h4>
|
||||
<p class="text-muted">Uso de parámetros</p>
|
||||
<button name="%(action_lims_parameter_statistics)d"
|
||||
string="Ver Estadísticas"
|
||||
type="action"
|
||||
class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,125 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View -->
|
||||
<record id="view_lims_parameter_range_form" model="ir.ui.view">
|
||||
<field name="name">lims.parameter.range.form</field>
|
||||
<field name="model">lims.parameter.range</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rango de Referencia">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Parámetro">
|
||||
<field name="parameter_id"
|
||||
options="{'no_create': True}"
|
||||
context="{'form_view_ref': 'lims_management.view_lims_analysis_parameter_form'}"/>
|
||||
<field name="parameter_unit"/>
|
||||
</group>
|
||||
<group string="Condiciones">
|
||||
<field name="gender"/>
|
||||
<field name="age_min"/>
|
||||
<field name="age_max"/>
|
||||
<field name="pregnant" invisible="gender == 'male'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Valores de Referencia">
|
||||
<group>
|
||||
<label for="normal_min"/>
|
||||
<div class="o_row">
|
||||
<field name="normal_min" class="oe_inline"/>
|
||||
<span class="oe_inline"> - </span>
|
||||
<field name="normal_max" class="oe_inline"/>
|
||||
<field name="parameter_unit" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<label for="critical_min"/>
|
||||
<div class="o_row">
|
||||
<span class="oe_inline">< </span>
|
||||
<field name="critical_min" class="oe_inline"/>
|
||||
<span class="oe_inline"> o > </span>
|
||||
<field name="critical_max" class="oe_inline"/>
|
||||
<field name="parameter_unit" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Interpretación Clínica">
|
||||
<field name="interpretation" nolabel="1"
|
||||
placeholder="Ingrese guías de interpretación clínica para este rango..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_lims_parameter_range_list" model="ir.ui.view">
|
||||
<field name="name">lims.parameter.range.list</field>
|
||||
<field name="model">lims.parameter.range</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Rangos de Referencia" editable="bottom">
|
||||
<field name="parameter_id" optional="hide"/>
|
||||
<field name="name"/>
|
||||
<field name="gender"/>
|
||||
<field name="age_min"/>
|
||||
<field name="age_max"/>
|
||||
<field name="pregnant" optional="show"/>
|
||||
<field name="normal_min"/>
|
||||
<field name="normal_max"/>
|
||||
<field name="critical_min" optional="show"/>
|
||||
<field name="critical_max" optional="show"/>
|
||||
<field name="parameter_unit" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_lims_parameter_range_search" model="ir.ui.view">
|
||||
<field name="name">lims.parameter.range.search</field>
|
||||
<field name="model">lims.parameter.range</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Rangos">
|
||||
<field name="parameter_id"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="name"/>
|
||||
<filter string="Masculino" name="male" domain="[('gender', '=', 'male')]"/>
|
||||
<filter string="Femenino" name="female" domain="[('gender', '=', 'female')]"/>
|
||||
<filter string="Ambos" name="both" domain="[('gender', '=', 'both')]"/>
|
||||
<separator/>
|
||||
<filter string="Embarazadas" name="pregnant" domain="[('pregnant', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Pediátrico (<18)" name="pediatric"
|
||||
domain="[('age_min', '<', 18)]"/>
|
||||
<filter string="Adulto (18-65)" name="adult"
|
||||
domain="[('age_min', '>=', 18), ('age_max', '<=', 65)]"/>
|
||||
<filter string="Geriátrico (>65)" name="geriatric"
|
||||
domain="[('age_max', '>', 65)]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Parámetro" name="group_parameter"
|
||||
context="{'group_by': 'parameter_id'}"/>
|
||||
<filter string="Género" name="group_gender"
|
||||
context="{'group_by': 'gender'}"/>
|
||||
<filter string="Embarazo" name="group_pregnant"
|
||||
context="{'group_by': 'pregnant'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_lims_parameter_range" model="ir.actions.act_window">
|
||||
<field name="name">Rangos de Referencia</field>
|
||||
<field name="res_model">lims.parameter.range</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_parameter_range_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Crear nuevo rango de referencia
|
||||
</p>
|
||||
<p>
|
||||
Los rangos de referencia definen los valores normales y críticos
|
||||
para cada parámetro según edad, género y otras condiciones del paciente.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -11,8 +11,6 @@
|
|||
<field name="name"/>
|
||||
<field name="gender"/>
|
||||
<field name="birthdate_date"/>
|
||||
<field name="age" optional="show"/>
|
||||
<field name="is_pregnant" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -45,9 +43,7 @@
|
|||
<field name="patient_identifier" invisible="not is_patient" readonly="patient_identifier"/>
|
||||
<field name="origin" readonly="id" invisible="not is_patient"/>
|
||||
<field name="birthdate_date" invisible="not is_patient"/>
|
||||
<field name="age" invisible="not is_patient or not birthdate_date"/>
|
||||
<field name="gender" invisible="not is_patient"/>
|
||||
<field name="is_pregnant" invisible="not is_patient or gender != 'female'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_doctor"/>
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for Configuration -->
|
||||
<record id="view_product_template_parameter_config_form" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.config.form</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuración de Parámetro en Análisis">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Análisis">
|
||||
<field name="product_tmpl_id"
|
||||
readonly="1"
|
||||
options="{'no_open': True}"/>
|
||||
</group>
|
||||
<group string="Parámetro">
|
||||
<field name="parameter_id"
|
||||
readonly="1"
|
||||
options="{'no_open': True}"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="parameter_value_type"/>
|
||||
<field name="parameter_unit" invisible="parameter_value_type != 'numeric'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Configuración">
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="required"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="instructions" widget="text"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View for Configuration -->
|
||||
<record id="view_product_template_parameter_config_list" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.config.list</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Configuración de Parámetros por Análisis">
|
||||
<field name="product_tmpl_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="parameter_value_type"/>
|
||||
<field name="parameter_unit" optional="show"/>
|
||||
<field name="sequence"/>
|
||||
<field name="required"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_product_template_parameter_config_search" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.config.search</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Configuración">
|
||||
<field name="product_tmpl_id" string="Análisis"/>
|
||||
<field name="parameter_id" string="Parámetro"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="parameter_name"/>
|
||||
<filter string="Requeridos" name="required"
|
||||
domain="[('required', '=', True)]"/>
|
||||
<filter string="Opcionales" name="optional"
|
||||
domain="[('required', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Numéricos" name="numeric"
|
||||
domain="[('parameter_value_type', '=', 'numeric')]"/>
|
||||
<filter string="Texto" name="text"
|
||||
domain="[('parameter_value_type', '=', 'text')]"/>
|
||||
<filter string="Sí/No" name="boolean"
|
||||
domain="[('parameter_value_type', '=', 'boolean')]"/>
|
||||
<filter string="Selección" name="selection"
|
||||
domain="[('parameter_value_type', '=', 'selection')]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Análisis" name="group_analysis"
|
||||
context="{'group_by': 'product_tmpl_id'}"/>
|
||||
<filter string="Parámetro" name="group_parameter"
|
||||
context="{'group_by': 'parameter_id'}"/>
|
||||
<filter string="Tipo de Valor" name="group_value_type"
|
||||
context="{'group_by': 'parameter_value_type'}"/>
|
||||
<filter string="Requerido" name="group_required"
|
||||
context="{'group_by': 'required'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Pivot View for Analysis -->
|
||||
<record id="view_product_template_parameter_pivot" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.pivot</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Matriz de Parámetros por Análisis">
|
||||
<field name="product_tmpl_id" type="row"/>
|
||||
<field name="parameter_id" type="col"/>
|
||||
<field name="required" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_product_template_parameter_config" model="ir.actions.act_window">
|
||||
<field name="name">Configuración Parámetros-Análisis</field>
|
||||
<field name="res_model">product.template.parameter</field>
|
||||
<field name="view_mode">list,form,pivot</field>
|
||||
<field name="search_view_id" ref="view_product_template_parameter_config_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Configurar parámetros en análisis
|
||||
</p>
|
||||
<p>
|
||||
Esta vista muestra la configuración de qué parámetros
|
||||
están incluidos en cada análisis clínico.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|