Compare commits

..

No commits in common. "dev" and "feature/6-lab-requests" have entirely different histories.

144 changed files with 243 additions and 17688 deletions

View File

@ -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
View File

@ -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

Binary file not shown.

476
CLAUDE.md
View File

@ -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&#205;NICO</h4>
```
- í = &#237;
- Í = &#205;
- á = &#225;
- Á = &#193;
- é = &#233;
- É = &#201;
- ó = &#243;
- Ó = &#211;
- ú = &#250;
- Ú = &#218;
- ñ = &#241;
- Ñ = &#209;
##### Layout de etiquetas múltiples por página
```xml
<!-- Contenedor principal sin salto de página -->
<div class="page">
<t t-foreach="docs" t-as="o">
<!-- Cada etiqueta como inline-block -->
<div style="display: inline-block; vertical-align: top;
page-break-inside: avoid; overflow: hidden;">
<!-- Contenido de la etiqueta -->
</div>
</t>
</div>
```

202
GEMINI.md
View File

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

View File

@ -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."

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because it is too large Load Diff

View File

@ -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.
---

View File

@ -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

View File

@ -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

View File

@ -1,57 +0,0 @@
# Plan de Actividades: Issue #7 - Gestión de Muestras de Laboratorio
## Objetivo
Extender el modelo de Lotes/Números de Serie de Odoo (`stock.lot`) para representar y gestionar las **Muestras de Laboratorio**. Esto permitirá la trazabilidad completa de la muestra desde su recolección hasta el análisis.
## TODO
- [x] **Extender el Modelo de Lote/Número de Serie (`stock.lot`):**
- [x] Crear el archivo `lims_management/models/stock_lot.py`.
- [x] Heredar del modelo `stock.lot`.
- [x] Añadir campos: `is_lab_sample`, `patient_id`, `request_id`, `collection_date`, `container_type`.
- [ ] **(Nuevo)** Añadir campo `collector_id` (Many2one a `res.users`) para registrar quién tomó la muestra.
- [x] **Adaptar las Vistas de Lote/Número de Serie:**
- [x] Crear el archivo `lims_management/views/stock_lot_views.xml`.
- [x] Crear vistas de lista y formulario para las muestras.
- [x] Crear un producto de servicio por defecto para las muestras.
- [ ] **(Nuevo)** Añadir el campo `collector_id` a las vistas de lista y formulario.
- [x] **Crear el Menú "Gestión de Muestras":**
- [x] Modificar `lims_management/views/menus.xml`.
- [x] Crear acción de ventana y `menuitem` para `stock.lot` con el dominio y contexto adecuados.
- [x] **Establecer Permisos y Reglas de Dominio:**
- [x] Modificar `lims_management/security/ir.model.access.csv` para dar permisos sobre `stock.lot`.
- [x] Añadir dominios en las vistas para los campos relacionales.
- [x] **Actualizar el Manifiesto (`__manifest__.py`):**
- [x] Añadir nuevos archivos de modelos, vistas y datos al manifiesto.
- [x] **Verificación Final:**
- [x] Reiniciar y verificar la instancia de Odoo.
- [x] **Mejorar Modelo de Productos para Tipos de Muestra:**
- [x] Añadir un campo booleano `is_sample_type` al modelo `product.template`.
- [x] **Crear Menú para "Tipos de Muestra":**
- [x] Añadir acción de ventana y `menuitem` para los tipos de muestra.
- [x] **Actualizar Vista de Muestras (`stock.lot`):**
- [x] Hacer visible y aplicar dominio al campo `product_id` (Tipo de Muestra).
- [x] Eliminar el producto genérico y su referencia en el contexto.
- [x] **Crear Datos de Demostración:**
- [x] Crear archivo `demo/z_sample_demo.xml` con tipos de muestra y muestras de ejemplo.
- [x] Añadir el archivo de demostración al manifiesto.
- [ ] **(Nuevo)** Actualizar los datos de demostración para incluir el `collector_id`.
- [x] **Verificación Final (con Demo):**
- [x] Validar la funcionalidad completa con los datos de demostración.
---
## Consideraciones Futuras (Siguientes Issues)
- **Ciclo de Vida de la Muestra:** Implementar un campo de estado (`state`) con su lógica de transiciones (ej. 'Recolectada' -> 'Recibida' -> 'En Proceso' -> 'Completada' -> 'Almacenada').
- **Informes de Muestras:** Crear informes en PDF o vistas dinámicas sobre el estado y trazabilidad de las muestras.

View File

@ -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

View File

@ -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.

View File

@ -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: `&#205;` para Í, `&#241;` 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

View File

@ -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:
@ -258,4 +104,4 @@ except FileNotFoundError:
sys.exit(1)
except Exception as e:
print(f"Ocurrió un error inesperado al ejecutar Odoo: {e}")
sys.exit(1)
sys.exit(1)

View File

@ -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.

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizards

View File

