298 lines
12 KiB
Python
298 lines
12 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)
|
|
|
|
def _auto_generate_lots_for_subcontract(self):
|
|
"""
|
|
Auto-generate lot numbers for subcontracting moves.
|
|
This method handles the automatic lot generation for subcontracted products.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if not self.product_id.tracking in ['lot', 'serial']:
|
|
return []
|
|
|
|
product = self.product_id
|
|
tmpl = product.product_tmpl_id
|
|
lot_sequence = getattr(tmpl, 'lot_sequence_id', False)
|
|
|
|
if not lot_sequence:
|
|
_logger.warning(f"No lot sequence configured for product {product.display_name}")
|
|
return []
|
|
|
|
# Check if we already have move lines with lots
|
|
existing_lots = self.move_line_ids.filtered(lambda ml: ml.lot_id)
|
|
if existing_lots:
|
|
_logger.info(f"Move already has lots assigned: {[lot.lot_id.name for lot in existing_lots]}")
|
|
return existing_lots.mapped('lot_id')
|
|
|
|
# Calculate how many lots we need to generate
|
|
remaining_qty = self.product_uom_qty
|
|
|
|
# For serial tracking, create one lot per unit
|
|
if product.tracking == 'serial':
|
|
lots_needed = int(remaining_qty)
|
|
else:
|
|
# For lot tracking, create one lot for the entire quantity
|
|
lots_needed = 1
|
|
|
|
if lots_needed <= 0:
|
|
return []
|
|
|
|
# Generate lot names using the optimized batch method
|
|
if lots_needed > 10:
|
|
lot_names = self._allocate_sequence_batch(lot_sequence, lots_needed)
|
|
else:
|
|
lot_names = [lot_sequence.next_by_id() for _ in range(lots_needed)]
|
|
|
|
# Create the lots
|
|
Lot = self.env['stock.lot']
|
|
lot_vals_list = []
|
|
for lot_name in lot_names:
|
|
lot_vals = {
|
|
'name': lot_name,
|
|
'product_id': product.id,
|
|
'company_id': self.company_id.id,
|
|
}
|
|
lot_vals_list.append(lot_vals)
|
|
|
|
lots = Lot.create(lot_vals_list)
|
|
|
|
# Assign lots to existing move lines or create new ones
|
|
existing_move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id)
|
|
|
|
if existing_move_lines and product.tracking == 'lot':
|
|
# For lot tracking, assign the first lot to the first available move line
|
|
if lots:
|
|
existing_move_lines[0].lot_id = lots[0].id
|
|
_logger.info(f"Assigned lot {lots[0].name} to existing move line")
|
|
elif product.tracking == 'serial':
|
|
# For serial tracking, we need one move line per lot
|
|
# If we don't have enough move lines, let Odoo handle the creation
|
|
# by using the standard lot assignment mechanism
|
|
for i, lot in enumerate(lots):
|
|
if i < len(existing_move_lines):
|
|
existing_move_lines[i].lot_id = lot.id
|
|
else:
|
|
# Let Odoo create additional move lines as needed
|
|
# This is safer than trying to create them manually
|
|
break
|
|
|
|
_logger.info(f"Auto-generated {len(lots)} lots for subcontract move of product {product.display_name}")
|
|
return lots
|
|
|
|
def action_generate_lots_for_move(self):
|
|
"""Open the lot generator wizard for this specific move."""
|
|
self.ensure_one()
|
|
|
|
if not self.is_subcontract:
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': 'Not a Subcontract Move',
|
|
'message': 'This action is only available for subcontract moves.',
|
|
'type': 'warning',
|
|
'sticky': False,
|
|
}
|
|
}
|
|
|
|
if self.product_id.tracking == 'none':
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': 'No Tracking Required',
|
|
'message': 'This product does not require lot/serial tracking.',
|
|
'type': 'info',
|
|
'sticky': False,
|
|
}
|
|
}
|
|
|
|
return {
|
|
'name': 'Generate Lots for Move',
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'subcontract.lot.generator',
|
|
'view_mode': 'form',
|
|
'target': 'new',
|
|
'context': {
|
|
'default_move_id': self.id,
|
|
'default_picking_id': self.picking_id.id,
|
|
'default_product_id': self.product_id.id,
|
|
'default_quantity': self.product_uom_qty,
|
|
},
|
|
} |