Compare commits

..

No commits in common. "dev" and "feature/32-automatic-sample-generation" have entirely different histories.

131 changed files with 261 additions and 14985 deletions

View File

@ -15,21 +15,7 @@
"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:*)"
"Bash(ls:*)"
],
"deny": []
}

265
CLAUDE.md
View File

@ -2,11 +2,6 @@
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.
@ -21,7 +16,6 @@ This is a Laboratory Information Management System (LIMS) module for Odoo 18 ERP
## Development Commands
### Starting the Environment
```bash
# Start all services
docker-compose up -d
@ -36,47 +30,28 @@ 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`):
1. Create script (e.g., `verify_products.py`):
```python
import odoo
import json
@ -94,53 +69,42 @@ if __name__ == '__main__':
```
2. Copy to container:
```bash
docker cp test/verify_products.py lims_odoo:/tmp/verify_products.py
docker cp 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"
python 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"
python 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
python 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"
python 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
python gitea_cli_helper.py close-issue --issue-number 123
```
## 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
@ -151,37 +115,31 @@ At the start of each work session, read these documents to understand requiremen
### Odoo 18 Specific Conventions
#### View Definitions
- **CRITICAL**: Use `<list>` instead of `<tree>` in view XML - using `<tree>` causes error "El nodo raíz de una vista list debe ser <list>, no <tree>"
- View mode in actions must be `tree,form` not `list,form` (paradójicamente, el modo se llama "tree" pero el XML debe usar `<list>`)
- **CRITICAL**: Use `<list>` instead of `<tree>` - using `<tree>` causes `ValueError: Wrong value for ir.ui.view.type: 'tree'`
- View mode in actions must be `list,form` not `tree,form`
#### 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 -->
@ -191,15 +149,13 @@ At the start of each work session, read these documents to understand requiremen
```
### Data Management
- **Initial Data**: `lims_management/data/` - Sequences, categories, basic configuration
- **Demo Data**:
- **Demo Data**:
- XML files in `lims_management/demo/`
- Python scripts in `test/` directory for complex demo data creation
- Python scripts in root 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.
@ -207,7 +163,6 @@ At the start of each work session, read these documents to understand requiremen
## Environment Variables
Required in `.env` file:
- `GITEA_API_KEY`: Personal Access Token for Gitea
- `GITEA_API_KEY_URL`: Gitea API base URL (e.g., `https://gitea.grupoconsiti.com/api/v1/`)
- `GITEA_USERNAME`: Gitea username (repository owner)
@ -216,7 +171,6 @@ Required in `.env` file:
## Important Patterns
### Sample Lifecycle States
```python
STATE_PENDING_COLLECTION = 'pending_collection'
STATE_COLLECTED = 'collected'
@ -226,7 +180,6 @@ STATE_CANCELLED = 'cancelled'
```
### Barcode Generation
- 13-digit format: YYMMDDNNNNNNC
- Uses `barcode` Python library for Code-128 generation
- Stored as PDF with human-readable text
@ -234,34 +187,30 @@ STATE_CANCELLED = 'cancelled'
### Demo Data Creation
#### XML Files (Simple Data)
- Use for basic records without complex dependencies
- Place in `lims_management/demo/`
- Use `noupdate="1"` to prevent reloading
- **IMPORTANT**: Do NOT create sale.order records in XML demo files - use Python scripts instead
#### Python Scripts (Complex Data)
For data with dependencies or business logic:
#### Test Scripts
- **IMPORTANT**: Always create test scripts inside the `test/` folder within the project directory
- Example: `test/test_sample_generation.py`
- This ensures scripts are properly organized and accessible
1. Create script:
```python
import odoo
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Use ref() to get existing records
patient1 = env.ref('lims_management.demo_patient_1')
hemograma = env.ref('lims_management.analysis_hemograma')
# Create records with business logic
env['sale.order'].create({
'partner_id': patient1.id,
@ -285,192 +234,10 @@ if __name__ == '__main__':
## Git Workflow
### Pre-commit Hook
Automatically installed via `scripts/install_hooks.sh`:
- Prevents commits to 'main' or 'dev' branches
- Enforces feature branch workflow
### Branch Naming
- Feature branches: `feature/XX-description` (where XX is issue number)
- Always create PRs to 'dev' branch, not 'main'
## Desarrollo de nuevos modelos y vistas
### Orden de carga en **manifest**.py
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>
```
- Always create PRs to 'dev' branch, not 'main'

78
create_lab_requests.py Normal file
View File

