feat(#9): Implementar flujo de validación y seguridad
- Ajustar permisos base: recepcionistas solo lectura, técnicos sin eliminar - Crear reglas de registro para control granular por estado - Implementar verificación de permisos en todas las transiciones - Agregar mail.thread a stock.lot para trazabilidad completa - Validar transiciones de estado y muestras asociadas - Actualizar vistas con restricciones según grupos de usuario - Mejorar mensajes del chatter con más contexto Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
39318f9073
commit
58e1648493
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
@ -190,6 +190,12 @@ class LimsTest(models.Model):
|
|||
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:
|
||||
|
@ -203,20 +209,44 @@ class LimsTest(models.Model):
|
|||
# Log en el chatter
|
||||
self.message_post(
|
||||
body=_('Prueba iniciada por %s') % self.env.user.name,
|
||||
subject=_('Proceso Iniciado')
|
||||
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_analysis'})
|
||||
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_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({
|
||||
|
@ -226,13 +256,15 @@ class LimsTest(models.Model):
|
|||
})
|
||||
self.message_post(
|
||||
body=_('Resultados ingresados y auto-validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados')
|
||||
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')
|
||||
subject=_('Resultados Ingresados'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -240,10 +272,23 @@ class LimsTest(models.Model):
|
|||
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.'))
|
||||
|
||||
# TODO: Verificar permisos cuando se implemente seguridad
|
||||
# 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_out_of_range and result.parameter_id.is_critical:
|
||||
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',
|
||||
|
@ -251,26 +296,56 @@ class LimsTest(models.Model):
|
|||
'validation_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Log en el chatter
|
||||
# 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=_('Resultados validados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Validados')
|
||||
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()
|
||||
if self.state == 'validated':
|
||||
raise UserError(_('No se pueden cancelar pruebas validadas.'))
|
||||
|
||||
# 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
|
||||
# Log en el chatter con el estado anterior
|
||||
self.message_post(
|
||||
body=_('Prueba cancelada por %s') % self.env.user.name,
|
||||
subject=_('Prueba Cancelada')
|
||||
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
|
||||
|
@ -278,6 +353,12 @@ class LimsTest(models.Model):
|
|||
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.'))
|
||||
|
||||
|
@ -292,7 +373,8 @@ class LimsTest(models.Model):
|
|||
|
||||
self.message_post(
|
||||
body=_('Resultados regenerados por %s') % self.env.user.name,
|
||||
subject=_('Resultados Regenerados')
|
||||
subject=_('Resultados Regenerados'),
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -300,9 +382,98 @@ class LimsTest(models.Model):
|
|||
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'
|
||||
|
||||
return True
|
||||
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"""
|
||||
# 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'))
|
||||
|
||||
return super().create(vals)
|
||||
|
||||
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
|
|
@ -4,7 +4,8 @@ from datetime import datetime
|
|||
import random
|
||||
|
||||
class StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
_name = 'stock.lot'
|
||||
_inherit = ['stock.lot', 'mail.thread', 'mail.activity.mixin']
|
||||
|
||||
is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio')
|
||||
|
||||
|
@ -86,31 +87,73 @@ class StockLot(models.Model):
|
|||
|
||||
def action_collect(self):
|
||||
"""Mark sample as collected"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'collected', 'collection_date': fields.Datetime.now()})
|
||||
self.message_post(
|
||||
body='Muestra recolectada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recolectada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_receive(self):
|
||||
"""Mark sample as received in laboratory"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'received'})
|
||||
self.message_post(
|
||||
body='Muestra recibida en laboratorio por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Recibida',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_start_analysis(self):
|
||||
"""Start analysis process"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'in_process'})
|
||||
self.message_post(
|
||||
body='Análisis iniciado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: En Proceso',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_complete_analysis(self):
|
||||
"""Mark analysis as completed"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'analyzed'})
|
||||
self.message_post(
|
||||
body='Análisis completado por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Analizada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_store(self):
|
||||
"""Store the sample"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'stored'})
|
||||
self.message_post(
|
||||
body='Muestra almacenada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Almacenada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
def action_dispose(self):
|
||||
"""Dispose of the sample"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'disposed'})
|
||||
self.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"""
|
||||
old_state = self.state
|
||||
self.write({'state': 'cancelled'})
|
||||
self.message_post(
|
||||
body='Muestra cancelada por %s' % self.env.user.name,
|
||||
subject='Estado actualizado: Cancelada',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
@api.onchange('sample_type_product_id')
|
||||
def _onchange_sample_type_product_id(self):
|
||||
|
|
|
@ -7,5 +7,9 @@ access_lims_parameter_range_user,lims.parameter.range.user,model_lims_parameter_
|
|||
access_lims_parameter_range_manager,lims.parameter.range.manager,model_lims_parameter_range,group_lims_admin,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
|
||||
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
|
||||
|
|
|
|
@ -33,5 +33,81 @@
|
|||
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>
|
||||
|
|
|
@ -11,23 +11,29 @@
|
|||
<header>
|
||||
<button name="action_start_process" string="Iniciar Proceso"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="state != 'draft'"/>
|
||||
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'"/>
|
||||
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"/>
|
||||
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']"/>
|
||||
invisible="state in ['validated', 'cancelled']"
|
||||
groups="lims_management.group_lims_technician"/>
|
||||
<button name="action_draft" string="Volver a Borrador"
|
||||
type="object"
|
||||
invisible="state != 'cancelled'"/>
|
||||
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."/>
|
||||
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>
|
||||
|
@ -58,7 +64,7 @@
|
|||
<notebook>
|
||||
<page string="Resultados">
|
||||
<field name="result_ids"
|
||||
readonly="state in ['validated', 'cancelled']"
|
||||
readonly="state in ['validated', 'cancelled'] or not env.user.has_group('lims_management.group_lims_technician')"
|
||||
context="{'default_test_id': id, 'default_patient_id': patient_id, 'default_test_date': create_date}"
|
||||
mode="list">
|
||||
<list string="Resultados" editable="bottom"
|
||||
|
|
74
pr_body_54.txt
Normal file
74
pr_body_54.txt
Normal file
|
@ -0,0 +1,74 @@
|
|||
## Descripción
|
||||
|
||||
Implementación de la funcionalidad para cancelar automáticamente muestras y pruebas cuando se cancela una orden de laboratorio, evitando que queden elementos "huérfanos" en el sistema.
|
||||
|
||||
## 🎯 Objetivo
|
||||
|
||||
Resolver el issue #54: Las muestras y pruebas asociadas a una orden de laboratorio deben cancelarse automáticamente cuando se cancela la orden.
|
||||
|
||||
## 🔧 Cambios implementados
|
||||
|
||||
### 1. Modelo `stock.lot` (Muestras)
|
||||
- Agregado nuevo estado `'cancelled'` a la selección de estados
|
||||
- Implementado método `action_cancel()` para cambiar el estado a cancelado
|
||||
|
||||
### 2. Modelo `sale.order` (Órdenes)
|
||||
- Override del método `action_cancel()` que:
|
||||
- Llama primero al método padre para mantener el comportamiento estándar
|
||||
- Si es una orden de laboratorio (`is_lab_request = True`):
|
||||
- Cancela muestras en estados: `pending_collection`, `collected`, `received`, `in_process`
|
||||
- Cancela pruebas asociadas que no estén en estado `validated` o `cancelled`
|
||||
- Registra mensajes en el chatter de cada elemento cancelado
|
||||
- Muestra un resumen en la orden con la cantidad de elementos cancelados
|
||||
|
||||
### 3. Tests unitarios
|
||||
- Creado `test_order_cancel_cascade.py` con 6 tests que verifican:
|
||||
- ✅ Cancelación correcta de muestras
|
||||
- ✅ Cancelación correcta de pruebas
|
||||
- ✅ No cancelación de muestras en estados finales (analyzed, stored, disposed)
|
||||
- ✅ No cancelación de pruebas validadas
|
||||
- ✅ Generación de mensajes en chatter
|
||||
- ✅ Órdenes normales (no laboratorio) no afectadas
|
||||
|
||||
## 🧪 Pruebas realizadas
|
||||
|
||||
### Test manual exitoso:
|
||||
```
|
||||
📦 Muestras generadas:
|
||||
- 0000012: Contenedor de Heces (Estado: pending_collection)
|
||||
|
||||
🔬 Pruebas generadas:
|
||||
- LAB-2025-00014: Coprocultivo (Estado: draft)
|
||||
|
||||
❌ Cancelando la orden de laboratorio...
|
||||
|
||||
📦 Estado final de las muestras:
|
||||
- 0000012: cancelled ✓
|
||||
|
||||
🔬 Estado final de las pruebas:
|
||||
- LAB-2025-00014: cancelled ✓
|
||||
|
||||
✅ Mensajes de cancelación registrados en todos los elementos
|
||||
```
|
||||
|
||||
## 📋 Checklist
|
||||
|
||||
- [x] Código implementado y probado
|
||||
- [x] Tests unitarios creados
|
||||
- [x] Pruebas manuales exitosas
|
||||
- [x] Mensajes en chatter funcionando
|
||||
- [x] Sin errores o warnings
|
||||
- [x] Documentación en código
|
||||
|
||||
## 🔍 Cómo probar
|
||||
|
||||
1. Crear una orden de laboratorio con análisis
|
||||
2. Confirmar la orden (se generan muestras y pruebas)
|
||||
3. Opcionalmente iniciar proceso en alguna prueba
|
||||
4. Cancelar la orden
|
||||
5. Verificar que:
|
||||
- Las muestras cambiaron a estado "Cancelada"
|
||||
- Las pruebas cambiaron a estado "Cancelado"
|
||||
- Hay mensajes en el chatter explicando la cancelación
|
||||
|
||||
Resuelve #54
|
Loading…
Reference in New Issue
Block a user