From a77e848922259ec8470106921e50ecf94e3e97d6 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Wed, 16 Jul 2025 09:44:19 -0600 Subject: [PATCH] =?UTF-8?q?fix(#60):=20Mejorar=20estructura=20de=20re-mues?= =?UTF-8?q?treos=20para=20mantener=20jerarqu=C3=ADa=20plana?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Las re-muestras de re-muestras ahora se crean como hijas del padre original - Evita nombres anidados como RE-RE-0000002-1 - Mantiene estructura más clara: todas las re-muestras son hermanas - Actualiza mensajes para reflejar la muestra rechazada que originó la re-muestra - Mejora trazabilidad manteniendo referencia clara al origen 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../__pycache__/stock_lot.cpython-312.pyc | Bin 21193 -> 24133 bytes lims_management/models/stock_lot.py | 37 ++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index 94794ea4c0283ab73c6f8e4a4cfba2e5ef29d7e6..120db7342250e84e9d14a64826fb91ae9a29aaf4 100644 GIT binary patch delta 6132 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 PKXghxq5eanwyyeLBpOww diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index 0cd362a..dfe9ef6 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -456,13 +456,18 @@ class StockLot(models.Model): """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( + # Determine the parent sample for the new resample + # If current sample is already a resample, use its parent + # Otherwise, use the current sample as parent + parent_for_resample = self.parent_sample_id if self.parent_sample_id else self + + # Check if there's already an active resample for the parent + active_resamples = parent_for_resample.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'))) + raise UserError(_('La muestra %s ya tiene una re-muestra activa (%s). No se puede crear otra hasta que se procese o rechace la existente.') % + (parent_for_resample.name, ', '.join(active_resamples.mapped('name')))) # Get configuration IrConfig = self.env['ir.config_parameter'].sudo() @@ -472,7 +477,7 @@ class StockLot(models.Model): max_attempts = int(IrConfig.get_param('lims_management.max_resample_attempts', '3')) # Find the original sample (root of the resample chain) - original_sample = self + original_sample = parent_for_resample while original_sample.parent_sample_id: original_sample = original_sample.parent_sample_id @@ -483,12 +488,12 @@ class StockLot(models.Model): 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 + # Calculate resample number for naming (based on parent's resample count) + resample_number = len(parent_for_resample.child_sample_ids) + 1 # Prepare values for new sample vals = { - 'name': f"{prefix}{self.name}-{resample_number}", + 'name': f"{prefix}{parent_for_resample.name}-{resample_number}", 'product_id': self.product_id.id, 'patient_id': self.patient_id.id, 'doctor_id': self.doctor_id.id, @@ -498,23 +503,31 @@ class StockLot(models.Model): 'is_lab_sample': True, 'state': initial_state, 'analysis_names': self.analysis_names, - 'parent_sample_id': self.id, + 'parent_sample_id': parent_for_resample.id, # Always use the determined parent 'request_id': self.request_id.id if self.request_id else False, } # Create the resample resample = self.create(vals) - # Post message in both samples + # Post message in all relevant samples self.message_post( body=_('Re-muestra creada: %s') % resample.name, subject='Re-muestreo', message_type='notification' ) + if self != parent_for_resample: + # If we're creating from a resample, also notify the parent + parent_for_resample.message_post( + body=_('Nueva re-muestra creada: %s (debido al rechazo de %s)') % (resample.name, self.name), + subject='Re-muestreo', + message_type='notification' + ) + resample.message_post( - body=_('Esta es una re-muestra de: %s
Motivo: %s') % - (self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'), + body=_('Esta es una re-muestra de: %s
Creada debido al rechazo de: %s
Motivo: %s') % + (parent_for_resample.name, self.name, self.rejection_reason_id.name if self.rejection_reason_id else 'No especificado'), subject='Re-muestra creada', message_type='notification' )