Merge pull request 'feat(#8): Implementar gestión de pruebas y resultados de laboratorio' (#52) from feature/8-test-results-management into dev
This commit is contained in:
commit
beaed060eb
17
CLAUDE.md
17
CLAUDE.md
|
@ -40,6 +40,17 @@ After successful installation/update, the instance must remain active for user v
|
|||
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
|
||||
|
@ -94,6 +105,12 @@ python gitea_cli_helper.py comment-issue --issue-number 123 --body "Comment text
|
|||
|
||||
# Close issue
|
||||
python gitea_cli_helper.py close-issue --issue-number 123
|
||||
|
||||
# Get issue details and comments
|
||||
python gitea_cli_helper.py get-issue --issue-number 8
|
||||
|
||||
# List all open issues
|
||||
python gitea_cli_helper.py list-open-issues
|
||||
```
|
||||
|
||||
## Mandatory Reading
|
||||
|
|
|
@ -25,6 +25,7 @@ services:
|
|||
- ./odoo.conf:/etc/odoo/odoo.conf
|
||||
- ./init_odoo.py:/app/init_odoo.py
|
||||
- ./create_lab_requests.py:/app/create_lab_requests.py
|
||||
- ./test:/app/test
|
||||
command: ["/usr/bin/python3", "/app/init_odoo.py"]
|
||||
environment:
|
||||
HOST: db
|
||||
|
|
163
documents/plans/ISSUE8_PLAN.md
Normal file
163
documents/plans/ISSUE8_PLAN.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
# 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
|
|
@ -110,6 +110,60 @@ 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"
|
||||
|
@ -223,6 +277,10 @@ 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()
|
||||
|
||||
|
@ -238,6 +296,8 @@ 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()
|
||||
|
||||
|
|
34
init_odoo.py
34
init_odoo.py
|
@ -94,6 +94,40 @@ 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()
|
||||
|
||||
if os.path.exists("/app/test/create_test_demo_data.py"):
|
||||
with open("/app/test/create_test_demo_data.py", "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
|
||||
)
|
||||
|
||||
print("--- Create Test Demo Data stdout ---")
|
||||
print(result.stdout)
|
||||
print("--- Create Test Demo Data stderr ---")
|
||||
print(result.stderr)
|
||||
sys.stdout.flush()
|
||||
|
||||
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})")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
@ -16,17 +16,25 @@
|
|||
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
|
||||
'category': 'Industries',
|
||||
'version': '18.0.1.0.0',
|
||||
'depends': ['base', 'product', 'sale'],
|
||||
'depends': ['base', 'product', 'sale', 'base_setup'],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'lims_management/static/src/css/lims_test.css',
|
||||
],
|
||||
},
|
||||
'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',
|
||||
'views/partner_views.xml',
|
||||
'views/analysis_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/stock_lot_views.xml',
|
||||
'views/lims_test_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/menus.xml',
|
||||
],
|
||||
'demo': [
|
||||
|
|
15
lims_management/data/lims_sequence.xml
Normal file
15
lims_management/data/lims_sequence.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -4,3 +4,6 @@ from . import product
|
|||
from . import partner
|
||||
from . import sale_order
|
||||
from . import stock_lot
|
||||
from . import lims_test
|
||||
from . import lims_result
|
||||
from . import res_config_settings
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
124
lims_management/models/lims_result.py
Normal file
124
lims_management/models/lims_result.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# -*- 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'
|
||||
)
|
||||
|
||||
# Por ahora, estos campos básicos
|
||||
parameter_name = fields.Char(
|
||||
string='Parámetro',
|
||||
required=True
|
||||
)
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='Secuencia',
|
||||
default=10
|
||||
)
|
||||
|
||||
# TODO: Implementar parameter_id cuando exista lims.test.parameter
|
||||
# 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(
|
||||
[], # Por ahora vacío
|
||||
string='Valor de Selección'
|
||||
)
|
||||
|
||||
is_out_of_range = fields.Boolean(
|
||||
string='Fuera de Rango',
|
||||
compute='_compute_is_out_of_range',
|
||||
store=True
|
||||
)
|
||||
|
||||
notes = fields.Text(
|
||||
string='Notas del Técnico'
|
||||
)
|
||||
|
||||
# Campos para rangos normales (temporal)
|
||||
normal_min = fields.Float(
|
||||
string='Valor Normal Mínimo'
|
||||
)
|
||||
|
||||
normal_max = fields.Float(
|
||||
string='Valor Normal Máximo'
|
||||
)
|
||||
|
||||
unit = fields.Char(
|
||||
string='Unidad'
|
||||
)
|
||||
|
||||
@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', 'normal_min', 'normal_max')
|
||||
def _compute_is_out_of_range(self):
|
||||
"""Determina si el valor está fuera del rango normal."""
|
||||
for record in self:
|
||||
if record.value_numeric and (record.normal_min or record.normal_max):
|
||||
if record.normal_min and record.value_numeric < record.normal_min:
|
||||
record.is_out_of_range = True
|
||||
elif record.normal_max and record.value_numeric > record.normal_max:
|
||||
record.is_out_of_range = True
|
||||
else:
|
||||
record.is_out_of_range = False
|
||||
else:
|
||||
record.is_out_of_range = False
|
||||
|
||||
@api.constrains('value_numeric', 'value_text', 'value_selection')
|
||||
def _check_single_value_type(self):
|
||||
"""Asegura que solo un tipo de valor esté lleno."""
|
||||
for record in self:
|
||||
filled_values = 0
|
||||
if record.value_numeric:
|
||||
filled_values += 1
|
||||
if record.value_text:
|
||||
filled_values += 1
|
||||
if record.value_selection:
|
||||
filled_values += 1
|
||||
|
||||
if filled_values > 1:
|
||||
raise ValidationError(
|
||||
_('Solo se puede ingresar un tipo de valor (numérico, texto o selección) por resultado.')
|
||||
)
|
||||
|
||||
if filled_values == 0:
|
||||
raise ValidationError(
|
||||
_('Debe ingresar al menos un valor para el resultado.')
|
||||
)
|
247
lims_management/models/lims_test.py
Normal file
247
lims_management/models/lims_test.py
Normal file
|
@ -0,0 +1,247 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
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'
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@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)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Genera código único al crear."""
|
||||
for vals in vals_list:
|
||||
if vals.get('name', 'Nuevo') == 'Nuevo':
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('lims.test') or 'Nuevo'
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_start_process(self):
|
||||
"""Inicia el proceso de análisis."""
|
||||
self.ensure_one()
|
||||
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')
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_enter_results(self):
|
||||
"""Marca como resultados ingresados."""
|
||||
self.ensure_one()
|
||||
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.'))
|
||||
|
||||
# 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')
|
||||
)
|
||||
else:
|
||||
self.state = 'result_entered'
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Ingresados')
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_validate(self):
|
||||
"""Valida los resultados (solo administradores)."""
|
||||
self.ensure_one()
|
||||
if self.state != 'result_entered':
|
||||
raise UserError(_('Solo se pueden validar pruebas con resultados ingresados.'))
|
||||
|
||||
# TODO: Verificar permisos cuando se implemente seguridad
|
||||
|
||||
self.write({
|
||||
'state': 'validated',
|
||||
'validator_id': self.env.user.id,
|
||||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Resultados validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados')
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancela la prueba."""
|
||||
self.ensure_one()
|
||||
if self.state == 'validated':
|
||||
raise UserError(_('No se pueden cancelar pruebas validadas.'))
|
||||
|
||||
self.state = 'cancelled'
|
||||
|
||||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Prueba cancelada por %s') % self.env.user.name,
|
||||
subject=_('Prueba Cancelada')
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_draft(self):
|
||||
"""Regresa a borrador."""
|
||||
self.ensure_one()
|
||||
if self.state not in ['cancelled']:
|
||||
raise UserError(_('Solo se pueden regresar a borrador pruebas canceladas.'))
|
||||
|
||||
self.state = 'draft'
|
||||
|
||||
return True
|
20
lims_management/models/res_config_settings.py
Normal file
20
lims_management/models/res_config_settings.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- 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
|
||||
)
|
|
@ -34,20 +34,21 @@ class SaleOrder(models.Model):
|
|||
)
|
||||
|
||||
def action_confirm(self):
|
||||
"""Override to generate laboratory samples automatically"""
|
||||
"""Override to generate laboratory samples and tests automatically"""
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
|
||||
# Generate samples only for laboratory requests
|
||||
# Generate samples and tests only for laboratory requests
|
||||
for order in self.filtered('is_lab_request'):
|
||||
try:
|
||||
order._generate_lab_samples()
|
||||
order._generate_lab_tests()
|
||||
except Exception as e:
|
||||
_logger.error(f"Error generating samples for order {order.name}: {str(e)}")
|
||||
# Continue with order confirmation even if sample generation fails
|
||||
_logger.error(f"Error generating samples/tests for order {order.name}: {str(e)}")
|
||||
# Continue with order confirmation even if generation fails
|
||||
# But notify the user
|
||||
order.message_post(
|
||||
body=_("Error al generar muestras automáticamente: %s. "
|
||||
"Por favor, genere las muestras manualmente.") % str(e),
|
||||
body=_("Error al generar muestras/pruebas automáticamente: %s. "
|
||||
"Por favor, genere las muestras y pruebas manualmente.") % str(e),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
|
@ -163,3 +164,71 @@ 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
|
||||
|
|
|
@ -2,3 +2,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|||
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_stock_lot_user,stock.lot.user,stock.model_stock_lot,base.group_user,1,1,1,1
|
||||
access_lims_test_user,lims.test.user,model_lims_test,base.group_user,1,1,1,1
|
||||
access_lims_result_user,lims.result.user,model_lims_result,base.group_user,1,1,1,1
|
||||
|
|
|
21
lims_management/static/src/css/lims_test.css
Normal file
21
lims_management/static/src/css/lims_test.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* 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;
|
||||
}
|
213
lims_management/views/lims_test_views.xml
Normal file
213
lims_management/views/lims_test_views.xml
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?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'"/>
|
||||
<button name="action_enter_results" string="Marcar Resultados Ingresados"
|
||||
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' or not require_validation"/>
|
||||
<button name="action_cancel" string="Cancelar"
|
||||
type="object"
|
||||
invisible="state in ['validated', 'cancelled']"/>
|
||||
<button name="action_draft" string="Volver a Borrador"
|
||||
type="object"
|
||||
invisible="state != 'cancelled'"/>
|
||||
<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)]"/>
|
||||
</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">
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
context="{'default_test_id': id}">
|
||||
<list string="Resultados" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="parameter_name"/>
|
||||
<field name="value_numeric"
|
||||
invisible="value_text or value_selection"
|
||||
decoration-danger="is_out_of_range"/>
|
||||
<field name="value_text"
|
||||
invisible="value_numeric or value_selection"/>
|
||||
<field name="value_selection"
|
||||
invisible="value_numeric or value_text"/>
|
||||
<field name="unit" optional="show"/>
|
||||
<field name="normal_min" optional="hide"/>
|
||||
<field name="normal_max" optional="hide"/>
|
||||
<field name="is_out_of_range" invisible="1"/>
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Observaciones">
|
||||
<field name="notes" placeholder="Agregar observaciones generales de la prueba..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista lista para lims.test -->
|
||||
<record id="view_lims_test_list" model="ir.ui.view">
|
||||
<field name="name">lims.test.list</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"/>
|
||||
<field name="technician_id" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'validated'"
|
||||
decoration-warning="state == 'result_entered'"
|
||||
decoration-info="state == 'in_process'"
|
||||
decoration-muted="state == 'cancelled'"/>
|
||||
<field name="create_date" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista kanban para lims.test -->
|
||||
<record id="view_lims_test_kanban" model="ir.ui.view">
|
||||
<field name="name">lims.test.kanban</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="state"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="create_date"/>
|
||||
<templates>
|
||||
<t t-name="kanban-card">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title">
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<div>
|
||||
<i class="fa fa-user" title="Paciente"/>
|
||||
<field name="patient_id"/>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa fa-flask" title="Análisis"/>
|
||||
<field name="product_id"/>
|
||||
</div>
|
||||
<div t-if="record.technician_id.raw_value">
|
||||
<i class="fa fa-user-md" title="Técnico"/>
|
||||
<field name="technician_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<field name="create_date" widget="date"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista búsqueda para lims.test -->
|
||||
<record id="view_lims_test_search" model="ir.ui.view">
|
||||
<field name="name">lims.test.search</field>
|
||||
<field name="model">lims.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Buscar Pruebas">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="sample_id"/>
|
||||
<field name="technician_id"/>
|
||||
<separator/>
|
||||
<filter string="Borrador" name="draft" domain="[('state','=','draft')]"/>
|
||||
<filter string="En Proceso" name="in_process" domain="[('state','=','in_process')]"/>
|
||||
<filter string="Resultado Ingresado" name="result_entered" domain="[('state','=','result_entered')]"/>
|
||||
<filter string="Validado" name="validated" domain="[('state','=','validated')]"/>
|
||||
<separator/>
|
||||
<filter string="Mis Pruebas" name="my_tests" domain="[('technician_id','=',uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Hoy" name="today" domain="[('create_date','>=',(datetime.datetime.now().replace(hour=0, minute=0, second=0)).strftime('%Y-%m-%d %H:%M:%S'))]"/>
|
||||
<group expand="0" string="Agrupar Por">
|
||||
<filter string="Estado" name="group_by_state" context="{'group_by':'state'}"/>
|
||||
<filter string="Paciente" name="group_by_patient" context="{'group_by':'patient_id'}"/>
|
||||
<filter string="Análisis" name="group_by_product" context="{'group_by':'product_id'}"/>
|
||||
<filter string="Técnico" name="group_by_technician" context="{'group_by':'technician_id'}"/>
|
||||
<filter string="Fecha" name="group_by_date" context="{'group_by':'create_date:day'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- 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="search_view_id" ref="view_lims_test_search"/>
|
||||
<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>
|
||||
|
||||
</data>
|
||||
</odoo>
|
|
@ -101,6 +101,13 @@
|
|||
parent="lims_menu_root"
|
||||
action="action_lims_lab_sample"
|
||||
sequence="16"/>
|
||||
|
||||
<!-- Menú para Pruebas -->
|
||||
<menuitem id="menu_lims_tests"
|
||||
name="Pruebas"
|
||||
parent="lims_menu_root"
|
||||
action="action_lims_test"
|
||||
sequence="25"/>
|
||||
|
||||
<!-- Submenú de Configuración -->
|
||||
<menuitem
|
||||
|
@ -160,5 +167,12 @@
|
|||
parent="lims_menu_config"
|
||||
action="action_lims_sample_type_catalog"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Menú de configuración de ajustes -->
|
||||
<menuitem id="menu_lims_config_settings"
|
||||
name="Ajustes"
|
||||
parent="lims_menu_config"
|
||||
action="lims_management.action_lims_config_settings"
|
||||
sequence="100"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
37
lims_management/views/res_config_settings_views.xml
Normal file
37
lims_management/views/res_config_settings_views.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
</data>
|
||||
</odoo>
|
225
test/create_test_demo_data.py
Normal file
225
test/create_test_demo_data.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import odoo
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def create_test_demo_data(cr):
|
||||
"""Crea datos de demostración para lims.test y lims.result"""
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
|
||||
# Buscar algunos pacientes y análisis existentes
|
||||
patients = env['res.partner'].search([('is_patient', '=', True)], limit=3)
|
||||
if not patients:
|
||||
print("No se encontraron pacientes para crear pruebas de demostración")
|
||||
return
|
||||
|
||||
# Buscar análisis disponibles
|
||||
hemograma = env.ref('lims_management.analysis_hemograma', raise_if_not_found=False)
|
||||
glucosa = env.ref('lims_management.analysis_glucosa', raise_if_not_found=False)
|
||||
|
||||
if not hemograma or not glucosa:
|
||||
print("No se encontraron análisis de demostración")
|
||||
return
|
||||
|
||||
# Buscar o crear una orden de laboratorio simple
|
||||
lab_order = env['sale.order'].search([
|
||||
('is_lab_request', '=', True),
|
||||
('state', '=', 'sale')
|
||||
], limit=1)
|
||||
|
||||
if not lab_order:
|
||||
# Crear una orden básica si no existe
|
||||
lab_order = env['sale.order'].create({
|
||||
'partner_id': patients[0].id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': hemograma.product_variant_id.id,
|
||||
'product_uom_qty': 1
|
||||
}), (0, 0, {
|
||||
'product_id': glucosa.product_variant_id.id,
|
||||
'product_uom_qty': 1
|
||||
})]
|
||||
})
|
||||
lab_order.action_confirm()
|
||||
|
||||
# Obtener las líneas de orden
|
||||
order_lines = lab_order.order_line
|
||||
if not order_lines:
|
||||
print("No se encontraron líneas de orden")
|
||||
return
|
||||
|
||||
# Buscar muestras existentes
|
||||
samples = env['stock.lot'].search([
|
||||
('is_lab_sample', '=', True),
|
||||
('patient_id', '=', lab_order.partner_id.id)
|
||||
], limit=2)
|
||||
|
||||
if not samples:
|
||||
print("No se encontraron muestras de laboratorio")
|
||||
return
|
||||
|
||||
# Crear prueba 1: Hemograma en proceso
|
||||
test1 = env['lims.test'].create({
|
||||
'sale_order_line_id': order_lines[0].id,
|
||||
'sample_id': samples[0].id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
# Iniciar proceso
|
||||
test1.action_start_process()
|
||||
|
||||
# Crear resultados para hemograma
|
||||
results_data = [
|
||||
{
|
||||
'test_id': test1.id,
|
||||
'parameter_name': 'Glóbulos Rojos',
|
||||
'sequence': 10,
|
||||
'value_numeric': 4.5,
|
||||
'unit': '10^6/µL',
|
||||
'normal_min': 4.2,
|
||||
'normal_max': 5.4
|
||||
},
|
||||
{
|
||||
'test_id': test1.id,
|
||||
'parameter_name': 'Glóbulos Blancos',
|
||||
'sequence': 20,
|
||||
'value_numeric': 12.5, # Fuera de rango
|
||||
'unit': '10^3/µL',
|
||||
'normal_min': 4.5,
|
||||
'normal_max': 11.0,
|
||||
'notes': 'Valor elevado - posible infección'
|
||||
},
|
||||
{
|
||||
'test_id': test1.id,
|
||||
'parameter_name': 'Hemoglobina',
|
||||
'sequence': 30,
|
||||
'value_numeric': 14.2,
|
||||
'unit': 'g/dL',
|
||||
'normal_min': 12.0,
|
||||
'normal_max': 16.0
|
||||
},
|
||||
{
|
||||
'test_id': test1.id,
|
||||
'parameter_name': 'Plaquetas',
|
||||
'sequence': 40,
|
||||
'value_numeric': 250,
|
||||
'unit': '10^3/µL',
|
||||
'normal_min': 150,
|
||||
'normal_max': 400
|
||||
}
|
||||
]
|
||||
|
||||
for result_data in results_data:
|
||||
env['lims.result'].create(result_data)
|
||||
|
||||
print(f"Creada prueba {test1.name} con 4 resultados")
|
||||
|
||||
# Crear prueba 2: Glucosa con resultado ingresado
|
||||
if len(order_lines) > 1:
|
||||
test2 = env['lims.test'].create({
|
||||
'sale_order_line_id': order_lines[1].id,
|
||||
'sample_id': samples[0].id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
test2.action_start_process()
|
||||
|
||||
# Crear resultado de glucosa
|
||||
env['lims.result'].create({
|
||||
'test_id': test2.id,
|
||||
'parameter_name': 'Glucosa en Ayunas',
|
||||
'sequence': 10,
|
||||
'value_numeric': 125, # Fuera de rango
|
||||
'unit': 'mg/dL',
|
||||
'normal_min': 70,
|
||||
'normal_max': 110,
|
||||
'notes': 'Valor elevado - prediabetes'
|
||||
})
|
||||
|
||||
# Marcar resultados como ingresados
|
||||
test2.action_enter_results()
|
||||
|
||||
print(f"Creada prueba {test2.name} con resultado ingresado")
|
||||
|
||||
# Crear prueba 3: Uroanálisis con valores mixtos (si hay más pacientes)
|
||||
if len(patients) > 1 and len(samples) > 1:
|
||||
# Crear una orden adicional
|
||||
urine_analysis = env['product.template'].search([
|
||||
('is_analysis', '=', True),
|
||||
('name', 'ilike', 'orina')
|
||||
], limit=1)
|
||||
|
||||
if urine_analysis:
|
||||
lab_order2 = env['sale.order'].create({
|
||||
'partner_id': patients[1].id,
|
||||
'is_lab_request': True,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': urine_analysis.product_variant_id.id,
|
||||
'product_uom_qty': 1
|
||||
})]
|
||||
})
|
||||
lab_order2.action_confirm()
|
||||
|
||||
test3 = env['lims.test'].create({
|
||||
'sale_order_line_id': lab_order2.order_line[0].id,
|
||||
'sample_id': samples[1].id,
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
test3.action_start_process()
|
||||
|
||||
# Crear resultados mixtos
|
||||
urine_results = [
|
||||
{
|
||||
'test_id': test3.id,
|
||||
'parameter_name': 'Color',
|
||||
'sequence': 10,
|
||||
'value_text': 'Amarillo claro'
|
||||
},
|
||||
{
|
||||
'test_id': test3.id,
|
||||
'parameter_name': 'pH',
|
||||
'sequence': 20,
|
||||
'value_numeric': 6.5,
|
||||
'normal_min': 4.6,
|
||||
'normal_max': 8.0
|
||||
},
|
||||
{
|
||||
'test_id': test3.id,
|
||||
'parameter_name': 'Densidad',
|
||||
'sequence': 30,
|
||||
'value_numeric': 1.020,
|
||||
'normal_min': 1.005,
|
||||
'normal_max': 1.030
|
||||
},
|
||||
{
|
||||
'test_id': test3.id,
|
||||
'parameter_name': 'Proteínas',
|
||||
'sequence': 40,
|
||||
'value_text': 'Negativo'
|
||||
},
|
||||
{
|
||||
'test_id': test3.id,
|
||||
'parameter_name': 'Glucosa',
|
||||
'sequence': 50,
|
||||
'value_text': 'Negativo'
|
||||
}
|
||||
]
|
||||
|
||||
for result_data in urine_results:
|
||||
env['lims.result'].create(result_data)
|
||||
|
||||
# Ingresar y validar resultados
|
||||
test3.action_enter_results()
|
||||
if test3.state == 'result_entered':
|
||||
test3.action_validate()
|
||||
|
||||
print(f"Creada prueba {test3.name} validada con resultados mixtos")
|
||||
|
||||
print("\nDatos de demostración de pruebas creados exitosamente")
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_name = 'lims_demo'
|
||||
registry = odoo.registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
create_test_demo_data(cr)
|
||||
cr.commit()
|
Loading…
Reference in New Issue
Block a user