@ -16,51 +16,20 @@
'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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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: &lt;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: &lt;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: &lt;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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Muestras de Laboratorio (Lotes) con el nuevo campo sample_type_product_id -->
<record id="lab_sample_01" model="stock.lot">
<field name="name">SAM-2025-00001</field>
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_serum_tube').product_variant_id.id"/>
<field name="is_lab_sample" eval="True"/>
<field name="patient_id" ref="lims_management.demo_patient_1"/>
<field name="collector_id" ref="base.user_admin"/>
<field name="collection_date" eval="(DateTime.now() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="sample_type_product_id" ref="lims_management.sample_type_serum_tube"/>
<field name="container_type">serum_tube</field>
<field name="state">received</field>
</record>
<record id="lab_sample_02" model="stock.lot">
<field name="name">SAM-2025-00002</field>
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_edta_tube').product_variant_id.id"/>
<field name="is_lab_sample" eval="True"/>
<field name="patient_id" ref="lims_management.demo_patient_2"/>
<field name="collector_id" ref="base.user_admin"/>
<field name="collection_date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="sample_type_product_id" ref="lims_management.sample_type_edta_tube"/>
<field name="container_type">edta_tube</field>
<field name="state">in_process</field>
</record>
<record id="lab_sample_03" model="stock.lot">
<field name="name">SAM-2025-00003</field>
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_urine_container').product_variant_id.id"/>
<field name="is_lab_sample" eval="True"/>
<field name="patient_id" ref="lims_management.demo_patient_3"/>
<field name="collector_id" ref="base.user_admin"/>
<field name="collection_date" eval="(DateTime.now() - timedelta(hours=6)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="sample_type_product_id" ref="lims_management.sample_type_urine_container"/>
<field name="container_type">urine</field>
<field name="state">collected</field>
</record>
<record id="lab_sample_04" model="stock.lot">
<field name="name">SAM-2025-00004</field>
<field name="product_id" model="product.product" eval="obj().env.ref('lims_management.sample_type_citrate_tube').product_variant_id.id"/>
<field name="is_lab_sample" eval="True"/>
<field name="patient_id" ref="lims_management.demo_patient_1"/>
<field name="collector_id" ref="base.user_admin"/>
<field name="collection_date" eval="(DateTime.now() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="sample_type_product_id" ref="lims_management.sample_type_citrate_tube"/>
<field name="state">analyzed</field>
</record>
</data>
</odoo>

View File

@ -1,13 +1,5 @@
# -*- coding: utf-8 -*-
from . import analysis_parameter
from . import product_template_parameter
from . import parameter_range
from . import product
from . import partner
from . import sale_order
from . import stock_lot
from . import rejection_reason
from . import lims_test
from . import lims_result
from . import res_config_settings
from . import lims_config
from . import product
from . import analysis_range
from . import sale_order

View File

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

View 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")

View File

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

View File

@ -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 = '' 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)

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from odoo import models, fields
class ProductTemplate(models.Model):
_inherit = 'product.template'
@ -22,38 +21,8 @@ class ProductTemplate(models.Model):
string="Especificaciones Técnicas"
)
parameter_ids = fields.One2many(
'product.template.parameter',
'product_tmpl_id',
string="Parámetros del Análisis",
help="Parámetros que se medirán en este análisis"
value_range_ids = fields.One2many(
'lims.analysis.range',
'analysis_id',
string="Rangos de Referencia"
)
is_sample_type = fields.Boolean(
string="Es Tipo de Muestra",
help="Marcar si este producto representa un tipo de contenedor de muestra de laboratorio."
)
required_sample_type_id = fields.Many2one(
'product.template',
string='Tipo de Muestra Requerida',
domain="[('is_sample_type', '=', True)]",
help="Tipo de muestra/contenedor requerido para realizar este análisis"
)
sample_volume_ml = fields.Float(
string='Volumen Requerido (ml)',
help="Volumen de muestra requerido en mililitros para realizar este análisis"
)
@api.constrains('required_sample_type_id', 'is_analysis')
def _check_sample_type_for_analysis(self):
for product in self:
if product.required_sample_type_id and not product.is_analysis:
raise ValidationError("Solo los productos marcados como 'Es un Análisis Clínico' pueden tener un tipo de muestra requerida.")
@api.constrains('sample_volume_ml', 'is_analysis')
def _check_volume_for_analysis(self):
for product in self:
if product.sample_volume_ml and not product.is_analysis:
raise ValidationError("Solo los productos marcados como 'Es un Análisis Clínico' pueden tener un volumen requerido.")

View File

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

View File

@ -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!'),
]

View File

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

View File

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

View File