@ -0,0 +1,78 @@
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
try:
# Get patients and doctors - using search instead of ref to be more robust
patient1 = env['res.partner'].search([('patient_identifier', '=', 'P-A87B01'), ('is_patient', '=', True)], limit=1)
patient2 = env['res.partner'].search([('patient_identifier', '=', 'P-C45D02'), ('is_patient', '=', True)], limit=1)
doctor1 = env['res.partner'].search([('doctor_license', '=', 'L-98765'), ('is_doctor', '=', True)], limit=1)
if not patient1:
print("Warning: Patient 1 not found, skipping lab requests creation")
return
# Get analysis products - using search instead of ref
hemograma = env['product.template'].search([('name', '=', 'Hemograma Completo'), ('is_analysis', '=', True)], limit=1)
perfil_lipidico = env['product.template'].search([('name', '=', 'Perfil Lipídico'), ('is_analysis', '=', True)], limit=1)
glucosa = env['product.template'].search([('name', '=', 'Glucosa en Sangre'), ('is_analysis', '=', True)], limit=1)
urocultivo = env['product.template'].search([('name', '=', 'Urocultivo'), ('is_analysis', '=', True)], limit=1)
# Create Lab Request 1 - Multiple analyses with same sample type
if patient1 and hemograma and perfil_lipidico:
order1 = env['sale.order'].create({
'partner_id': patient1.id,
'doctor_id': doctor1.id if doctor1 else False,
'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})
]
})
print(f"Created Lab Order 1: {order1.name}")
# Confirm the order to test automatic sample generation
order1.action_confirm()
print(f"Confirmed Lab Order 1. Generated samples: {len(order1.generated_sample_ids)}")
# Create Lab Request 2 - Different sample types
if patient2 and glucosa and urocultivo:
order2 = env['sale.order'].create({
'partner_id': patient2.id,
'is_lab_request': True,
'order_line': [
(0, 0, {'product_id': glucosa.product_variant_id.id, 'product_uom_qty': 1}),
(0, 0, {'product_id': urocultivo.product_variant_id.id, 'product_uom_qty': 1})
]
})
print(f"Created Lab Order 2: {order2.name}")
# Confirm to test automatic sample generation with different types
order2.action_confirm()
print(f"Confirmed Lab Order 2. Generated samples: {len(order2.generated_sample_ids)}")
except Exception as e:
print(f"Error creating lab requests: {str(e)}")
import traceback
traceback.print_exc()
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,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

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

@ -110,60 +110,6 @@ def close_issue(issue_number):
_make_gitea_request("PATCH", endpoint, payload)
print(f"Issue #{issue_number} cerrado exitosamente.")
def get_issue_details(issue_number):
"""Gets details and comments for a specific issue."""
# Get issue details
endpoint = f"repos/{GITEA_USERNAME}/{GITEA_REPO_NAME}/issues/{issue_number}"
print(f"Obteniendo detalles del issue #{issue_number}...")
try:
issue = _make_gitea_request("GET", endpoint)
# Display issue information
print(f"\n{'=' * 80}")
print(f"Issue #{issue.get('number', 'N/A')}: {issue.get('title', 'Sin título')}")
print(f"{'=' * 80}")
print(f"Estado: {'Abierto' if issue.get('state') == 'open' else 'Cerrado'}")
print(f"Autor: {issue.get('user', {}).get('login', 'Desconocido')}")
print(f"Creado: {issue.get('created_at', '').replace('T', ' ').split('+')[0] if issue.get('created_at') else 'N/A'}")
if issue.get('closed_at'):
print(f"Cerrado: {issue.get('closed_at', '').replace('T', ' ').split('+')[0]}")
labels = [label.get('name', '') for label in issue.get('labels', [])]
if labels:
print(f"Etiquetas: {', '.join(labels)}")
print(f"URL: {issue.get('html_url', 'N/A')}")
print(f"\nDescripción:")
print("-" * 40)
print(issue.get('body', 'Sin descripción'))
# Get comments
comments_endpoint = f"{endpoint}/comments"
comments = _make_gitea_request("GET", comments_endpoint)
if comments:
print(f"\nComentarios ({len(comments)}):")
print("-" * 40)
for i, comment in enumerate(comments, 1):
author = comment.get('user', {}).get('login', 'Desconocido')
created = comment.get('created_at', '').replace('T', ' ').split('+')[0] if comment.get('created_at') else 'N/A'
body = comment.get('body', '')
print(f"\nComentario {i} - {author} ({created}):")
print(body)
if i < len(comments):
print("-" * 40)
else:
print(f"\nNo hay comentarios en este issue.")
print(f"\n{'=' * 80}\n")
except Exception as e:
print(f"Error al obtener el issue #{issue_number}: {e}")
def list_open_issues():
"""Lists all open issues in the repository."""
endpoint = f"repos/{GITEA_USERNAME}/{GITEA_REPO_NAME}/issues"
@ -277,10 +223,6 @@ def main():
# Subparser para listar issues abiertos
list_issues_parser = subparsers.add_parser("list-open-issues", help="Lista todos los issues abiertos del repositorio.")
# Subparser para obtener detalles de un issue
get_issue_parser = subparsers.add_parser("get-issue", help="Obtiene detalles y comentarios de un issue específico.")
get_issue_parser.add_argument("--issue-number", type=int, required=True, help="Número del issue a consultar.")
args = parser.parse_args()
@ -296,8 +238,6 @@ def main():
merge_pull_request(args.pr_number, args.merge_method)
elif args.command == "list-open-issues":
list_open_issues()
elif args.command == "get-issue":
get_issue_details(args.issue_number)
else:
parser.print_help()

View File

@ -94,159 +94,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:

38
issue_content.txt Normal file
View File

