feat(#8): Task 1 y 2 completadas - Crear modelos lims.test y lims.result

- Modelo lims.test con todos los campos especificados
- Modelo lims.result con soporte para múltiples tipos de valor
- Secuencia automática para códigos de prueba
- Flujo de estados: draft -> in_process -> result_entered -> validated
- Validación de un solo tipo de valor por resultado
- Permisos de seguridad configurados
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-15 00:36:58 -06:00
parent 7e2dfb6465
commit a1b8f7b1de
11 changed files with 362 additions and 0 deletions

View File

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

View File

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

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

View File

@ -4,3 +4,5 @@ from . import product
from . import partner
from . import sale_order
from . import stock_lot
from . import lims_test
from . import lims_result

View 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.')
)

View File

@ -0,0 +1,207 @@
# -*- 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)]",
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."""
for record in self:
# Por ahora, siempre requiere validación
# TODO: Implementar cuando se agregue res.config.settings
record.require_validation = True
@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.'))
self.state = 'result_entered'
# Log en el chatter
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

View File

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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_lims_analysis_range_user lims.analysis.range.user model_lims_analysis_range base.group_user 1 1 1 1
3 access_sale_order_receptionist sale.order.receptionist sale.model_sale_order group_lims_receptionist 1 1 1 0
4 access_stock_lot_user stock.lot.user stock.model_stock_lot base.group_user 1 1 1 1
5 access_lims_test_user lims.test.user model_lims_test base.group_user 1 1 1 1
6 access_lims_result_user lims.result.user model_lims_result base.group_user 1 1 1 1