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:
parent
be6c97cfad
commit
3e97c9f418
Binary file not shown.
|
@ -131,6 +131,18 @@ class StockLot(models.Model):
|
||||||
compute='_compute_is_resample',
|
compute='_compute_is_resample',
|
||||||
store=True
|
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):
|
def action_collect(self):
|
||||||
"""Mark sample as collected"""
|
"""Mark sample as collected"""
|
||||||
|
@ -216,8 +228,12 @@ class StockLot(models.Model):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def action_reject(self):
|
def action_reject(self, create_resample=None):
|
||||||
"""Reject the sample - to be called from wizard"""
|
"""Reject the sample - to be called from wizard
|
||||||
|
|
||||||
|
Args:
|
||||||
|
create_resample: Boolean to force resample creation. If None, uses system config
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if self.state == 'completed':
|
if self.state == 'completed':
|
||||||
raise ValueError('No se puede rechazar una muestra ya completada')
|
raise ValueError('No se puede rechazar una muestra ya completada')
|
||||||
|
@ -250,11 +266,19 @@ class StockLot(models.Model):
|
||||||
message_type='notification'
|
message_type='notification'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if automatic resample is enabled
|
# Determine if we should create a resample
|
||||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
should_create_resample = False
|
||||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
|
||||||
|
|
||||||
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:
|
try:
|
||||||
# Create resample automatically
|
# Create resample automatically
|
||||||
resample_action = self.action_create_resample()
|
resample_action = self.action_create_resample()
|
||||||
|
@ -408,10 +432,38 @@ class StockLot(models.Model):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.resample_count = len(record.child_sample_ids)
|
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):
|
def action_create_resample(self):
|
||||||
"""Create a new sample as a resample of the current one"""
|
"""Create a new sample as a resample of the current one"""
|
||||||
self.ensure_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
|
# Get configuration
|
||||||
IrConfig = self.env['ir.config_parameter'].sudo()
|
IrConfig = self.env['ir.config_parameter'].sudo()
|
||||||
auto_resample = IrConfig.get_param('lims_management.auto_resample_on_rejection', 'True') == 'True'
|
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-')
|
prefix = IrConfig.get_param('lims_management.resample_prefix', 'RE-')
|
||||||
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
|
max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3'))
|
||||||
|
|
||||||
# Check maximum resample attempts
|
# Find the original sample (root of the resample chain)
|
||||||
if max_attempts > 0 and self.resample_count >= max_attempts:
|
original_sample = self
|
||||||
raise UserError(_('Se ha alcanzado el número máximo de re-muestreos (%d) para esta muestra.') % max_attempts)
|
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
|
# Calculate resample number for naming
|
||||||
resample_number = self.resample_count + 1
|
resample_number = self.resample_count + 1
|
||||||
|
@ -483,6 +543,20 @@ class StockLot(models.Model):
|
||||||
'target': 'current',
|
'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):
|
def _notify_resample_created(self, resample):
|
||||||
"""Notify receptionist users about the created resample"""
|
"""Notify receptionist users about the created resample"""
|
||||||
# Find receptionist users
|
# Find receptionist users
|
||||||
|
|
|
@ -91,10 +91,12 @@
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
|
<page string="Re-muestreo" invisible="not is_resample and resample_count == 0">
|
||||||
<group>
|
<group col="4">
|
||||||
<field name="is_resample" invisible="1"/>
|
<field name="is_resample" invisible="1"/>
|
||||||
<field name="resample_count" invisible="1"/>
|
<field name="resample_count" invisible="1"/>
|
||||||
<field name="parent_sample_id" readonly="1" invisible="not is_resample"/>
|
<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>
|
||||||
<group string="Re-muestras Generadas" invisible="resample_count == 0">
|
<group string="Re-muestras Generadas" invisible="resample_count == 0">
|
||||||
<field name="child_sample_ids" nolabel="1">
|
<field name="child_sample_ids" nolabel="1">
|
||||||
|
@ -103,9 +105,16 @@
|
||||||
<field name="state" widget="badge"/>
|
<field name="state" widget="badge"/>
|
||||||
<field name="collection_date"/>
|
<field name="collection_date"/>
|
||||||
<field name="rejection_reason_id"/>
|
<field name="rejection_reason_id"/>
|
||||||
|
<field name="resample_count" string="Re-muestras propias"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</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>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
|
|
@ -67,12 +67,8 @@ class SampleRejectionWizard(models.TransientModel):
|
||||||
'rejection_notes': self.rejection_notes
|
'rejection_notes': self.rejection_notes
|
||||||
})
|
})
|
||||||
|
|
||||||
# Call the rejection method on the sample
|
# Call the rejection method on the sample with explicit resample creation preference
|
||||||
self.sample_id.action_reject()
|
self.sample_id.action_reject(create_resample=self.create_new_sample)
|
||||||
|
|
||||||
# Create new sample request if needed
|
|
||||||
if self.create_new_sample and self.sample_id.request_id:
|
|
||||||
self._create_new_sample_request()
|
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user