from odoo import api, models import logging _logger = logging.getLogger(__name__) class StockMove(models.Model): _inherit = 'stock.move' @api.onchange('product_id') def _onchange_product_id(self): """Seed the next_serial field on stock.move when product changes, if product has a sequence.""" res = super()._onchange_product_id() if self.product_id and getattr(self.product_id.product_tmpl_id, 'lot_sequence_id', False): self.next_serial = getattr(self.product_id.product_tmpl_id, 'next_serial', False) return res def _create_lot_ids_from_move_line_vals(self, vals_list, product_id, company_id=False): """ Optimized batch lot creation for large quantities. - If user leaves '0' or empty as lot name, create lots in batch using optimized sequence allocation - Otherwise, fallback to the standard behavior for explicit names. """ Lot = self.env['stock.lot'] # Separate auto-generated from explicit names auto_gen_vals = [] remaining_vals = [] for vals in vals_list: lot_name = (vals.get('lot_name') or '').strip() if not lot_name or lot_name == '0': auto_gen_vals.append(vals) else: remaining_vals.append(vals) # Batch create auto-generated lots if auto_gen_vals: product = self.env['product.product'].browse(product_id) lot_sequence = getattr(product.product_tmpl_id, 'lot_sequence_id', False) if lot_sequence and len(auto_gen_vals) > 1: # Use optimized batch generation for multiple lots lot_names = self._allocate_sequence_batch(lot_sequence, len(auto_gen_vals)) # Prepare batch lot creation lot_vals_list = [] for lot_name in lot_names: lot_vals = { 'name': lot_name, 'product_id': product_id, } if company_id: lot_vals['company_id'] = company_id lot_vals_list.append(lot_vals) # Batch create all lots at once lots = Lot.create(lot_vals_list) # Assign lot_ids to vals for vals, lot in zip(auto_gen_vals, lots): vals['lot_id'] = lot.id vals['lot_name'] = False _logger.info(f"Batch created {len(lots)} lots for product {product.display_name}") else: # Single lot or no sequence - use standard creation for vals in auto_gen_vals: lot_vals = { 'product_id': product_id, } if company_id: lot_vals['company_id'] = company_id lot = Lot.create([lot_vals])[0] vals['lot_id'] = lot.id vals['lot_name'] = False # Delegate remaining with explicit names to the standard implementation if remaining_vals: return super()._create_lot_ids_from_move_line_vals(remaining_vals, product_id, company_id) return None def _allocate_sequence_batch(self, sequence, count): """ Allocate multiple sequence numbers in a single database operation. This is significantly faster than calling next_by_id() in a loop. Properly handles date format codes like %(y)s, %(month)s, %(day)s, etc. """ if count <= 0: return [] # Use PostgreSQL's generate_series to allocate multiple sequence values at once self.env.cr.execute(""" SELECT nextval(%s) FROM generate_series(1, %s) """, (f"ir_sequence_{sequence.id:03d}", count)) sequence_numbers = [row[0] for row in self.env.cr.fetchall()] # Get the interpolation context for date formatting (same as ir.sequence) from datetime import datetime now = datetime.now() # Build the interpolation dictionary (same format as Odoo's ir.sequence) interpolation_dict = { 'year': now.strftime('%Y'), 'y': now.strftime('%y'), 'month': now.strftime('%m'), 'day': now.strftime('%d'), 'doy': now.strftime('%j'), 'woy': now.strftime('%W'), 'weekday': now.strftime('%w'), 'h24': now.strftime('%H'), 'h12': now.strftime('%I'), 'min': now.strftime('%M'), 'sec': now.strftime('%S'), } # Format prefix and suffix with date codes try: prefix = (sequence.prefix or '') % interpolation_dict if sequence.prefix else '' except (KeyError, ValueError): # If formatting fails, use prefix as-is prefix = sequence.prefix or '' try: suffix = (sequence.suffix or '') % interpolation_dict if sequence.suffix else '' except (KeyError, ValueError): # If formatting fails, use suffix as-is suffix = sequence.suffix or '' # Format the sequence numbers according to the sequence configuration lot_names = [] for seq_num in sequence_numbers: lot_name = '{}{:0{}d}{}'.format( prefix, seq_num, sequence.padding, suffix ) lot_names.append(lot_name) return lot_names @api.model def action_generate_lot_line_vals(self, context, mode, first_lot, count, lot_text): """ Optimized lot generation for large quantities. If the 'Generate Serials/Lots' action is invoked with an empty or '0' base, generate names using the per-product sequence with batch optimization. """ if mode == 'generate': product_id = context.get('default_product_id') if product_id: product = self.env['product.product'].browse(product_id) tmpl = product.product_tmpl_id if (not first_lot or first_lot == '0') and getattr(tmpl, 'lot_sequence_id', False): seq = tmpl.lot_sequence_id # Use optimized batch generation for large quantities if count and count > 10: _logger.info(f"Using optimized batch generation for {count} lots") generated_names = self._allocate_sequence_batch(seq, count) else: # For small quantities, use standard generation generated_names = [seq.next_by_id() for _ in range(count or 0)] # Reuse parent implementation for the rest of the processing (locations, uom, etc.) fake_first = 'SEQDUMMY-1' vals_list = super().action_generate_lot_line_vals(context, mode, fake_first, count, lot_text) # Overwrite the lot_name with sequence-based names for vals, name in zip(vals_list, generated_names): vals['lot_name'] = name return vals_list # Fallback to standard behavior return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text)