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:
parent
7e2dfb6465
commit
a1b8f7b1de
11
CLAUDE.md
11
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
|
||||
|
|
|
@ -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',
|
||||
|
|
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,5 @@ from . import product
|
|||
from . import partner
|
||||
from . import sale_order
|
||||
from . import stock_lot
|
||||
from . import lims_test
|
||||
from . import lims_result
|
||||
|
|
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.')
|
||||
)
|
207
lims_management/models/lims_test.py
Normal file
207
lims_management/models/lims_test.py
Normal 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
|
|
@ -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
|
||||
|
|
|
Loading…
Reference in New Issue
Block a user