Compare commits
No commits in common. "dev" and "feature/7-sample-management" have entirely different histories.
dev
...
feature/7-
|
@ -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>
|
||||
```
|
202
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
|
||||
|
||||
|
|
53
create_lab_requests.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import odoo
|
||||
import json
|
||||
|
||||
def create_lab_requests(cr):
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
# Delete unwanted demo sale orders
|
||||
unwanted_orders = env['sale.order'].search([('name', 'in', ['S00001', 'S00002', 'S00003', 'S00004', 'S00005', 'S00006', 'S00007', 'S00008', 'S00009', 'S00010', 'S00011', 'S00012', 'S00013', 'S00014', 'S00015', 'S00016', 'S00017', 'S00018', 'S00019', 'S00020', 'S00021', 'S00022'])])
|
||||
for order in unwanted_orders:
|
||||
try:
|
||||
order.action_cancel()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
unwanted_orders.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get patient and doctor
|
||||
patient1 = env.ref('lims_management.demo_patient_1')
|
||||
doctor1 = env.ref('lims_management.demo_doctor_1')
|
||||
patient2 = env.ref('lims_management.demo_patient_2')
|
||||
|
||||
# Get analysis products
|
||||
hemograma = env.ref('lims_management.analysis_hemograma')
|
||||
perfil_lipidico = env.ref('lims_management.analysis_perfil_lipidico')
|
||||
|
||||
# Create Lab Request 1
|
||||
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}),
|
||||
(0, 0, {'product_id': perfil_lipidico.product_variant_id.id, 'product_uom_qty': 1})
|
||||
]
|
||||
})
|
||||
|
||||
# Create Lab Request 2
|
||||
env['sale.order'].create({
|
||||
'partner_id': patient2.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()
|
|
@ -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,7 @@ 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
|
||||
- ./create_lab_requests.py:/app/create_lab_requests.py
|
||||
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,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,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
|
154
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"
|
||||
]
|
||||
|
||||
|
@ -94,159 +93,6 @@ EOF
|
|||
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:
|
||||
|
|
|
@ -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,22 @@
|
|||
'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', 'sale'],
|
||||
'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',
|
||||
],
|
||||
'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,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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -12,13 +12,30 @@
|
|||
<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>
|
||||
|
||||
<!-- 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">
|
||||
|
@ -29,93 +46,25 @@
|
|||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
|
|
|
@ -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>
|
|
@ -10,9 +10,8 @@
|
|||
<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="phone">+1-202-555-0174</field>
|
||||
<field name="email">ana.torres@example.com</field>
|
||||
<field name="vat">03245678-9</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_patient_2" model="res.partner">
|
||||
|
@ -22,21 +21,8 @@
|
|||
<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="phone">+1-202-555-0192</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 -->
|
||||
|
@ -44,7 +30,7 @@
|
|||
<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="phone">+1-202-555-0145</field>
|
||||
<field name="email">luis.herrera@hospital.com</field>
|
||||
</record>
|
||||
|
||||
|
@ -52,14 +38,14 @@
|
|||
<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="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">+503 7456-7890</field>
|
||||
<field name="phone">+1-202-555-0188</field>
|
||||
<field name="email">laura.mendoza@example.com</field>
|
||||
</record>
|
||||
|
||||
|
@ -73,562 +59,5 @@
|
|||
<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 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Muestras de Laboratorio (Lotes) con el nuevo campo sample_type_product_id -->
|
||||
<data>
|
||||
<!-- Tipos de Muestra (Productos) -->
|
||||
<record id="sample_type_serum" model="product.template">
|
||||
<field name="name">Tubo de Suero (Tapa Roja)</field>
|
||||
<field name="is_sample_type" eval="True"/>
|
||||
<field name="type">service</field>
|
||||
</record>
|
||||
<record id="sample_type_edta" model="product.template">
|
||||
<field name="name">Tubo EDTA (Tapa Morada)</field>
|
||||
<field name="is_sample_type" eval="True"/>
|
||||
<field name="type">service</field>
|
||||
</record>
|
||||
<record id="sample_type_urine" model="product.template">
|
||||
<field name="name">Contenedor de Orina</field>
|
||||
<field name="is_sample_type" eval="True"/>
|
||||
<field name="type">service</field>
|
||||
</record>
|
||||
|
||||
<!-- Muestras de Laboratorio (Lotes) -->
|
||||
<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="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_serum').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="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_edta').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,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import analysis_parameter
|
||||
from . import product_template_parameter
|
||||
from . import parameter_range
|
||||
from . import analysis_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
|
||||
|
|
|
@ -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,13 @@ 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."
|
||||
string="Is a Sample Type",
|
||||
help="Check if this product represents a type of laboratory sample container."
|
||||
)
|
||||
|
||||
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 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo import models, fields
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
is_lab_request = fields.Boolean(
|
||||
string="Es Orden de Laboratorio",
|
||||
string="Is a Laboratory Request",
|
||||
default=False,
|
||||
copy=False,
|
||||
help="Campo técnico para identificar si la orden de venta es una solicitud de laboratorio."
|
||||
help="Technical field to identify if the sale order is a laboratory request."
|
||||
)
|
||||
|
||||
doctor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Médico Referente",
|
||||
string="Referring Doctor",
|
||||
domain="[('is_doctor', '=', True)]",
|
||||
help="El médico que refirió al paciente para esta solicitud de laboratorio."
|
||||
help="The doctor who referred the patient for this laboratory request."
|
||||
)
|
||||
|
||||
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 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime
|
||||
import random
|
||||
from odoo import models, fields
|
||||
|
||||
class StockLot(models.Model):
|
||||
_name = 'stock.lot'
|
||||
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
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"
|
||||
)
|
||||
is_lab_sample = fields.Boolean(string='Is a Laboratory Sample')
|
||||
|
||||
patient_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Paciente',
|
||||
string='Patient',
|
||||
domain="[('is_patient', '=', True)]"
|
||||
)
|
||||
|
||||
request_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Orden de Laboratorio',
|
||||
string='Lab Request',
|
||||
domain="[('is_lab_request', '=', True)]"
|
||||
)
|
||||
|
||||
collection_date = fields.Datetime(string='Fecha de Recolección')
|
||||
collection_date = fields.Datetime(string='Collection Date')
|
||||
|
||||
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"
|
||||
)
|
||||
('serum_tube', 'Serum Tube'),
|
||||
('edta_tube', 'EDTA Tube'),
|
||||
('swab', 'Swab'),
|
||||
('urine', 'Urine Container'),
|
||||
('other', 'Other')
|
||||
], string='Container Type')
|
||||
|
||||
collector_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Recolectado por',
|
||||
string='Collected by',
|
||||
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,4 @@
|
|||
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_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,base.group_user,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
|
||||
|
|
|
|
@ -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>
|
|
@ -101,118 +101,6 @@
|
|||
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
|
||||
|
@ -272,64 +160,5 @@
|
|||
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>
|
|
@ -1,93 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View -->
|
||||
<record id="view_product_template_parameter_form" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.form</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Parámetro del Análisis">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Información General">
|
||||
<field name="product_tmpl_id" readonly="1"/>
|
||||
<field name="parameter_id"
|
||||
options="{'no_create': True}"
|
||||
context="{'form_view_ref': 'lims_management.view_lims_analysis_parameter_form'}"/>
|
||||
<field name="sequence"/>
|
||||
<field name="required"/>
|
||||
</group>
|
||||
<group string="Detalles del Parámetro">
|
||||
<field name="parameter_code"/>
|
||||
<field name="parameter_value_type"/>
|
||||
<field name="parameter_unit"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Instrucciones Específicas">
|
||||
<field name="instructions" nolabel="1" placeholder="Ingrese instrucciones especiales para este parámetro en este análisis..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_product_template_parameter_list" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.list</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Parámetros por Análisis" 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>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_product_template_parameter_search" model="ir.ui.view">
|
||||
<field name="name">product.template.parameter.search</field>
|
||||
<field name="model">product.template.parameter</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Parámetros">
|
||||
<field name="product_tmpl_id"/>
|
||||
<field name="parameter_id"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="parameter_code"/>
|
||||
<filter string="Obligatorios" 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')]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Análisis" name="group_product" 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="Obligatorio" name="group_required" context="{'group_by': 'required'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_product_template_parameter" model="ir.actions.act_window">
|
||||
<field name="name">Parámetros por Análisis</field>
|
||||
<field name="res_model">product.template.parameter</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_product_template_parameter_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Configurar parámetros para análisis
|
||||
</p>
|
||||
<p>
|
||||
Aquí puede ver y configurar qué parámetros se miden en cada análisis,
|
||||
su orden de aparición y si son obligatorios u opcionales.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,93 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- List View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_list" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.list</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Motivos de Rechazo" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="severity" widget="badge"/>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="rejection_count"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_form" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.form</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Motivo de Rechazo">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archivado" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Motivo de rechazo..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="severity"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample"/>
|
||||
<field name="active"/>
|
||||
<field name="rejection_count"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Descripción">
|
||||
<field name="description" nolabel="1" placeholder="Descripción detallada del motivo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Rejection Reasons -->
|
||||
<record id="view_lims_rejection_reason_search" model="ir.ui.view">
|
||||
<field name="name">lims.rejection.reason.search</field>
|
||||
<field name="model">lims.rejection.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Motivos de Rechazo">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<filter string="Activos" name="active" domain="[('active', '=', True)]"/>
|
||||
<filter string="Archivados" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Requiere Nueva Muestra" name="requires_new" domain="[('requires_new_sample', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Severidad Alta/Crítica" name="high_severity" domain="[('severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Severidad" name="group_severity" context="{'group_by': 'severity'}"/>
|
||||
<filter string="Requiere Nueva Muestra" name="group_requires_new" context="{'group_by': 'requires_new_sample'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejection Reasons -->
|
||||
<record id="action_lims_rejection_reason" model="ir.actions.act_window">
|
||||
<field name="name">Motivos de Rechazo</field>
|
||||
<field name="res_model">lims.rejection.reason</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_lims_rejection_reason_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Configure los motivos de rechazo de muestras
|
||||
</p>
|
||||
<p>
|
||||
Los motivos de rechazo permiten categorizar y documentar
|
||||
las razones por las cuales una muestra no puede ser procesada.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Vista formulario heredada para res.config.settings -->
|
||||
<record id="res_config_settings_view_form_lims" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.lims</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Laboratorio" string="Laboratorio" name="lims_management">
|
||||
<block title="Configuración del Laboratorio" name="lims_settings">
|
||||
<setting help="Si está activado, los resultados de las pruebas deben ser validados por un administrador">
|
||||
<field name="lims_require_validation"/>
|
||||
</setting>
|
||||
<setting help="Si está activado, se generarán automáticamente registros de pruebas al confirmar órdenes">
|
||||
<field name="lims_auto_generate_tests"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -8,21 +8,6 @@
|
|||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Agregar botón de imprimir etiquetas en el header -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_print_sample_labels"
|
||||
string="Imprimir Etiquetas"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="not is_lab_request or state != 'sale' or not all_sample_ids"
|
||||
icon="fa-print"/>
|
||||
<button name="action_print_lab_results"
|
||||
string="Imprimir Informe de Resultados"
|
||||
type="object"
|
||||
class="btn-success"
|
||||
invisible="not can_print_results or not is_lab_request"
|
||||
icon="fa-file-pdf-o"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="doctor_id" invisible="not is_lab_request"/>
|
||||
</xpath>
|
||||
|
@ -33,54 +18,6 @@
|
|||
<xpath expr="//notebook/page[@name='order_lines']//field[@name='product_template_id']" position="attributes">
|
||||
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
|
||||
</xpath>
|
||||
<!-- Add Generated Samples tab -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Muestras" name="all_samples" invisible="not is_lab_request">
|
||||
<group string="Todas las Muestras (incluyendo Re-muestras)">
|
||||
<field name="all_sample_ids" nolabel="1" readonly="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}">
|
||||
<list string="Todas las Muestras" create="false" edit="false" delete="false">
|
||||
<field name="name" string="Código de Muestra"/>
|
||||
<field name="barcode" string="Código de Barras" optional="show"/>
|
||||
<field name="sample_type_product_id" string="Tipo de Muestra"/>
|
||||
<field name="volume_ml" string="Volumen (ml)" optional="show"/>
|
||||
<field name="analysis_names" string="Análisis" optional="show"/>
|
||||
<field name="is_resample" string="Es Re-muestra" widget="boolean_toggle"/>
|
||||
<field name="parent_sample_id" string="Muestra Original" optional="show"/>
|
||||
<field name="state" string="Estado" widget="badge"
|
||||
decoration-success="state == 'analyzed'"
|
||||
decoration-info="state == 'in_process'"
|
||||
decoration-danger="state == 'rejected'"
|
||||
decoration-warning="state == 'pending_collection'"/>
|
||||
<field name="rejection_reason_id" string="Motivo Rechazo" optional="show"/>
|
||||
<button name="action_collect" string="Recolectar" type="object"
|
||||
class="btn-sm btn-primary" invisible="state != 'pending_collection'"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Resumen" col="4">
|
||||
<field name="generated_sample_ids" invisible="1"/>
|
||||
<group>
|
||||
<label for="generated_sample_ids" string="Muestras Originales:"/>
|
||||
<div>
|
||||
<span class="badge badge-primary"><field name="generated_sample_ids" readonly="1" widget="many2many_tags"/></span>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p><i class="fa fa-info-circle"/> Las muestras han sido generadas automáticamente basándose en los análisis solicitados.</p>
|
||||
<p>Las re-muestras se generan cuando una muestra es rechazada.</p>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Observaciones Lab" name="lab_notes" invisible="not is_lab_request">
|
||||
<group>
|
||||
<field name="lab_notes" nolabel="1" placeholder="Ingrese observaciones generales sobre la orden o los resultados..."/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
@ -93,10 +30,6 @@
|
|||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="doctor_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='state']" position="before">
|
||||
<field name="is_lab_request" optional="show" string="Orden Lab"/>
|
||||
<field name="generated_sample_ids" widget="many2many_tags" optional="hide" string="Muestras"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
|
@ -7,17 +7,13 @@
|
|||
<field name="name">lab.sample.list</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Muestras de Laboratorio">
|
||||
<field name="name" string="Código"/>
|
||||
<field name="patient_id" string="Paciente"/>
|
||||
<field name="product_id" string="Tipo de Muestra"/>
|
||||
<field name="sample_type_product_id" string="Tipo de Muestra"/>
|
||||
<field name="collection_date" string="Fecha de Recolección"/>
|
||||
<field name="collector_id" string="Recolectado por"/>
|
||||
<field name="container_type" optional="hide" string="Tipo Contenedor (Obsoleto)"/>
|
||||
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-danger="state == 'rejected'" decoration-muted="state == 'stored' or state == 'disposed' or state == 'cancelled'" widget="badge"/>
|
||||
<field name="is_resample" string="Re-muestra" widget="boolean_toggle" optional="show"/>
|
||||
<field name="resample_count" string="Re-muestreos" optional="show"/>
|
||||
<list string="Lab Samples">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id" string="Sample Type"/>
|
||||
<field name="collection_date"/>
|
||||
<field name="collector_id"/>
|
||||
<field name="container_type"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -27,28 +23,7 @@
|
|||
<field name="name">lab.sample.form</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Muestra de Laboratorio">
|
||||
<header>
|
||||
<button name="action_collect" string="Recolectar" type="object" class="oe_highlight" invisible="state != 'pending_collection'"/>
|
||||
<button name="action_receive" string="Recibir" type="object" class="oe_highlight" invisible="state != 'collected'"/>
|
||||
<button name="action_start_analysis" string="Iniciar Análisis" type="object" class="oe_highlight" invisible="state != 'received'"/>
|
||||
<button name="action_complete_analysis" string="Completar Análisis" type="object" class="oe_highlight" invisible="state != 'in_process'"/>
|
||||
<button name="action_store" string="Almacenar" type="object" invisible="state not in ['analyzed', 'in_process', 'received']"/>
|
||||
<button name="action_dispose" string="Desechar" type="object" invisible="state == 'disposed'"/>
|
||||
<button name="action_open_rejection_wizard"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-danger"
|
||||
invisible="state in ['completed', 'rejected', 'disposed', 'cancelled']"/>
|
||||
<button name="action_cancel" string="Cancelar" type="object" invisible="state in ['cancelled', 'rejected', 'disposed']"/>
|
||||
<button name="action_create_resample"
|
||||
string="Crear Re-muestra"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'rejected' or resample_count >= 3"
|
||||
confirm="¿Está seguro de que desea crear una re-muestra para esta muestra rechazada?"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored,rejected"/>
|
||||
</header>
|
||||
<form string="Lab Sample">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
|
@ -57,126 +32,23 @@
|
|||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="patient_id" readonly="state not in ['pending_collection', 'collected']"/>
|
||||
<field name="doctor_id" readonly="state not in ['pending_collection', 'collected']"/>
|
||||
<field name="origin" readonly="1"/>
|
||||
<field name="request_id"
|
||||
readonly="state not in ['pending_collection', 'collected']"
|
||||
domain="[('is_lab_request', '=', True), '|', ('partner_id', '=', False), ('partner_id', '=', patient_id)]"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="request_id"/>
|
||||
<field name="product_id"
|
||||
string="Sample Type"
|
||||
domain="[('is_sample_type', '=', True)]"
|
||||
options="{'no_create': True, 'no_create_edit': True}"
|
||||
readonly="state not in ['pending_collection', 'collected']"/>
|
||||
options="{'no_create': True, 'no_create_edit': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="barcode" readonly="1"/>
|
||||
<field name="collection_date" readonly="state not in ['pending_collection', 'collected']"/>
|
||||
<field name="collector_id" readonly="state not in ['pending_collection', 'collected']"/>
|
||||
<field name="sample_type_product_id"
|
||||
readonly="state not in ['pending_collection', 'collected']"
|
||||
options="{'no_create': True, 'no_create_edit': True}"/>
|
||||
<field name="volume_ml" readonly="1"/>
|
||||
<field name="analysis_names" readonly="1"/>
|
||||
<field name="container_type"
|
||||
readonly="state not in ['pending_collection', 'collected']"
|
||||
invisible="sample_type_product_id != False"/>
|
||||
<field name="collection_date"/>
|
||||
<field name="collector_id"/>
|
||||
<field name="container_type"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información de Rechazo" invisible="state != 'rejected'" col="4">
|
||||
<field name="rejection_reason_id" readonly="1"/>
|
||||
<field name="rejected_by" readonly="1"/>
|
||||
<field name="rejection_date" readonly="1"/>
|
||||
<field name="rejection_notes" readonly="1" colspan="4"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
|
||||
<group col="4">
|
||||
<field name="is_resample" invisible="1"/>
|
||||
<field name="resample_count" invisible="1"/>
|
||||
<field name="parent_sample_id" readonly="1" invisible="not is_resample"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}"/>
|
||||
<field name="root_sample_id" readonly="1" invisible="not is_resample"/>
|
||||
<field name="resample_chain_count" readonly="1" invisible="resample_chain_count == 0"/>
|
||||
</group>
|
||||
<group string="Re-muestras Generadas" invisible="resample_count == 0">
|
||||
<field name="child_sample_ids" nolabel="1"
|
||||
context="{'form_view_ref': 'lims_management.view_lab_sample_form',
|
||||
'tree_view_ref': 'lims_management.view_lab_sample_list'}">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="collection_date"/>
|
||||
<field name="rejection_reason_id"/>
|
||||
<field name="resample_count" string="Re-muestras propias"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Información de Trazabilidad" invisible="not is_resample">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p><i class="fa fa-info-circle"/> Esta muestra es parte de una cadena de re-muestreo.</p>
|
||||
<p>Total de re-muestreos en la cadena: <field name="resample_chain_count" readonly="1" nolabel="1" class="oe_inline"/></p>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for Lab Samples -->
|
||||
<record id="view_lab_sample_search" model="ir.ui.view">
|
||||
<field name="name">lab.sample.search</field>
|
||||
<field name="model">stock.lot</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Muestras">
|
||||
<field name="name" string="Código"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="barcode"/>
|
||||
<field name="analysis_names"/>
|
||||
<filter string="Pendientes" name="pending" domain="[('state', 'in', ['pending_collection', 'collected', 'received'])]"/>
|
||||
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
|
||||
<filter string="Analizadas" name="analyzed" domain="[('state', '=', 'analyzed')]"/>
|
||||
<filter string="Rechazadas" name="rejected" domain="[('state', '=', 'rejected')]"/>
|
||||
<filter string="Re-muestras" name="resamples" domain="[('is_resample', '=', True)]"/>
|
||||
<filter string="Con Re-muestras" name="has_resamples" domain="[('resample_count', '>', 0)]"/>
|
||||
<separator/>
|
||||
<filter string="Hoy" name="today" domain="[('collection_date', '>=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')), ('collection_date', '<=', datetime.datetime.now().strftime('%Y-%m-%d 23:59:59'))]"/>
|
||||
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '>=', (datetime.datetime.now() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Rechazadas - Alta Severidad" name="rejected_high"
|
||||
domain="[('state', '=', 'rejected'), ('rejection_reason_id.severity', 'in', ['high', 'critical'])]"/>
|
||||
<group expand="0" string="Agrupar por">
|
||||
<filter string="Estado" name="group_state" context="{'group_by': 'state'}"/>
|
||||
<filter string="Paciente" name="group_patient" context="{'group_by': 'patient_id'}"/>
|
||||
<filter string="Fecha de Recolección" name="group_collection" context="{'group_by': 'collection_date:day'}"/>
|
||||
<filter string="Motivo de Rechazo" name="group_rejection" context="{'group_by': 'rejection_reason_id'}"/>
|
||||
<filter string="Es Re-muestra" name="group_resample" context="{'group_by': 'is_resample'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Rejected Samples -->
|
||||
<record id="action_lab_sample_rejected" model="ir.actions.act_window">
|
||||
<field name="name">Muestras Rechazadas</field>
|
||||
<field name="res_model">stock.lot</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('is_lab_sample', '=', True), ('state', '=', 'rejected')]</field>
|
||||
<field name="context">{'search_default_rejected': 1, 'default_is_lab_sample': True}</field>
|
||||
<field name="search_view_id" ref="view_lab_sample_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No hay muestras rechazadas
|
||||
</p>
|
||||
<p>
|
||||
Las muestras rechazadas aparecerán aquí con información
|
||||
sobre el motivo del rechazo y las acciones tomadas.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import sample_rejection_wizard
|
|
@ -1,90 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class SampleRejectionWizard(models.TransientModel):
|
||||
_name = 'lims.sample.rejection.wizard'
|
||||
_description = 'Wizard para Rechazo de Muestras'
|
||||
|
||||
sample_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Muestra',
|
||||
required=True,
|
||||
readonly=True,
|
||||
domain=[('is_lab_sample', '=', True)]
|
||||
)
|
||||
|
||||
rejection_reason_id = fields.Many2one(
|
||||
'lims.rejection.reason',
|
||||
string='Motivo de Rechazo',
|
||||
required=True,
|
||||
domain=[('active', '=', True)]
|
||||
)
|
||||
|
||||
rejection_notes = fields.Text(
|
||||
string='Notas Adicionales',
|
||||
help="Información adicional sobre el rechazo"
|
||||
)
|
||||
|
||||
requires_new_sample = fields.Boolean(
|
||||
string='Requiere Nueva Muestra',
|
||||
related='rejection_reason_id.requires_new_sample',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
create_new_sample = fields.Boolean(
|
||||
string='Crear Nueva Solicitud',
|
||||
help="Crear automáticamente una nueva solicitud de muestra"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SampleRejectionWizard, self).default_get(fields)
|
||||
active_id = self.env.context.get('active_id')
|
||||
if active_id:
|
||||
sample = self.env['stock.lot'].browse(active_id)
|
||||
res['sample_id'] = sample.id
|
||||
return res
|
||||
|
||||
@api.onchange('rejection_reason_id')
|
||||
def _onchange_rejection_reason_id(self):
|
||||
if self.rejection_reason_id and self.rejection_reason_id.requires_new_sample:
|
||||
self.create_new_sample = True
|
||||
|
||||
def action_reject_sample(self):
|
||||
"""Reject the sample with the provided reason"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.sample_id:
|
||||
raise ValidationError('No se ha seleccionado ninguna muestra')
|
||||
|
||||
if self.sample_id.state == 'completed':
|
||||
raise ValidationError('No se puede rechazar una muestra ya completada')
|
||||
|
||||
# Update sample with rejection information
|
||||
self.sample_id.write({
|
||||
'rejection_reason_id': self.rejection_reason_id.id,
|
||||
'rejection_notes': self.rejection_notes
|
||||
})
|
||||
|
||||
# Call the rejection method on the sample with explicit resample creation preference
|
||||
self.sample_id.action_reject(create_resample=self.create_new_sample)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _create_new_sample_request(self):
|
||||
"""Create a new sample request based on the rejected one"""
|
||||
original_order = self.sample_id.request_id
|
||||
|
||||
# Create a note in the original order
|
||||
original_order.message_post(
|
||||
body=f'Se solicitará una nueva muestra debido al rechazo. Motivo: {self.rejection_reason_id.name}',
|
||||
subject='Nueva Muestra Solicitada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
# Here you could implement logic to create a new sale.order
|
||||
# or a specific request for a new sample
|
||||
# For now, we'll just add a note
|
||||
|
||||
return True
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for Sample Rejection Wizard -->
|
||||
<record id="view_lims_sample_rejection_wizard_form" model="ir.ui.view">
|
||||
<field name="name">lims.sample.rejection.wizard.form</field>
|
||||
<field name="model">lims.sample.rejection.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rechazar Muestra">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="sample_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="rejection_reason_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requires_new_sample" invisible="1"/>
|
||||
<field name="create_new_sample"
|
||||
invisible="not requires_new_sample"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Información Adicional">
|
||||
<field name="rejection_notes" placeholder="Agregue cualquier información relevante sobre el rechazo..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_reject_sample"
|
||||
string="Rechazar Muestra"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
confirm="¿Está seguro de rechazar esta muestra? Esta acción no se puede deshacer."/>
|
||||
<button string="Cancelar" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Sample Rejection Wizard -->
|
||||
<record id="action_lims_sample_rejection_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Rechazar Muestra</field>
|
||||
<field name="res_model">lims.sample.rejection.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_sample_id': active_id}</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para asignar el usuario admin al grupo de Administrador de Laboratorio
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# Buscar el usuario admin
|
||||
admin_user = env['res.users'].search([('login', '=', 'admin')], limit=1)
|
||||
if not admin_user:
|
||||
_logger.error("No se encontró el usuario admin")
|
||||
exit(1)
|
||||
|
||||
# Buscar el grupo de Administrador de Laboratorio
|
||||
try:
|
||||
lab_admin_group = env.ref('lims_management.group_lims_admin')
|
||||
except ValueError:
|
||||
_logger.error("No se encontró el grupo de Administrador de Laboratorio")
|
||||
exit(1)
|
||||
|
||||
# Verificar si el usuario ya está en el grupo
|
||||
if lab_admin_group in admin_user.groups_id:
|
||||
_logger.info("El usuario admin ya está en el grupo de Administrador de Laboratorio")
|
||||
else:
|
||||
# Agregar el usuario al grupo
|
||||
admin_user.write({
|
||||
'groups_id': [(4, lab_admin_group.id)]
|
||||
})
|
||||
_logger.info("Usuario admin agregado exitosamente al grupo de Administrador de Laboratorio")
|
||||
|
||||
# Confirmar los grupos del usuario
|
||||
group_names = ', '.join(admin_user.groups_id.mapped('name'))
|
||||
_logger.info(f"Grupos del usuario admin: {group_names}")
|
||||
|
||||
env.cr.commit()
|
||||
_logger.info("Cambios guardados exitosamente")
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Error al asignar usuario admin al grupo: {str(e)}")
|
||||
exit(1)
|