product_lot_sequence_per_pr.../models/stock_move.py

177 lines
7.4 KiB
Python

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)