# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import UserError import logging _logger = logging.getLogger(__name__) class SaleOrder(models.Model): _inherit = 'sale.order' is_lab_request = fields.Boolean( string="Es Orden de Laboratorio", default=False, copy=False, help="Campo técnico para identificar si la orden de venta es una solicitud de laboratorio." ) doctor_id = fields.Many2one( 'res.partner', string="Médico Referente", domain="[('is_doctor', '=', True)]", help="El médico que refirió al paciente para esta solicitud de laboratorio." ) generated_sample_ids = fields.Many2many( 'stock.lot', 'sale_order_stock_lot_rel', 'order_id', 'lot_id', string='Muestras Generadas', domain="[('is_lab_sample', '=', True)]", readonly=True, help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden" ) all_sample_ids = fields.Many2many( 'stock.lot', string='Todas las Muestras (inc. Re-muestras)', compute='_compute_all_samples', help="Todas las muestras relacionadas con esta orden, incluyendo re-muestras" ) @api.depends('generated_sample_ids', 'generated_sample_ids.child_sample_ids') def _compute_all_samples(self): """Compute all samples including resamples""" for order in self: all_samples = order.generated_sample_ids # Add all resamples recursively resamples = self.env['stock.lot'] for sample in order.generated_sample_ids: resamples |= self._get_all_resamples(sample) order.all_sample_ids = all_samples | resamples def _get_all_resamples(self, sample): """Recursively get all resamples of a sample""" resamples = sample.child_sample_ids for resample in sample.child_sample_ids: resamples |= self._get_all_resamples(resample) return resamples def action_confirm(self): """Override to generate laboratory samples and tests automatically""" res = super(SaleOrder, self).action_confirm() # Generate samples and tests only for laboratory requests for order in self.filtered('is_lab_request'): try: order._generate_lab_samples() order._generate_lab_tests() except Exception as e: _logger.error(f"Error generating samples/tests for order {order.name}: {str(e)}") # Continue with order confirmation even if generation fails # But notify the user order.message_post( body=_("Error al generar muestras/pruebas automáticamente: %s. " "Por favor, genere las muestras y pruebas manualmente.") % str(e), message_type='notification' ) return res def _generate_lab_samples(self): """Generate laboratory samples based on the analyses in the order""" self.ensure_one() _logger.info(f"Generating laboratory samples for order {self.name}") # Group analyses by sample type sample_groups = self._group_analyses_by_sample_type() if not sample_groups: _logger.warning(f"No analyses with sample types found in order {self.name}") return # Create samples for each group created_samples = self.env['stock.lot'] for sample_type_id, group_data in sample_groups.items(): sample = self._create_sample_for_group(group_data) if sample: created_samples |= sample # Link created samples to the order if created_samples: self.generated_sample_ids = [(6, 0, created_samples.ids)] _logger.info(f"Created {len(created_samples)} samples for order {self.name}") # Post message with created samples sample_list = "" self.message_post( body=_("Muestras generadas automáticamente: %s") % sample_list, message_type='notification' ) def _group_analyses_by_sample_type(self): """Group order lines by required sample type""" groups = {} for line in self.order_line: product = line.product_id # Skip non-analysis products if not product.is_analysis: continue # Check if analysis has a required sample type if not product.required_sample_type_id: _logger.warning( f"Analysis {product.name} has no required sample type defined" ) # Post warning message self.message_post( body=_("Advertencia: El análisis '%s' no tiene tipo de muestra definido") % product.name, message_type='notification' ) continue sample_type = product.required_sample_type_id # Initialize group if not exists if sample_type.id not in groups: groups[sample_type.id] = { 'sample_type': sample_type, 'lines': [], 'total_volume': 0.0, 'analyses': [] } # Add line to group groups[sample_type.id]['lines'].append(line) groups[sample_type.id]['analyses'].append(product.name) groups[sample_type.id]['total_volume'] += (product.sample_volume_ml or 0.0) * line.product_uom_qty return groups def _create_sample_for_group(self, group_data): """Create a single sample for a group of analyses""" try: sample_type = group_data['sample_type'] # Generate a unique lot name using sequence sequence = self.env['ir.sequence'].next_by_code('stock.lot.serial') if not sequence: # Fallback to timestamp-based name if no sequence exists import time sequence = 'LAB-' + str(int(time.time()))[-8:] # Prepare sample values vals = { 'name': sequence, # Add the lot name 'product_id': sample_type.product_variant_id.id, 'patient_id': self.partner_id.id, 'doctor_id': self.doctor_id.id if self.doctor_id else False, 'origin': self.name, 'sample_type_product_id': sample_type.id, 'volume_ml': group_data['total_volume'], 'is_lab_sample': True, 'state': 'pending_collection', 'analysis_names': ', '.join(group_data['analyses'][:3]) + ('...' if len(group_data['analyses']) > 3 else '') } # Create the sample sample = self.env['stock.lot'].create(vals) _logger.info( f"Created sample {sample.name} for {len(group_data['analyses'])} analyses" ) return sample except Exception as e: _logger.error(f"Error creating sample: {str(e)}") raise UserError( _("Error al crear muestra para %s: %s") % (sample_type.name, str(e)) ) def _generate_lab_tests(self): """Generate laboratory tests for analysis order lines""" self.ensure_one() _logger.info(f"Generating laboratory tests for order {self.name}") # Get the test model TestModel = self.env['lims.test'] created_tests = TestModel.browse() # Create a test for each analysis line for line in self.order_line: if not line.product_id.is_analysis: continue # Find appropriate sample for this analysis sample = self._find_sample_for_analysis(line.product_id) if not sample: _logger.warning( f"No sample found for analysis {line.product_id.name} in order {self.name}" ) self.message_post( body=_("Advertencia: No se encontró muestra para el análisis '%s'") % line.product_id.name, message_type='notification' ) continue # Create the test try: test = TestModel.create({ 'sale_order_line_id': line.id, 'sample_id': sample.id, }) created_tests |= test _logger.info(f"Created test {test.name} for analysis {line.product_id.name}") except Exception as e: _logger.error(f"Error creating test for {line.product_id.name}: {str(e)}") self.message_post( body=_("Error al crear prueba para '%s': %s") % (line.product_id.name, str(e)), message_type='notification' ) # Post message with created tests if created_tests: test_list = "" self.message_post( body=_("Pruebas generadas automáticamente: %s") % test_list, message_type='notification' ) _logger.info(f"Created {len(created_tests)} tests for order {self.name}") def _find_sample_for_analysis(self, product): """Find the appropriate sample for an analysis product""" # Check if the analysis has a required sample type if not product.required_sample_type_id: return False # Find a generated sample with matching sample type for sample in self.generated_sample_ids: if sample.sample_type_product_id.id == product.required_sample_type_id.id: return sample return False def action_cancel(self): """Override para cancelar automáticamente muestras y pruebas asociadas cuando se cancela una orden de laboratorio""" # Primero llamar al método padre res = super(SaleOrder, self).action_cancel() # Si es una orden de laboratorio, cancelar muestras y pruebas asociadas if self.is_lab_request: # Cancelar muestras que estén en estados cancelables cancelable_sample_states = ['pending_collection', 'collected', 'received', 'in_process'] samples_to_cancel = self.generated_sample_ids.filtered( lambda s: s.state in cancelable_sample_states ) if samples_to_cancel: # Cancelar las muestras samples_to_cancel.action_cancel() # Registrar en el chatter de cada muestra for sample in samples_to_cancel: sample.message_post( body=_("Muestra cancelada automáticamente debido a la cancelación de la orden %s") % self.name, message_type='notification' ) # Buscar y cancelar pruebas asociadas a estas muestras tests_to_cancel = self.env['lims.test'].search([ ('sample_id', 'in', samples_to_cancel.ids), ('state', 'not in', ['validated', 'cancelled']) ]) if tests_to_cancel: for test in tests_to_cancel: test.action_cancel() test.message_post( body=_("Prueba cancelada automáticamente debido a la cancelación de la orden %s") % self.name, message_type='notification' ) # Registrar en el chatter de la orden message = _("Se cancelaron automáticamente:
") message += _("- %d muestras
") % len(samples_to_cancel) if tests_to_cancel: message += _("- %d pruebas de laboratorio") % len(tests_to_cancel) self.message_post( body=message, message_type='notification' ) _logger.info(f"Cancelled {len(samples_to_cancel)} samples and {len(tests_to_cancel)} tests for order {self.name}") return res def action_print_sample_labels(self): """Imprimir etiquetas de todas las muestras generadas para esta orden""" self.ensure_one() if not self.generated_sample_ids: raise UserError(_('No hay muestras generadas para esta orden. Por favor, confirme la orden primero.')) # Asegurar que todas las muestras tengan código de barras self.generated_sample_ids._ensure_barcode() # Obtener el reporte report = self.env.ref('lims_management.action_report_sample_label') # Retornar la acción de imprimir el reporte para todas las muestras return report.report_action(self.generated_sample_ids)