@ -0,0 +1,38 @@
**Contexto:**
Para poder implementar la automatización de generación de muestras (Issue #32), es necesario establecer una relación entre los productos tipo análisis y los tipos de muestra que requieren.
**Problema Actual:**
- Los productos tipo test (is_analysis=True) no tienen campo que indique qué tipo de muestra requieren
- Los productos tipo muestra (is_sample_type=True) no están relacionados con los tests
- El modelo stock.lot tiene container_type como Selection hardcodeado, no como relación
**Tareas Requeridas:**
1. **Modificar product.template:**
- Agregar campo Many2one 'required_sample_type_id' que relacione análisis con tipo de muestra
- Domain: [('is_sample_type', '=', True)]
2. **Actualizar stock.lot:**
- Opción A: Cambiar container_type de Selection a Many2one hacia product.template
- Opción B: Agregar nuevo campo sample_type_product_id
- Mantener compatibilidad con datos existentes
3. **Actualizar vistas:**
- Agregar campo en formulario de productos cuando is_analysis=True
- Mostrar tipo de muestra requerida en vistas de análisis
4. **Migración de datos:**
- Mapear valores actuales de container_type a productos tipo muestra
- Actualizar registros existentes
5. **Actualizar demo data:**
- Asignar tipos de muestra correctos a cada análisis
- Ejemplo: Hemograma → Tubo EDTA, Glucosa → Tubo Suero
**Beneficios:**
- Permitirá automatizar la generación de muestras al confirmar órdenes
- Evitará errores al saber exactamente qué contenedor usar para cada test
- Facilitará la agrupación de análisis que usan el mismo tipo de muestra
**Dependencia:**
Este issue es prerequisito para poder implementar el Issue #32

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,50 +16,23 @@
'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,

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

@ -19,6 +19,25 @@
</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">
@ -36,6 +55,21 @@
</field>
</record>
<!-- Rangos para Colesterol Total -->
<record id="range_colesterol_total" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">200</field>
<field name="unit_of_measure">mg/dL</field>
</record>
<!-- Rangos para Colesterol LDL -->
<record id="range_colesterol_ldl" model="lims.analysis.range">
<field name="analysis_id" ref="analysis_perfil_lipidico"/>
<field name="min_value">0</field>
<field name="max_value">100</field>
<field name="unit_of_measure">mg/dL</field>
</record>
<!-- Análisis: Glucosa -->

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,9 +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">
@ -34,9 +32,8 @@
<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="phone">+1-202-555-0201</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 +41,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 +49,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 +70,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,13 +1,6 @@
# -*- coding: utf-8 -*-
from . import analysis_parameter
from . import product_template_parameter
from . import parameter_range
from . import analysis_range
from . import product
from . import partner
from . import sale_order
from . import stock_lot
from . import rejection_reason
from . import lims_test
from . import lims_result
from . import res_config_settings
from . import lims_config

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

@ -22,11 +22,10 @@ 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(

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

@ -33,47 +33,21 @@ class SaleOrder(models.Model):
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"""
"""Override to generate laboratory samples automatically"""
res = super(SaleOrder, self).action_confirm()
# Generate samples and tests only for laboratory requests
# Generate samples 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
_logger.error(f"Error generating samples for order {order.name}: {str(e)}")
# Continue with order confirmation even if sample 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),
body=_("Error al generar muestras automáticamente: %s. "
"Por favor, genere las muestras manualmente.") % str(e),
message_type='notification'
)
@ -161,16 +135,8 @@ class SaleOrder(models.Model):
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,
@ -197,198 +163,3 @@ class SaleOrder(models.Model):
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,12 +1,10 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo import models, fields, api
from datetime import datetime
import random
class StockLot(models.Model):
_name = 'stock.lot'
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
_inherit = 'stock.lot'
is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio')
@ -82,225 +80,32 @@ class StockLot(models.Model):
('in_process', 'En Proceso'),
('analyzed', 'Analizada'),
('stored', 'Almacenada'),
('disposed', 'Desechada'),
('cancelled', 'Cancelada'),
('rejected', 'Rechazada')
('disposed', 'Desechada')
], 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'
)
"""Mark sample as collected"""
self.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
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'
)
"""Mark sample as received in laboratory"""
self.write({'state': 'received'})
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'
)
self.write({'state': 'in_process'})
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'
)
self.write({'state': 'analyzed'})
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'
)
"""Store the sample"""
self.write({'state': 'stored'})
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'
)
"""Dispose of the sample"""
self.write({'state': 'disposed'})
@api.onchange('sample_type_product_id')
def _onchange_sample_type_product_id(self):
@ -419,184 +224,3 @@ class StockLot(models.Model):
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,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_lims_analysis_parameter_user,lims.analysis.parameter.user,model_lims_analysis_parameter,base.group_user,1,0,0,0
access_lims_analysis_parameter_manager,lims.analysis.parameter.manager,model_lims_analysis_parameter,group_lims_admin,1,1,1,1
access_product_template_parameter_user,product.template.parameter.user,model_product_template_parameter,base.group_user,1,0,0,0
access_product_template_parameter_manager,product.template.parameter.manager,model_product_template_parameter,group_lims_admin,1,1,1,1
access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_range,base.group_user,1,0,0,0
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,1,1,1,1
access_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,base.group_user,1,1,1,1
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0
access_sale_order_line_receptionist,sale.order.line.receptionist,sale.model_sale_order_line,group_lims_receptionist,1,1,1,0
access_sale_order_technician,sale.order.technician,sale.model_sale_order,group_lims_technician,1,0,0,0
access_sale_order_line_technician,sale.order.line.technician,sale.model_sale_order_line,group_lims_technician,1,0,0,0
access_sale_order_admin,sale.order.admin,sale.model_sale_order,group_lims_admin,1,1,1,1
access_sale_order_line_admin,sale.order.line.admin,sale.model_sale_order_line,group_lims_admin,1,1,1,1
access_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
access_lims_test_receptionist,lims.test.receptionist,model_lims_test,group_lims_receptionist,1,0,0,0
access_lims_test_technician,lims.test.technician,model_lims_test,group_lims_technician,1,1,1,0
access_lims_test_admin,lims.test.admin,model_lims_test,group_lims_admin,1,1,1,1
access_lims_result_receptionist,lims.result.receptionist,model_lims_result,group_lims_receptionist,1,0,0,0
access_lims_result_technician,lims.result.technician,model_lims_result,group_lims_technician,1,1,1,0
access_lims_result_admin,lims.result.admin,model_lims_result,group_lims_admin,1,1,1,1
access_lims_rejection_reason_user,lims.rejection.reason.user,model_lims_rejection_reason,base.group_user,1,0,0,0
access_lims_rejection_reason_technician,lims.rejection.reason.technician,model_lims_rejection_reason,group_lims_technician,1,0,0,0
access_lims_rejection_reason_admin,lims.rejection.reason.admin,model_lims_rejection_reason,group_lims_admin,1,1,1,1
access_lims_sample_rejection_wizard_user,lims.sample.rejection.wizard.user,model_lims_sample_rejection_wizard,base.group_user,1,1,1,1
access_lims_sample_rejection_wizard_technician,lims.sample.rejection.wizard.technician,model_lims_sample_rejection_wizard,group_lims_technician,1,1,1,1
access_lims_config_settings_admin,lims.config.settings.admin,model_lims_config_settings,group_lims_admin,1,1,1,1

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
4 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>
@ -20,21 +36,9 @@
<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 -->

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

@ -101,118 +101,6 @@
parent="lims_menu_root"
action="action_lims_lab_sample"
sequence="16"/>
<!-- Menú para Muestras Rechazadas -->
<menuitem
id="lims_menu_lab_samples_rejected"
name="Muestras Rechazadas"
parent="lims_menu_root"
action="action_lab_sample_rejected"
sequence="17"/>
<!-- Submenú de Laboratorio -->
<menuitem
id="lims_menu_laboratory"
name="Laboratorio"
parent="lims_menu_root"
sequence="20"/>
<!-- Acción para lims.test -->
<record id="action_lims_test" model="ir.actions.act_window">
<field name="name">Pruebas de Laboratorio</field>
<field name="res_model">lims.test</field>
<field name="view_mode">list,kanban,form</field>
<field name="context">{'search_default_my_tests': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crear primera prueba de laboratorio
</p>
<p>
Aquí podrá gestionar las pruebas de laboratorio,
ingresar resultados y validarlos.
</p>
</field>
</record>
<!-- Menú para Pruebas -->
<menuitem id="menu_lims_tests"
name="Pruebas"
parent="lims_menu_laboratory"
action="action_lims_test"
sequence="10"/>
<!-- Menú para Ingreso de Resultados -->
<menuitem id="menu_lims_result_entry"
name="Ingreso de Resultados"
parent="lims_menu_laboratory"
action="action_lims_result_entry"
sequence="25"/>
<!-- Menú para Resultados -->
<menuitem id="menu_lims_result"
name="Resultados"
parent="lims_menu_laboratory"
action="action_lims_result"
sequence="30"/>
<!-- Submenú de Dashboards -->
<menuitem
id="menu_lims_dashboards"
name="Dashboards"
parent="lims_menu_root"
sequence="85"
groups="lims_management.group_lims_admin"/>
<!-- Dashboards individuales -->
<menuitem id="menu_lab_order_dashboard"
name="Estado de &#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
@ -272,64 +160,5 @@
parent="lims_menu_config"
action="action_lims_sample_type_catalog"
sequence="20"/>
<!-- Acción para abrir configuración de laboratorio -->
<record id="action_lims_config_settings" model="ir.actions.act_window">
<field name="name">Configuración</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'lims_management'}</field>
</record>
<!-- Menú de Panel de Parámetros -->
<menuitem id="menu_lims_parameter_dashboard"
name="Panel de Parámetros"
parent="lims_menu_config"
action="action_lims_parameter_dashboard"
sequence="10"/>
<!-- Menú de Parámetros de Análisis -->
<menuitem id="menu_lims_analysis_parameter"
name="Parámetros de Análisis"
parent="lims_menu_config"
action="action_lims_analysis_parameter"
sequence="20"/>
<!-- Menú de Rangos de Referencia -->
<menuitem id="menu_lims_parameter_range"
name="Rangos de Referencia"
parent="lims_menu_config"
action="action_lims_parameter_range"
sequence="25"/>
<!-- Menú de Config. Parámetros-Análisis -->
<menuitem id="menu_product_template_parameter_config"
name="Config. Parámetros-Análisis"
parent="lims_menu_config"
action="action_product_template_parameter_config"
sequence="30"/>
<!-- Menú de Estadísticas -->
<menuitem id="menu_lims_parameter_statistics"
name="Estadísticas"
parent="lims_menu_config"
action="action_lims_parameter_statistics"
sequence="40"/>
<!-- Menú de Motivos de Rechazo -->
<menuitem id="menu_lims_rejection_reason"
name="Motivos de Rechazo"
parent="lims_menu_config"
action="action_lims_rejection_reason"
sequence="50"/>
<!-- Menú de configuración de ajustes -->
<menuitem id="menu_lims_config_settings"
name="Ajustes"
parent="lims_menu_config"
action="action_lims_config_settings"
sequence="100"/>
</data>
</odoo>

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>
@ -35,49 +20,26 @@
</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">
<page string="Muestras Generadas" name="generated_samples" invisible="not is_lab_request">
<group>
<field name="generated_sample_ids" nolabel="1" readonly="1">
<list string="Muestras Generadas" 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="barcode" string="Código de Barras"/>
<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"/>
<field name="volume_ml" string="Volumen (ml)"/>
<field name="analysis_names" string="Análisis"/>
<field name="state" string="Estado"/>
<button name="action_collect" string="Recolectar" type="object"
class="btn-sm btn-primary" invisible="state != 'pending_collection'"/>
class="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 invisible="not generated_sample_ids">
<div class="alert alert-info" role="alert">
<p>Las muestras han sido generadas automáticamente basándose en los análisis solicitados.
Cada muestra agrupa los análisis que requieren el mismo tipo de contenedor.</p>
</div>
</group>
</page>
</xpath>

View File

@ -15,9 +15,7 @@
<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"/>
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-muted="state == 'stored' or state == 'disposed'" widget="badge"/>
</list>
</field>
</record>
@ -35,19 +33,7 @@
<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"/>
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored"/>
</header>
<sheet>
<div class="oe_title">
@ -83,100 +69,10 @@
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>

View File

@ -1,46 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para asignar el usuario admin al grupo de Administrador de Laboratorio
"""
import logging
# Configurar logging
logging.basicConfig(level=logging.INFO)
_logger = logging.getLogger(__name__)
try:
# Buscar el usuario admin
admin_user = env['res.users'].search([('login', '=', 'admin')], limit=1)
if not admin_user:
_logger.error("No se encontró el usuario admin")
exit(1)
# Buscar el grupo de Administrador de Laboratorio
try:
lab_admin_group = env.ref('lims_management.group_lims_admin')
except ValueError:
_logger.error("No se encontró el grupo de Administrador de Laboratorio")
exit(1)
# Verificar si el usuario ya está en el grupo
if lab_admin_group in admin_user.groups_id:
_logger.info("El usuario admin ya está en el grupo de Administrador de Laboratorio")
else:
# Agregar el usuario al grupo
admin_user.write({
'groups_id': [(4, lab_admin_group.id)]
})
_logger.info("Usuario admin agregado exitosamente al grupo de Administrador de Laboratorio")
# Confirmar los grupos del usuario
group_names = ', '.join(admin_user.groups_id.mapped('name'))
_logger.info(f"Grupos del usuario admin: {group_names}")
env.cr.commit()
_logger.info("Cambios guardados exitosamente")
except Exception as e:
_logger.error(f"Error al asignar usuario admin al grupo: {str(e)}")
exit(1)

View File

@ -1,31 +0,0 @@
#!/usr/bin/env python3
import odoo
import base64
import os
def update_logo():
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Buscar la empresa principal
company = env['res.company'].browse(1)
# Leer el logo
logo_path = '/mnt/extra-addons/lims_management/static/img/lab_logo.png'
with open(logo_path, 'rb') as f:
logo_base64 = base64.b64encode(f.read()).decode('utf-8')
# Actualizar
company.write({
'logo': logo_base64,
'name': 'Laboratorio Clínico LIMS'
})
cr.commit()
print("Logo actualizado exitosamente")
if __name__ == '__main__':
update_logo()

View File

@ -1,56 +0,0 @@
import base64
import os
import sys
try:
# El script se ejecuta dentro del shell de Odoo
# env ya está disponible en el contexto
print("Iniciando actualización de logo...")
# Buscar la empresa principal
company = env['res.company'].search([('id', '=', 1)], limit=1)
if not company:
print("ERROR: No se encontró la empresa principal")
sys.exit(1)
print(f"Empresa encontrada: {company.name}")
# Leer el archivo de logo
logo_path = '/mnt/extra-addons/lims_management/static/img/lab_logo.png'
if not os.path.exists(logo_path):
print(f"ERROR: No se encontró el archivo de logo en: {logo_path}")
sys.exit(1)
print(f"Archivo de logo encontrado en: {logo_path}")
# Leer y codificar la imagen
with open(logo_path, 'rb') as logo_file:
logo_data = logo_file.read()
logo_base64 = base64.b64encode(logo_data).decode('utf-8')
print(f"Logo leído correctamente, tamaño: {len(logo_data)} bytes")
# Actualizar el logo y nombre de la empresa
company.write({
'logo': logo_base64,
'name': 'Laboratorio Clínico LIMS'
})
print(f"Logo actualizado exitosamente para la empresa: {company.name}")
# También actualizar el partner asociado
if company.partner_id:
company.partner_id.write({
'image_1920': logo_base64
})
print(f"Logo del partner también actualizado (ID: {company.partner_id.id})")
# El commit se hace automáticamente al salir del shell
print("\nLogo de la empresa actualizado exitosamente en la base de datos.")
except Exception as e:
print(f"ERROR al actualizar el logo: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,69 +0,0 @@
import base64
import os
import sys
try:
# El script se ejecuta dentro del shell de Odoo
# env ya está disponible en el contexto
print("Iniciando actualización de logo...")
# Buscar TODAS las empresas (puede haber más de una)
companies = env['res.company'].search([])
print(f"Empresas encontradas: {len(companies)}")
# Leer el archivo de logo
logo_path = '/mnt/extra-addons/lims_management/static/img/lab_logo.png'
if not os.path.exists(logo_path):
print(f"ERROR: No se encontró el archivo de logo en: {logo_path}")
sys.exit(1)
print(f"Archivo de logo encontrado en: {logo_path}")
# Leer y codificar la imagen
with open(logo_path, 'rb') as logo_file:
logo_data = logo_file.read()
logo_base64 = base64.b64encode(logo_data).decode('utf-8')
print(f"Logo leído correctamente, tamaño: {len(logo_data)} bytes")
# Actualizar TODAS las empresas
for company in companies:
print(f"\nActualizando empresa ID {company.id}: {company.name}")
# Actualizar el logo y nombre de la empresa
company.write({
'logo': logo_base64,
'name': 'Laboratorio Clínico LIMS'
})
print(f"Logo actualizado para la empresa ID {company.id}")
# También actualizar el partner asociado
if company.partner_id:
company.partner_id.write({
'image_1920': logo_base64,
'name': 'Laboratorio Clínico LIMS'
})
print(f"Logo y nombre del partner también actualizados (ID: {company.partner_id.id})")
# IMPORTANTE: Hacer commit explícito
env.cr.commit()
print("\nCommit realizado - cambios guardados en la base de datos.")
# Verificar que los cambios se guardaron
companies_check = env['res.company'].search([])
for company in companies_check:
has_logo = bool(company.logo)
correct_name = company.name == 'Laboratorio Clínico LIMS'
print(f"\nVerificación empresa ID {company.id}:")
print(f" - Nombre: {company.name} ({'' if correct_name else ''})")
print(f" - Logo: {'✓ Presente' if has_logo else '✗ Ausente'}")
print("\nLogo de la empresa actualizado exitosamente en la base de datos.")
except Exception as e:
print(f"ERROR al actualizar el logo: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,86 +0,0 @@
import base64
import os
import sys
try:
# El script se ejecuta dentro del shell de Odoo
# env ya está disponible en el contexto
print("Iniciando actualización de logo...")
# Buscar TODAS las empresas
companies = env['res.company'].search([])
print(f"Empresas encontradas: {len(companies)}")
# Leer el archivo de logo
logo_path = '/mnt/extra-addons/lims_management/static/img/lab_logo.png'
if not os.path.exists(logo_path):
print(f"ERROR: No se encontró el archivo de logo en: {logo_path}")
sys.exit(1)
print(f"Archivo de logo encontrado en: {logo_path}")
# Leer y codificar la imagen
with open(logo_path, 'rb') as logo_file:
logo_data = logo_file.read()
logo_base64 = base64.b64encode(logo_data).decode('utf-8')
print(f"Logo leído correctamente, tamaño: {len(logo_data)} bytes")
# Actualizar las empresas con nombres únicos
for idx, company in enumerate(companies):
print(f"\nActualizando empresa ID {company.id}: {company.name}")
# Generar nombre único para cada empresa
if idx == 0:
new_name = 'Laboratorio Clínico LIMS'
else:
new_name = f'Laboratorio Clínico LIMS - Sucursal {idx}'
try:
# Actualizar el logo y nombre de la empresa
company.write({
'logo': logo_base64,
'name': new_name
})
print(f"Logo actualizado para la empresa ID {company.id}")
print(f"Nuevo nombre: {new_name}")
# También actualizar el partner asociado
if company.partner_id:
company.partner_id.write({
'image_1920': logo_base64,
'name': new_name
})
print(f"Logo y nombre del partner también actualizados (ID: {company.partner_id.id})")
except Exception as e:
print(f"Error al actualizar empresa ID {company.id}: {str(e)}")
continue
# IMPORTANTE: Hacer commit explícito
env.cr.commit()
print("\nCommit realizado - cambios guardados en la base de datos.")
# Verificar que los cambios se guardaron
companies_check = env['res.company'].search([])
print("\n" + "="*60)
print("VERIFICACIÓN FINAL:")
print("="*60)
for company in companies_check:
has_logo = bool(company.logo)
print(f"\nEmpresa ID {company.id}:")
print(f" - Nombre: {company.name}")
print(f" - Logo: {'✓ Presente' if has_logo else '✗ Ausente'}")
if has_logo:
logo_size = len(base64.b64decode(company.logo))
print(f" - Tamaño del logo: {logo_size:,} bytes")
print("\nLogo de la empresa actualizado exitosamente en la base de datos.")
except Exception as e:
print(f"ERROR al actualizar el logo: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,91 +0,0 @@
import base64
import os
import sys
try:
# El script se ejecuta dentro del shell de Odoo
print("Iniciando actualización de logo para Odoo 18...")
# Buscar TODAS las empresas
companies = env['res.company'].search([])
print(f"Empresas encontradas: {len(companies)}")
# Leer el archivo de logo
logo_path = '/mnt/extra-addons/lims_management/static/img/lab_logo.png'
if not os.path.exists(logo_path):
print(f"ERROR: No se encontró el archivo de logo en: {logo_path}")
sys.exit(1)
print(f"Archivo de logo encontrado en: {logo_path}")
# Leer y codificar la imagen
with open(logo_path, 'rb') as logo_file:
logo_data = logo_file.read()
logo_base64 = base64.b64encode(logo_data) # No decodificar a string
print(f"Logo leído correctamente, tamaño: {len(logo_data)} bytes")
# Actualizar las empresas
for idx, company in enumerate(companies):
print(f"\nActualizando empresa ID {company.id}: {company.name}")
# Generar nombre único para cada empresa
if idx == 0:
new_name = 'Laboratorio Clínico LIMS'
else:
new_name = f'Laboratorio Clínico LIMS - Sucursal {idx}'
try:
# En Odoo 18, actualizar por separado para evitar problemas
# Primero el nombre
company.name = new_name
env.cr.commit()
print(f"Nombre actualizado: {new_name}")
# Luego el logo usando sudo para evitar problemas de permisos
company.sudo().write({
'logo': logo_base64,
})
env.cr.commit()
print(f"Logo actualizado para la empresa ID {company.id}")
# También actualizar el partner asociado
if company.partner_id:
company.partner_id.name = new_name
company.partner_id.sudo().write({
'image_1920': logo_base64,
})
env.cr.commit()
print(f"Partner actualizado (ID: {company.partner_id.id})")
except Exception as e:
print(f"Error al actualizar empresa ID {company.id}: {str(e)}")
env.cr.rollback()
continue
# Verificación final usando el ORM
print("\n" + "="*60)
print("VERIFICACIÓN FINAL:")
print("="*60)
# Verificar a través del ORM que es más seguro
for company in env['res.company'].search([], order='id'):
print(f"\nEmpresa ID {company.id}:")
print(f" - Nombre: {company.name}")
print(f" - Logo presente: {'SI' if company.logo else 'NO'}")
if company.logo:
print(f" - Tamaño del logo (base64): {len(company.logo):,} caracteres")
# Forzar actualización de caché
env['res.company']._invalidate_cache()
print("\nLogo de la empresa actualizado exitosamente.")
print("NOTA: Si el logo no aparece en la interfaz, puede ser necesario:")
print(" 1. Limpiar la caché del navegador (Ctrl+F5)")
print(" 2. Reiniciar el servicio de Odoo")
except Exception as e:
print(f"ERROR al actualizar el logo: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,127 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para agregar resultados a las pruebas ya validadas
"""
import odoo
import logging
import random
_logger = logging.getLogger(__name__)
def add_results_to_validated_tests(env):
"""Agregar resultados a pruebas ya validadas"""
# Buscar las órdenes S00029 y S00030
orders = env['sale.order'].search([('name', 'in', ['S00029', 'S00030'])], order='name')
if not orders:
print("No se encontraron las órdenes S00029 o S00030")
return
for order in orders:
print(f"\n=== Procesando orden {order.name} ===")
for test in order.lab_test_ids:
print(f"\nPrueba: {test.product_id.name}")
# Cambiar temporalmente a estado draft para poder modificar
test.sudo().write({'state': 'draft'})
# Generar resultados si no existen
if not test.result_ids:
test.sudo()._generate_test_results()
print(f" Generados {len(test.result_ids)} resultados")
# Asignar valores a los resultados
for result in test.result_ids:
parameter = result.parameter_id
vals = {}
if parameter.value_type == 'numeric':
# Valores específicos por código
if parameter.code == 'HGB': # Hemoglobina
vals['value_numeric'] = random.uniform(12.0, 16.0)
elif parameter.code == 'HCT': # Hematocrito
vals['value_numeric'] = random.uniform(36.0, 46.0)
elif parameter.code == 'WBC': # Leucocitos
vals['value_numeric'] = random.uniform(4.5, 10.0)
elif parameter.code == 'PLT': # Plaquetas
vals['value_numeric'] = random.uniform(150, 400)
elif parameter.code == 'RBC': # Eritrocitos
vals['value_numeric'] = random.uniform(4.0, 5.5)
elif parameter.code == 'GLU': # Glucosa
vals['value_numeric'] = random.uniform(70, 110)
elif parameter.code == 'CHOL': # Colesterol
vals['value_numeric'] = random.uniform(160, 220)
elif parameter.code == 'TRIG': # Triglicéridos
vals['value_numeric'] = random.uniform(50, 150)
elif parameter.code == 'HDL': # HDL
vals['value_numeric'] = random.uniform(40, 60)
elif parameter.code == 'LDL': # LDL
vals['value_numeric'] = random.uniform(80, 130)
else:
# Valor genérico
vals['value_numeric'] = random.uniform(10, 100)
print(f" - {parameter.name}: {vals['value_numeric']:.2f}")
elif parameter.value_type == 'text':
vals['value_text'] = "Normal"
elif parameter.value_type == 'selection':
vals['value_selection'] = "normal"
elif parameter.value_type == 'boolean':
vals['value_boolean'] = False
# Escribir con sudo para evitar restricciones
result.sudo().write(vals)
# Volver a estado validated
test.sudo().write({
'state': 'validated',
'validator_id': env.ref('base.user_admin').id,
'validation_date': fields.Datetime.now()
})
# Agregar notas a algunas pruebas
if 'Hemograma' in test.product_id.name:
test.sudo().write({'notes': 'Todos los parámetros dentro de rangos normales.'})
elif 'Lipídico' in test.product_id.name:
test.sudo().write({'notes': 'Perfil lipídico normal. Se recomienda mantener dieta balanceada.'})
print("\n✅ Resultados agregados exitosamente a todas las pruebas")
return orders
if __name__ == '__main__':
# Importar fields después de configurar Odoo
from odoo import fields
# Configuración
db_name = 'lims_demo'
# Conectar a Odoo
odoo.tools.config.parse_config(['--database', db_name])
# Obtener el registro de la base de datos
registry = odoo.registry(db_name)
# Crear cursor y environment
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
try:
# Agregar resultados a las pruebas
orders = add_results_to_validated_tests(env)
# Confirmar cambios
cr.commit()
print("\n📋 Ahora puedes probar el botón 'Imprimir Informe de Resultados' en las órdenes S00029 y S00030.")
except Exception as e:
cr.rollback()
print(f"\n❌ Error: {str(e)}")
_logger.error(f"Error agregando resultados: {str(e)}", exc_info=True)

View File

@ -1,63 +0,0 @@
import odoo
import json
def check_demo_users(cr):
"""Verificar si los usuarios demo fueron creados"""
cr.execute("""
SELECT
u.id,
u.login,
u.name,
u.active,
array_agg(g.name) as groups
FROM res_users u
LEFT JOIN res_groups_users_rel rel ON rel.uid = u.id
LEFT JOIN res_groups g ON g.id = rel.gid
WHERE u.login IN ('recepcionista', 'tecnico', 'administrador')
GROUP BY u.id, u.login, u.name, u.active
ORDER BY u.login
""")
users = cr.fetchall()
print("\n=== USUARIOS DEMO CREADOS ===")
print("-" * 60)
if not users:
print("❌ NO se encontraron usuarios demo")
return
for user in users:
user_id, login, name, active, groups = user
status = "✓ Activo" if active else "✗ Inactivo"
print(f"\nUsuario: {login}")
print(f" ID: {user_id}")
print(f" Nombre: {name}")
print(f" Estado: {status}")
print(f" Grupos: {', '.join(groups) if groups[0] else 'Sin grupos'}")
print("\n" + "-" * 60)
print(f"Total usuarios demo encontrados: {len(users)}")
# Verificar contraseñas (solo para confirmar que pueden loguearse)
expected_users = {
'recepcionista': 'Recepcionista Demo',
'tecnico': 'Técnico Demo',
'administrador': 'Administrador Lab Demo'
}
missing = []
for login, expected_name in expected_users.items():
if not any(u[1] == login for u in users):
missing.append(login)
if missing:
print(f"\n⚠️ Usuarios faltantes: {', '.join(missing)}")
else:
print("\n✅ Todos los usuarios demo esperados fueron creados")
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
check_demo_users(cr)

View File

@ -1,87 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para verificar los resultados recién creados
"""
import odoo
def check_results(cr):
"""Verificar resultados de las pruebas LAB-2025-00034, 00035 y 00036"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("🔍 Buscando las pruebas recién creadas...")
# Buscar las pruebas por nombre
test_names = ['LAB-2025-00034', 'LAB-2025-00035', 'LAB-2025-00036']
tests = env['lims.test'].search([('name', 'in', test_names)])
print(f"\n Pruebas encontradas: {len(tests)}")
for test in tests:
print(f"\n📋 Prueba: {test.name}")
print(f" Análisis: {test.product_id.name}")
print(f" Resultados: {len(test.result_ids)}")
for result in test.result_ids:
print(f"\n Resultado ID {result.id}:")
print(f" Parámetro: {result.parameter_id.name}")
print(f" Tipo: {result.parameter_value_type}")
print(f" value_numeric: {result.value_numeric}")
print(f" value_text: '{result.value_text}'")
print(f" value_selection: '{result.value_selection}'")
print(f" value_boolean: {result.value_boolean}")
# Verificar si es problemático
if result.parameter_value_type == 'selection':
values_count = 0
if result.value_numeric not in [False, 0.0]:
values_count += 1
if result.value_text:
values_count += 1
if result.value_selection:
values_count += 1
if result.value_boolean:
values_count += 1
if values_count > 1 or (values_count == 1 and not result.value_selection):
print(f" ❌ PROBLEMÁTICO: {values_count} valores establecidos")
# Intentar corregir
print(" 🔧 Corrigiendo...")
try:
# Primero intentar con SQL directo para evitar validaciones
cr.execute("""
UPDATE lims_result
SET value_numeric = NULL,
value_text = NULL,
value_boolean = FALSE
WHERE id = %s
""", (result.id,))
print(" ✓ Corregido con SQL directo")
except Exception as e:
print(f" ❌ Error al corregir: {e}")
# Verificar si la orden S00029 sigue en estado sale
order = env['sale.order'].search([('name', '=', 'S00029')], limit=1)
if order:
print(f"\n📊 Estado de la orden S00029: {order.state}")
# Si está en sale, las muestras deberían estar generadas
if order.state == 'sale':
print(f" Muestras generadas: {len(order.generated_sample_ids)}")
for sample in order.generated_sample_ids:
print(f" - {sample.name}: {sample.sample_state}")
if __name__ == '__main__':
db_name = 'lims_demo'
try:
registry = odoo.modules.registry.Registry(db_name)
with registry.cursor() as cr:
check_results(cr)
cr.commit()
print("\n✅ Cambios guardados exitosamente")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()

View File

@ -1,67 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script para verificar los datos de la orden S00025
"""
import odoo
def check_order_s00025(env):
"""Verificar datos específicos de la orden S00025"""
# Buscar la orden S00025
order = env['sale.order'].search([
('name', '=', 'S00025')
], limit=1)
if not order:
print("❌ No se encontró la orden S00025")
return
patient = order.partner_id
print(f"Orden: {order.name}")
print(f" Es orden de laboratorio: {order.is_lab_request}")
print(f" Paciente: {patient.name}")
print(f" ID Paciente: {patient.id}")
print(f" Fecha de nacimiento: {patient.birthdate_date}")
print(f" Edad: {patient.age if patient.birthdate_date else 'N/A'}")
print(f" Género: {patient.gender or 'No especificado'}")
# Verificar estado de las pruebas
print(f"\nPruebas de laboratorio ({len(order.lab_test_ids)}):")
for test in order.lab_test_ids:
print(f" - {test.product_id.name}: {test.state}")
# Verificar si puede imprimir resultados
print(f"\n¿Puede imprimir resultados?: {order.can_print_results}")
# Si el paciente no tiene fecha de nacimiento, actualizarla
if not patient.birthdate_date:
print("\n⚠️ El paciente no tiene fecha de nacimiento. Actualizando...")
patient.write({'birthdate_date': '1985-01-15'})
print(f" Fecha de nacimiento actualizada a: {patient.birthdate_date}")
print(f" Edad calculada: {patient.age} años")
if __name__ == '__main__':
# Configuración
db_name = 'lims_demo'
# Conectar a Odoo
odoo.tools.config.parse_config(['--database', db_name])
# Obtener el registro de la base de datos
registry = odoo.registry(db_name)
# Crear cursor y environment
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
try:
# Verificar datos
check_order_s00025(env)
cr.commit()
except Exception as e:
print(f"\n❌ Error: {str(e)}")
import traceback
traceback.print_exc()

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