@ -1,602 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from datetime import datetime
import random
class StockLot(models.Model):
_name = 'stock.lot'
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio')
barcode = fields.Char(
string='Código de Barras',
compute='_compute_barcode',
store=True,
readonly=True,
help="Código de barras único para la muestra en formato YYMMDDNNNNNNC"
)
patient_id = fields.Many2one(
'res.partner',
string='Paciente',
domain="[('is_patient', '=', True)]"
)
request_id = fields.Many2one(
'sale.order',
string='Orden de Laboratorio',
domain="[('is_lab_request', '=', True)]"
)
collection_date = fields.Datetime(string='Fecha de Recolección')
container_type = fields.Selection([
('serum_tube', 'Tubo de Suero'),
('edta_tube', 'Tubo EDTA'),
('swab', 'Hisopo'),
('urine', 'Contenedor de Orina'),
('other', 'Otro')
], string='Tipo de Contenedor (Obsoleto)', help='Campo obsoleto, use sample_type_product_id en su lugar')
sample_type_product_id = fields.Many2one(
'product.template',
string='Tipo de Muestra',
domain="[('is_sample_type', '=', True)]",
help="Producto que representa el tipo de contenedor/muestra"
)
collector_id = fields.Many2one(
'res.users',
string='Recolectado por',
default=lambda self: self.env.user
)
doctor_id = fields.Many2one(
'res.partner',
string='Médico Referente',
domain="[('is_doctor', '=', True)]",
help="Médico que ordenó los análisis"
)
origin = fields.Char(
string='Origen',
help="Referencia a la orden de laboratorio que generó esta muestra"
)
volume_ml = fields.Float(
string='Volumen (ml)',
help="Volumen total de muestra requerido"
)
analysis_names = fields.Char(
string='Análisis',
help="Lista de análisis que se realizarán con esta muestra"
)
state = fields.Selection([
('pending_collection', 'Pendiente de Recolección'),
('collected', 'Recolectada'),
('received', 'Recibida en Laboratorio'),
('in_process', 'En Proceso'),
('analyzed', 'Analizada'),
('stored', 'Almacenada'),
('disposed', 'Desechada'),
('cancelled', 'Cancelada'),
('rejected', 'Rechazada')
], string='Estado', default='collected', tracking=True)
# Rejection fields
rejection_reason_id = fields.Many2one(
'lims.rejection.reason',
string='Motivo de Rechazo',
tracking=True
)
rejection_notes = fields.Text(
string='Notas de Rechazo',
help="Información adicional sobre el rechazo"
)
rejected_by = fields.Many2one(
'res.users',
string='Rechazado por',
readonly=True
)
rejection_date = fields.Datetime(
string='Fecha de Rechazo',
readonly=True
)
# Re-sampling fields
parent_sample_id = fields.Many2one(
'stock.lot',
string='Muestra Original',
help='Muestra original de la cual esta es un re-muestreo',
domain="[('is_lab_sample', '=', True)]"
)
child_sample_ids = fields.One2many(
'stock.lot',
'parent_sample_id',
string='Re-muestras',
help='Muestras generadas como re-muestreo de esta'
)
resample_count = fields.Integer(
string='Número de Re-muestreo',
help='Indica cuántas veces se ha re-muestreado esta muestra',
compute='_compute_resample_count',
store=True
)
is_resample = fields.Boolean(
string='Es Re-muestra',
compute='_compute_is_resample',
store=True
)
root_sample_id = fields.Many2one(
'stock.lot',
string='Muestra Original (Raíz)',
compute='_compute_root_sample',
store=True,
help='Muestra original de la cadena de re-muestreos'
)
resample_chain_count = fields.Integer(
string='Re-muestreos en Cadena',
compute='_compute_resample_chain_count',
help='Número total de re-muestreos en toda la cadena'
)
def action_collect(self):
"""Mark sample(s) as collected"""
for record in self:
old_state = record.state
record.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
record.message_post(
body='Muestra recolectada por %s' % self.env.user.name,
subject='Estado actualizado: Recolectada',
message_type='notification'
)
def action_receive(self):
"""Mark sample(s) as received in laboratory"""
for record in self:
old_state = record.state
record.write({'state': 'received'})
record.message_post(
body='Muestra recibida en laboratorio por %s' % self.env.user.name,
subject='Estado actualizado: Recibida',
message_type='notification'
)
def action_start_analysis(self):
"""Start analysis process"""
for record in self:
old_state = record.state
record.write({'state': 'in_process'})
record.message_post(
body='Análisis iniciado por %s' % self.env.user.name,
subject='Estado actualizado: En Proceso',
message_type='notification'
)
def action_complete_analysis(self):
"""Mark analysis as completed"""
for record in self:
old_state = record.state
record.write({'state': 'analyzed'})
record.message_post(
body='Análisis completado por %s' % self.env.user.name,
subject='Estado actualizado: Analizada',
message_type='notification'
)
def action_store(self):
"""Store the sample(s)"""
for record in self:
old_state = record.state
record.write({'state': 'stored'})
record.message_post(
body='Muestra almacenada por %s' % self.env.user.name,
subject='Estado actualizado: Almacenada',
message_type='notification'
)
def action_dispose(self):
"""Dispose of the sample(s)"""
for record in self:
old_state = record.state
record.write({'state': 'disposed'})
record.message_post(
body='Muestra desechada por %s. Motivo de disposición registrado.' % self.env.user.name,
subject='Estado actualizado: Desechada',
message_type='notification'
)
def action_cancel(self):
"""Cancel the sample(s)"""
for record in self:
old_state = record.state
record.write({'state': 'cancelled'})
record.message_post(
body='Muestra cancelada por %s' % self.env.user.name,
subject='Estado actualizado: Cancelada',
message_type='notification'
)
def action_open_rejection_wizard(self):
"""Open the rejection wizard"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Rechazar Muestra',
'res_model': 'lims.sample.rejection.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_sample_id': self.id,
}
}
def action_reject(self, create_resample=None):
"""Reject the sample - to be called from wizard
Args:
create_resample: Boolean to force resample creation. If None, uses system config
"""
self.ensure_one()
if self.state == 'completed':
raise ValueError('No se puede rechazar una muestra ya completada')
# This method is called from the wizard, so rejection fields should already be set
self.write({
'state': 'rejected',
'rejected_by': self.env.user.id,
'rejection_date': fields.Datetime.now()
})
reason_name = self.rejection_reason_id.name if self.rejection_reason_id else 'Sin especificar'
notes = self.rejection_notes or ''
body = f'Muestra rechazada por {self.env.user.name}<br/>Motivo: {reason_name}'
if notes:
body += f'<br/>Notas: {notes}'
self.message_post(
body=body,
subject='Estado actualizado: Rechazada',
message_type='notification'
)
# Notify related sale order if exists
if self.request_id:
self.request_id.message_post(
body=f'La muestra {self.name} ha sido rechazada. Motivo: {reason_name}',
subject='Muestra Rechazada',
message_type='notification'
)
# Determine if we should create a resample
should_create_resample = False
if create_resample is not None:
# Explicit value from wizard
should_create_resample = create_resample
else:
# Check system configuration
IrConfig = self.env['ir.config_parameter'].sudo()
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
should_create_resample = auto_resample
if should_create_resample:
try:
# Create resample automatically
resample_action = self.action_create_resample()
self.message_post(
body=_('Re-muestra generada automáticamente debido al rechazo'),
subject='Re-muestreo Automático',
message_type='notification'
)
except UserError as e:
# If resample creation fails (e.g., max attempts reached), log it
self.message_post(
body=_('No se pudo generar re-muestra automática: %s') % str(e),
subject='Error en Re-muestreo',
message_type='notification'
)
@api.onchange('sample_type_product_id')
def _onchange_sample_type_product_id(self):
"""Synchronize container_type when sample_type_product_id changes"""
if self.sample_type_product_id:
# Try to map product name to legacy container type
product_name = self.sample_type_product_id.name.lower()
if 'suero' in product_name or 'serum' in product_name:
self.container_type = 'serum_tube'
elif 'edta' in product_name:
self.container_type = 'edta_tube'
elif 'hisopo' in product_name or 'swab' in product_name:
self.container_type = 'swab'
elif 'orina' in product_name or 'urine' in product_name:
self.container_type = 'urine'
else:
self.container_type = 'other'
def get_container_name(self):
"""Get container name from product or legacy field"""
if self.sample_type_product_id:
return self.sample_type_product_id.name
elif self.container_type:
return dict(self._fields['container_type'].selection).get(self.container_type)
return 'Unknown'
@api.depends('is_lab_sample', 'create_date')
def _compute_barcode(self):
"""Generate unique barcode for laboratory samples"""
for record in self:
if record.is_lab_sample and not record.barcode:
record.barcode = record._generate_unique_barcode()
elif not record.is_lab_sample:
record.barcode = False
def _generate_unique_barcode(self):
"""Generate a unique barcode in format YYMMDDNNNNNNC
YY: Year (2 digits)
MM: Month (2 digits)
DD: Day (2 digits)
NNNNNN: Sequential number (6 digits)
C: Check digit
"""
self.ensure_one()
now = datetime.now()
date_prefix = now.strftime('%y%m%d')
# Get the highest sequence number for today
domain = [
('is_lab_sample', '=', True),
('barcode', 'like', date_prefix + '%'),
('id', '!=', self.id)
]
max_barcode = self.search(domain, order='barcode desc', limit=1)
if max_barcode and max_barcode.barcode:
# Extract sequence number from existing barcode
try:
sequence = int(max_barcode.barcode[6:12]) + 1
except:
sequence = 1
else:
sequence = 1
# Ensure we don't exceed 6 digits
if sequence > 999999:
# Add prefix based on sample type to allow more barcodes
prefix_map = {
'suero': '1',
'edta': '2',
'orina': '3',
'hisopo': '4',
'other': '9'
}
type_prefix = '9' # default
if self.sample_type_product_id:
name_lower = self.sample_type_product_id.name.lower()
for key, val in prefix_map.items():
if key in name_lower:
type_prefix = val
break
sequence = int(type_prefix + str(sequence % 100000).zfill(5))
# Format sequence with leading zeros
sequence_str = str(sequence).zfill(6)
# Calculate check digit using Luhn algorithm
barcode_without_check = date_prefix + sequence_str
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
final_barcode = barcode_without_check + str(check_digit)
# Verify uniqueness
existing = self.search([
('barcode', '=', final_barcode),
('id', '!=', self.id)
], limit=1)
if existing:
# If collision, add random component and retry
sequence = sequence * 10 + random.randint(0, 9)
sequence_str = str(sequence % 1000000).zfill(6)
barcode_without_check = date_prefix + sequence_str
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
final_barcode = barcode_without_check + str(check_digit)
return final_barcode
def _calculate_luhn_check_digit(self, number_str):
"""Calculate Luhn check digit for barcode validation"""
digits = [int(d) for d in number_str]
odd_sum = sum(digits[-1::-2])
even_sum = sum([sum(divmod(2 * d, 10)) for d in digits[-2::-2]])
total = odd_sum + even_sum
return (10 - (total % 10)) % 10
def _ensure_barcode(self):
"""Ensure all lab samples have a barcode"""
for record in self:
if record.is_lab_sample and not record.barcode:
record.barcode = record._generate_unique_barcode()
return True
@api.depends('parent_sample_id')
def _compute_is_resample(self):
"""Compute if this sample is a resample"""
for record in self:
record.is_resample = bool(record.parent_sample_id)
@api.depends('child_sample_ids')
def _compute_resample_count(self):
"""Compute the number of times this sample has been resampled"""
for record in self:
record.resample_count = len(record.child_sample_ids)
@api.depends('parent_sample_id')
def _compute_root_sample(self):
"""Compute the root sample of the resample chain"""
for record in self:
root = record
while root.parent_sample_id:
root = root.parent_sample_id
record.root_sample_id = root if root != record else False
@api.depends('parent_sample_id', 'child_sample_ids')
def _compute_resample_chain_count(self):
"""Compute total resamples in the entire chain"""
for record in self:
# Find root sample
root = record
while root.parent_sample_id:
root = root.parent_sample_id
# Count all resamples from root
record.resample_chain_count = self._count_all_resamples_in_chain(root)
def action_create_resample(self):
"""Create a new sample as a resample of the current one"""
self.ensure_one()
# Determine the parent sample for the new resample
# If current sample is already a resample, use its parent
# Otherwise, use the current sample as parent
parent_for_resample = self.parent_sample_id if self.parent_sample_id else self
# Check if there's already an active resample for the parent
active_resamples = parent_for_resample.child_sample_ids.filtered(
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
)
if active_resamples:
raise UserError(_('La muestra %s ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') %
(parent_for_resample.name, ', '.join(active_resamples.mapped('name'))))
# Get configuration
IrConfig = self.env['ir.config_parameter'].sudo()
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
initial_state = IrConfig.get_param('lims_management.resample_state', 'pending_collection')
prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-')
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
# Find the original sample (root of the resample chain)
original_sample = parent_for_resample
while original_sample.parent_sample_id:
original_sample = original_sample.parent_sample_id
# Count all resamples in the chain
total_resamples = self._count_all_resamples_in_chain(original_sample)
# Check maximum resample attempts based on the entire chain
if max_attempts > 0 and total_resamples >= max_attempts:
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta cadena de muestras.') % max_attempts)
# Calculate resample number for naming (based on parent's resample count)
resample_number = len(parent_for_resample.child_sample_ids) + 1
# Prepare values for new sample
vals = {
'name': f"{prefix}{parent_for_resample.name}-{resample_number}",
'product_id': self.product_id.id,
'patient_id': self.patient_id.id,
'doctor_id': self.doctor_id.id,
'origin': self.origin,
'sample_type_product_id': self.sample_type_product_id.id,
'volume_ml': self.volume_ml,
'is_lab_sample': True,
'state': initial_state,
'analysis_names': self.analysis_names,
'parent_sample_id': parent_for_resample.id, # Always use the determined parent
'request_id': self.request_id.id if self.request_id else False,
}
# Create the resample
resample = self.create(vals)
# Post message in all relevant samples
self.message_post(
body=_('Re-muestra creada: %s') % resample.name,
subject='Re-muestreo',
message_type='notification'
)
if self != parent_for_resample:
# If we're creating from a resample, also notify the parent
parent_for_resample.message_post(
body=_('Nueva re-muestra creada: %s (debido al rechazo de %s)') % (resample.name, self.name),
subject='Re-muestreo',
message_type='notification'
)
resample.message_post(
body=_('Esta es una re-muestra de: %s<br/>Creada debido al rechazo de: %s<br/>Motivo: %s') %
(parent_for_resample.name, self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'),
subject='Re-muestra creada',
message_type='notification'
)
# Notify receptionist if configured
auto_notify = IrConfig.get_param('lims_management.auto_notify_resample', 'True') == 'True'
if auto_notify:
self._notify_resample_created(resample)
# If there's a related order, update it
if self.request_id:
self.request_id.message_post(
body=_('Se ha creado una re-muestra (%s) para la muestra rechazada %s') % (resample.name, self.name),
subject='Re-muestra creada',
message_type='notification'
)
# Add the new sample to the order's generated samples
self.request_id.generated_sample_ids = [(4, resample.id)]
return {
'type': 'ir.actions.act_window',
'name': 'Re-muestra Creada',
'res_model': 'stock.lot',
'res_id': resample.id,
'view_mode': 'form',
'target': 'current',
}
def _count_all_resamples_in_chain(self, root_sample):
"""Count all resamples in the entire chain starting from root"""
count = 0
samples_to_check = [root_sample]
while samples_to_check:
sample = samples_to_check.pop(0)
# Add all child samples to the check list
for child in sample.child_sample_ids:
count += 1
samples_to_check.append(child)
return count
def _notify_resample_created(self, resample):
"""Notify receptionist users about the created resample"""
# Find receptionist users
receptionist_group = self.env.ref('lims_management.group_lims_receptionist', raise_if_not_found=False)
if receptionist_group:
receptionist_users = receptionist_group.users
# Get the model id for stock.lot
model_id = self.env['ir.model'].search([('model', '=', 'stock.lot')], limit=1).id
# Create activities for receptionists
for user in receptionist_users:
self.env['mail.activity'].create({
'res_model': 'stock.lot',
'res_model_id': model_id, # Campo obligatorio
'res_id': resample.id,
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'summary': _('Nueva re-muestra pendiente de recolección'),
'note': _('Se ha generado una re-muestra (%s) que requiere recolección. Muestra original: %s') %
(resample.name, self.name),
'user_id': user.id,
'date_deadline': fields.Date.today(),
})

View File

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@ -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&#205;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&#225;lisis:</strong> <span t-field="o.analysis_names"/></div>
</div>
</div>
</t>
</div>
</t>
</template>
</data>
</odoo>

View File

@ -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&#205;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&#243;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&#241;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&#250;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&#233;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&#193;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&#237;tico que requiere atenci&#243;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&#243;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&#225; dirigido exclusivamente al paciente y/o m&#233;dico tratante.</p>
<p>Los resultados se relacionan &#250;nicamente con las muestras analizadas.</p>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -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>

View File

@ -1,26 +1,3 @@
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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_lims_analysis_parameter_user access_lims_analysis_range_user lims.analysis.parameter.user lims.analysis.range.user model_lims_analysis_parameter model_lims_analysis_range base.group_user 1 0 1 0 1 0 1
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
3 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

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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')

View File

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

View File

@ -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, '')

View File

@ -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>

View File

@ -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>

View File

@ -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 &#211;rdenes" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Estado de Órdenes -->
<record id="view_lab_order_dashboard_pivot" model="ir.ui.view">
<field name="name">sale.order.lab.dashboard.pivot</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis de &#211;rdenes">
<field name="date_order" interval="month" type="col"/>
<field name="state" type="row"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Estado de Órdenes -->
<record id="action_lab_order_dashboard" model="ir.actions.act_window">
<field name="name">Estado de &#211;rdenes</field>
<field name="res_model">sale.order</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('is_lab_request', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_lab_order_dashboard_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_lab_order_dashboard_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay &#243;rdenes de laboratorio registradas
</p>
<p>
Este dashboard muestra el estado actual de todas las &#243;rdenes de laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 2: Productividad de Técnicos
================================================================ -->
<!-- Vista Graph para Productividad de Técnicos -->
<record id="view_test_technician_productivity_graph" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Productividad de T&#233;cnicos" type="bar">
<field name="technician_id"/>
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Productividad de Técnicos -->
<record id="view_test_technician_productivity_pivot" model="ir.ui.view">
<field name="name">lims.test.technician.productivity.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por T&#233;cnico">
<field name="technician_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Productividad de Técnicos -->
<record id="action_technician_productivity_dashboard" model="ir.actions.act_window">
<field name="name">Productividad de T&#233;cnicos</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="context">{'search_default_group_by_technician': 1, 'search_default_this_month': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_technician_productivity_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_technician_productivity_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay pruebas registradas
</p>
<p>
Este dashboard muestra la productividad de cada t&#233;cnico del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 3: Estado de Muestras
================================================================ -->
<!-- Vista Graph para Estado de Muestras -->
<record id="view_sample_status_graph" model="ir.ui.view">
<field name="name">stock.lot.sample.status.graph</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<graph string="Estado de Muestras" type="pie">
<field name="state"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Muestras por Tipo -->
<record id="view_sample_type_pivot" model="ir.ui.view">
<field name="name">stock.lot.sample.type.pivot</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<pivot string="Muestras por Tipo">
<field name="sample_type_product_id" type="row"/>
<field name="state" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Muestras -->
<record id="action_sample_dashboard" model="ir.actions.act_window">
<field name="name">Dashboard de Muestras</field>
<field name="res_model">stock.lot</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('is_lab_sample', '=', True)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_sample_status_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_sample_type_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay muestras registradas
</p>
<p>
Este dashboard muestra el estado de todas las muestras del laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 4: Parámetros Fuera de Rango
================================================================ -->
<!-- Vista Graph para Parámetros Fuera de Rango -->
<record id="view_result_out_of_range_graph" model="ir.ui.view">
<field name="name">lims.result.out.of.range.graph</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<graph string="Par&#225;metros Fuera de Rango" type="bar">
<field name="parameter_id"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Resultados Críticos -->
<record id="view_result_critical_pivot" model="ir.ui.view">
<field name="name">lims.result.critical.pivot</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<pivot string="Resultados Cr&#237;ticos">
<field name="parameter_id" type="row"/>
<field name="is_critical" type="col"/>
<field name="is_out_of_range" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Parámetros Fuera de Rango -->
<record id="action_out_of_range_dashboard" model="ir.actions.act_window">
<field name="name">Par&#225;metros Fuera de Rango</field>
<field name="res_model">lims.result</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="domain">[('test_id.state', '=', 'validated')]</field>
<field name="context">{'search_default_out_of_range': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_result_out_of_range_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_result_critical_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay resultados fuera de rango
</p>
<p>
Este dashboard muestra los par&#225;metros que est&#225;n fuera de los rangos normales.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 5: Análisis Más Solicitados
================================================================ -->
<!-- Vista Graph para Top Análisis -->
<record id="view_top_analysis_graph" model="ir.ui.view">
<field name="name">sale.order.line.top.analysis.graph</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<graph string="An&#225;lisis M&#225;s Solicitados" type="bar">
<field name="product_id"/>
<field name="product_uom_qty" type="measure"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Análisis por Período -->
<record id="view_analysis_period_pivot" model="ir.ui.view">
<field name="name">sale.order.line.analysis.period.pivot</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<pivot string="An&#225;lisis por Per&#237;odo">
<field name="create_date" interval="month" type="col"/>
<field name="product_id" type="row"/>
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Análisis Más Solicitados -->
<record id="action_top_analysis_dashboard" model="ir.actions.act_window">
<field name="name">An&#225;lisis M&#225;s Solicitados</field>
<field name="res_model">sale.order.line</field>
<field name="view_mode">graph,pivot,list</field>
<field name="domain">[('order_id.is_lab_request', '=', True), ('product_id.is_analysis', '=', True)]</field>
<field name="context">{'search_default_group_by_product': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_top_analysis_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_analysis_period_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay an&#225;lisis registrados
</p>
<p>
Este dashboard muestra los an&#225;lisis m&#225;s solicitados en el laboratorio.
</p>
</field>
</record>
<!-- ================================================================
DASHBOARD 6: Distribución de Tests por Demografía
================================================================ -->
<!-- Vista Graph para Distribución por Sexo -->
<record id="view_test_gender_distribution_graph" model="ir.ui.view">
<field name="name">lims.test.gender.distribution.graph</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<graph string="Distribuci&#243;n por G&#233;nero" type="pie">
<field name="patient_gender"/>
</graph>
</field>
</record>
<!-- Vista Pivot para Tests por Edad y Sexo -->
<record id="view_test_demographics_pivot" model="ir.ui.view">
<field name="name">lims.test.demographics.pivot</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<pivot string="Tests por Demograf&#237;a">
<field name="patient_age_range" type="row"/>
<field name="patient_gender" type="col"/>
</pivot>
</field>
</record>
<!-- Acción para Dashboard de Distribución Demográfica -->
<record id="action_test_demographics_dashboard" model="ir.actions.act_window">
<field name="name">Distribuci&#243;n Demogr&#225;fica de Tests</field>
<field name="res_model">lims.test</field>
<field name="view_mode">graph,pivot,list</field>
<field name="domain">[('state', '=', 'validated')]</field>
<field name="context">{'search_default_this_year': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('view_test_gender_distribution_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('view_test_demographics_pivot')})]"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No hay tests validados
</p>
<p>
Este dashboard muestra la distribuci&#243;n de tests por caracter&#237;sticas demogr&#225;ficas de los pacientes.
</p>
</field>
</record>
<!-- ================================================================
FILTROS DE BÚSQUEDA PARA DASHBOARDS
================================================================ -->
<!-- Filtros para Tests -->
<record id="view_lims_test_dashboard_search" model="ir.ui.view">
<field name="name">lims.test.dashboard.search</field>
<field name="model">lims.test</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Estado -->
<filter string="En Proceso" name="in_process" domain="[('state', '=', 'in_process')]"/>
<filter string="Validados" name="validated" domain="[('state', '=', 'validated')]"/>
<!-- Filtros de Tiempo -->
<filter string="Hoy" name="today" domain="[('create_date', '&gt;=', context_today())]"/>
<filter string="Esta Semana" name="this_week" domain="[('create_date', '&gt;=', (context_today() + relativedelta(days=-7)).strftime('%Y-%m-%d'))]"/>
<filter string="Este Mes" name="this_month" domain="[('create_date', '&gt;=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
<filter string="Este A&#241;o" name="this_year" domain="[('create_date', '&gt;=', (context_today() + relativedelta(month=1, day=1)).strftime('%Y-%m-%d'))]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="T&#233;cnico" name="group_by_technician" context="{'group_by': 'technician_id'}"/>
<filter string="Estado" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Paciente" name="group_by_patient" context="{'group_by': 'patient_id'}"/>
<filter string="An&#225;lisis" name="group_by_product" context="{'group_by': 'product_id'}"/>
<filter string="Fecha" name="group_by_date" context="{'group_by': 'create_date:month'}"/>
</group>
</search>
</field>
</record>
<!-- Filtros para Resultados -->
<record id="view_lims_result_dashboard_search" model="ir.ui.view">
<field name="name">lims.result.dashboard.search</field>
<field name="model">lims.result</field>
<field name="arch" type="xml">
<search>
<!-- Filtros de Rango -->
<filter string="Fuera de Rango" name="out_of_range" domain="[('is_out_of_range', '=', True)]"/>
<filter string="Cr&#237;ticos" name="critical" domain="[('is_critical', '=', True)]"/>
<!-- Agrupaciones -->
<group expand="0" string="Agrupar Por">
<filter string="Par&#225;metro" name="group_by_parameter" context="{'group_by': 'parameter_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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','&gt;=',(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>

View File

@ -75,145 +75,6 @@
action="action_lims_lab_request"
sequence="15"/>
<!-- Acción de Ventana para Muestras de Laboratorio -->
<record id="action_lims_lab_sample" model="ir.actions.act_window">
<field name="name">Muestras de Laboratorio</field>
<field name="res_model">stock.lot</field>
<field name="view_mode">list,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_lab_sample_list')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('view_lab_sample_form')})]"/>
<field name="domain">[('is_lab_sample', '=', True)]</field>
<field name="context" eval="{
'default_is_lab_sample': True
}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crea una nueva muestra de laboratorio
</p>
</field>
</record>
<!-- Menú para Muestras de Laboratorio -->
<menuitem
id="lims_menu_lab_samples"
name="Muestras"
parent="lims_menu_root"
action="action_lims_lab_sample"
sequence="16"/>
<!-- Menú para Muestras Rechazadas -->
<menuitem
id="lims_menu_lab_samples_rejected"
name="Muestras Rechazadas"
parent="lims_menu_root"
action="action_lab_sample_rejected"
sequence="17"/>
<!-- Submenú de Laboratorio -->
<menuitem
id="lims_menu_laboratory"
name="Laboratorio"
parent="lims_menu_root"
sequence="20"/>
<!-- Acción para lims.test -->
<record id="action_lims_test" model="ir.actions.act_window">
<field name="name">Pruebas de Laboratorio</field>
<field name="res_model">lims.test</field>
<field name="view_mode">list,kanban,form</field>
<field name="context">{'search_default_my_tests': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear primera prueba de laboratorio
</p>
<p>
Aquí podrá gestionar las pruebas de laboratorio,
ingresar resultados y validarlos.
</p>
</field>
</record>
<!-- Menú para Pruebas -->
<menuitem id="menu_lims_tests"
name="Pruebas"
parent="lims_menu_laboratory"
action="action_lims_test"
sequence="10"/>
<!-- Menú para Ingreso de Resultados -->
<menuitem id="menu_lims_result_entry"
name="Ingreso de Resultados"
parent="lims_menu_laboratory"
action="action_lims_result_entry"
sequence="25"/>
<!-- Menú para Resultados -->
<menuitem id="menu_lims_result"
name="Resultados"
parent="lims_menu_laboratory"
action="action_lims_result"
sequence="30"/>
<!-- Submenú de Dashboards -->
<menuitem
id="menu_lims_dashboards"
name="Dashboards"
parent="lims_menu_root"
sequence="85"
groups="lims_management.group_lims_admin"/>
<!-- Dashboards individuales -->
<menuitem id="menu_lab_order_dashboard"
name="Estado de &#211;rdenes"
parent="menu_lims_dashboards"
action="action_lab_order_dashboard"
sequence="10"/>
<menuitem id="menu_technician_productivity_dashboard"
name="Productividad de T&#233;cnicos"
parent="menu_lims_dashboards"
action="action_technician_productivity_dashboard"
sequence="20"/>
<menuitem id="menu_sample_dashboard"
name="Dashboard de Muestras"
parent="menu_lims_dashboards"
action="action_sample_dashboard"
sequence="30"/>
<menuitem id="menu_out_of_range_dashboard"
name="Par&#225;metros Fuera de Rango"
parent="menu_lims_dashboards"
action="action_out_of_range_dashboard"
sequence="40"/>
<menuitem id="menu_top_analysis_dashboard"
name="An&#225;lisis M&#225;s Solicitados"
parent="menu_lims_dashboards"
action="action_top_analysis_dashboard"
sequence="50"/>
<menuitem id="menu_test_demographics_dashboard"
name="Distribuci&#243;n Demogr&#225;fica"
parent="menu_lims_dashboards"
action="action_test_demographics_dashboard"
sequence="60"/>
<!-- Submenú de Reportes -->
<menuitem
id="lims_menu_reports"
name="Reportes"
parent="lims_menu_root"
sequence="90"/>
<!-- Menú para Análisis de Resultados en Reportes -->
<menuitem id="menu_lims_result_analysis"
name="Análisis de Resultados"
parent="lims_menu_reports"
action="action_lims_result_analysis"
sequence="20"/>
<!-- Submenú de Configuración -->
<menuitem
id="lims_menu_config"
@ -247,89 +108,5 @@
parent="lims_menu_config"
action="action_lims_analysis_catalog"
sequence="10"/>
<!-- Acción de Ventana para Tipos de Muestra -->
<record id="action_lims_sample_type_catalog" model="ir.actions.act_window">
<field name="name">Tipos de Muestra</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,form</field>
<field name="domain">[('is_sample_type', '=', True)]</field>
<field name="context" eval="{
'default_is_sample_type': True,
'default_type': 'service'
}"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crea un nuevo tipo de muestra
</p>
</field>
</record>
<!-- Menú para Tipos de Muestra -->
<menuitem
id="lims_menu_sample_type_catalog"
name="Tipos de Muestra"
parent="lims_menu_config"
action="action_lims_sample_type_catalog"
sequence="20"/>
<!-- Acción para abrir configuración de laboratorio -->
<record id="action_lims_config_settings" model="ir.actions.act_window">
<field name="name">Configuración</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'lims_management'}</field>
</record>
<!-- Menú de Panel de Parámetros -->
<menuitem id="menu_lims_parameter_dashboard"
name="Panel de Parámetros"
parent="lims_menu_config"
action="action_lims_parameter_dashboard"
sequence="10"/>
<!-- Menú de Parámetros de Análisis -->
<menuitem id="menu_lims_analysis_parameter"
name="Parámetros de Análisis"
parent="lims_menu_config"
action="action_lims_analysis_parameter"
sequence="20"/>
<!-- Menú de Rangos de Referencia -->
<menuitem id="menu_lims_parameter_range"
name="Rangos de Referencia"
parent="lims_menu_config"
action="action_lims_parameter_range"
sequence="25"/>
<!-- Menú de Config. Parámetros-Análisis -->
<menuitem id="menu_product_template_parameter_config"
name="Config. Parámetros-Análisis"
parent="lims_menu_config"
action="action_product_template_parameter_config"
sequence="30"/>
<!-- Menú de Estadísticas -->
<menuitem id="menu_lims_parameter_statistics"
name="Estadísticas"
parent="lims_menu_config"
action="action_lims_parameter_statistics"
sequence="40"/>
<!-- Menú de Motivos de Rechazo -->
<menuitem id="menu_lims_rejection_reason"
name="Motivos de Rechazo"
parent="lims_menu_config"
action="action_lims_rejection_reason"
sequence="50"/>
<!-- Menú de configuración de ajustes -->
<menuitem id="menu_lims_config_settings"
name="Ajustes"
parent="lims_menu_config"
action="action_lims_config_settings"
sequence="100"/>
</data>
</odoo>

View File

@ -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>

View File

@ -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">&lt; </span>
<field name="critical_min" class="oe_inline"/>
<span class="oe_inline"> o &gt; </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 (&lt;18)" name="pediatric"
domain="[('age_min', '&lt;', 18)]"/>
<filter string="Adulto (18-65)" name="adult"
domain="[('age_min', '&gt;=', 18), ('age_max', '&lt;=', 65)]"/>
<filter string="Geriátrico (&gt;65)" name="geriatric"
domain="[('age_max', '&gt;', 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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,182 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Vista de Lista para Muestras de Laboratorio -->
<record id="view_lab_sample_list" model="ir.ui.view">
<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>
</field>
</record>
<!-- Vista de Formulario para Muestras de Laboratorio -->
<record id="view_lab_sample_form" model="ir.ui.view">
<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>
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</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="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']"/>
</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"/>
</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', '&gt;=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')), ('collection_date', '&lt;=', datetime.datetime.now().strftime('%Y-%m-%d 23:59:59'))]"/>
<filter string="Esta Semana" name="this_week" domain="[('collection_date', '&gt;=', (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>

View File

@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from . import sample_rejection_wizard

View File

@ -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

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More