feat(#60): Mejorar control y trazabilidad de re-muestreos

- Respetar configuración del wizard (checkbox crear re-muestra)
- Prevenir creación de múltiples re-muestras activas
- Agregar campos para trazabilidad completa:
  - root_sample_id: muestra original de la cadena
  - resample_chain_count: total de re-muestreos en cadena
- Validar límite de re-muestreos por cadena completa
- Mejorar vista con información de trazabilidad
- Método auxiliar para contar re-muestreos recursivamente

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-16 09:12:28 -06:00
parent be6c97cfad
commit 3e97c9f418
4 changed files with 95 additions and 16 deletions

View File

@ -131,6 +131,18 @@ class StockLot(models.Model):
compute='_compute_is_resample',
store=True
)
root_sample_id = fields.Many2one(
'stock.lot',
string='Muestra Original (Raíz)',
compute='_compute_root_sample',
store=True,
help='Muestra original de la cadena de re-muestreos'
)
resample_chain_count = fields.Integer(
string='Re-muestreos en Cadena',
compute='_compute_resample_chain_count',
help='Número total de re-muestreos en toda la cadena'
)
def action_collect(self):
"""Mark sample as collected"""
@ -216,8 +228,12 @@ class StockLot(models.Model):
}
}
def action_reject(self):
"""Reject the sample - to be called from wizard"""
def action_reject(self, create_resample=None):
"""Reject the sample - to be called from wizard
Args:
create_resample: Boolean to force resample creation. If None, uses system config
"""
self.ensure_one()
if self.state == 'completed':
raise ValueError('No se puede rechazar una muestra ya completada')
@ -250,11 +266,19 @@ class StockLot(models.Model):
message_type='notification'
)
# Check if automatic resample is enabled
IrConfig = self.env['ir.config_parameter'].sudo()
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
# Determine if we should create a resample
should_create_resample = False
if auto_resample:
if create_resample is not None:
# Explicit value from wizard
should_create_resample = create_resample
else:
# Check system configuration
IrConfig = self.env['ir.config_parameter'].sudo()
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
should_create_resample = auto_resample
if should_create_resample:
try:
# Create resample automatically
resample_action = self.action_create_resample()
@ -408,10 +432,38 @@ class StockLot(models.Model):
for record in self:
record.resample_count = len(record.child_sample_ids)
@api.depends('parent_sample_id')
def _compute_root_sample(self):
"""Compute the root sample of the resample chain"""
for record in self:
root = record
while root.parent_sample_id:
root = root.parent_sample_id
record.root_sample_id = root if root != record else False
@api.depends('parent_sample_id', 'child_sample_ids')
def _compute_resample_chain_count(self):
"""Compute total resamples in the entire chain"""
for record in self:
# Find root sample
root = record
while root.parent_sample_id:
root = root.parent_sample_id
# Count all resamples from root
record.resample_chain_count = self._count_all_resamples_in_chain(root)
def action_create_resample(self):
"""Create a new sample as a resample of the current one"""
self.ensure_one()
# Check if there's already an active resample
active_resamples = self.child_sample_ids.filtered(
lambda s: s.state not in ['rejected', 'cancelled', 'disposed']
)
if active_resamples:
raise UserError(_('Esta muestra ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') %
', '.join(active_resamples.mapped('name')))
# Get configuration
IrConfig = self.env['ir.config_parameter'].sudo()
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
@ -419,9 +471,17 @@ class StockLot(models.Model):
prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-')
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
# Check maximum resample attempts
if max_attempts > 0 and self.resample_count >= max_attempts:
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta muestra.') % max_attempts)
# Find the original sample (root of the resample chain)
original_sample = self
while original_sample.parent_sample_id:
original_sample = original_sample.parent_sample_id
# Count all resamples in the chain
total_resamples = self._count_all_resamples_in_chain(original_sample)
# Check maximum resample attempts based on the entire chain
if max_attempts > 0 and total_resamples >= max_attempts:
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta cadena de muestras.') % max_attempts)
# Calculate resample number for naming
resample_number = self.resample_count + 1
@ -483,6 +543,20 @@ class StockLot(models.Model):
'target': 'current',
}
def _count_all_resamples_in_chain(self, root_sample):
"""Count all resamples in the entire chain starting from root"""
count = 0
samples_to_check = [root_sample]
while samples_to_check:
sample = samples_to_check.pop(0)
# Add all child samples to the check list
for child in sample.child_sample_ids:
count += 1
samples_to_check.append(child)
return count
def _notify_resample_created(self, resample):
"""Notify receptionist users about the created resample"""
# Find receptionist users

View File

@ -91,10 +91,12 @@
</group>
<notebook>
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
<group>
<group col="4">
<field name="is_resample" invisible="1"/>
<field name="resample_count" invisible="1"/>
<field name="parent_sample_id" readonly="1" invisible="not is_resample"/>
<field name="root_sample_id" readonly="1" invisible="not is_resample"/>
<field name="resample_chain_count" readonly="1" invisible="resample_chain_count == 0"/>
</group>
<group string="Re-muestras Generadas" invisible="resample_count == 0">
<field name="child_sample_ids" nolabel="1">
@ -103,9 +105,16 @@
<field name="state" widget="badge"/>
<field name="collection_date"/>
<field name="rejection_reason_id"/>
<field name="resample_count" string="Re-muestras propias"/>
</list>
</field>
</group>
<group string="Información de Trazabilidad" invisible="not is_resample">
<div class="alert alert-info" role="alert">
<p><i class="fa fa-info-circle"/> Esta muestra es parte de una cadena de re-muestreo.</p>
<p>Total de re-muestreos en la cadena: <field name="resample_chain_count" readonly="1" nolabel="1" class="oe_inline"/></p>
</div>
</group>
</page>
</notebook>
</sheet>

View File

@ -67,12 +67,8 @@ class SampleRejectionWizard(models.TransientModel):
'rejection_notes': self.rejection_notes
})
# Call the rejection method on the sample
self.sample_id.action_reject()
# Create new sample request if needed
if self.create_new_sample and self.sample_id.request_id:
self._create_new_sample_request()
# Call the rejection method on the sample with explicit resample creation preference
self.sample_id.action_reject(create_resample=self.create_new_sample)
return {'type': 'ir.actions.act_window_close'}