subcontract_generate_lot/models/stock_move.py

144 lines
5.6 KiB
Python

from odoo import api, models, _
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class StockMove(models.Model):
_inherit = 'stock.move'
def _auto_generate_lots_for_subcontract(self):
"""
Auto-generate lot numbers for subcontracting moves.
This method handles the automatic lot generation for subcontracted products.
It also validates that the linked Manufacturing Order is not cancelled.
"""
self.ensure_one()
if getattr(self, 'is_subcontract', False):
# Check for cancelled subcontract MOs
# We use getattr/safe access or rely on the fact that this module depends on mrp_subcontracting
productions = self._get_subcontract_production()
cancelled_productions = productions.filtered(lambda p: p.state == 'cancel')
if cancelled_productions:
raise UserError(_(
"Cannot generate lots for product %s.\n"
"The linked Subcontracting Manufacturing Order (e.g., %s) is cancelled.\n"
"Lots generated would not be correctly linked to the receipt quantity.\n"
"Please check the Manufacturing Order status."
) % (self.product_id.display_name, cancelled_productions[0].name))
if not self.product_id.tracking in ['lot', 'serial']:
return []
product = self.product_id
tmpl = product.product_tmpl_id
# Use standard Odoo 19 lot_sequence_id field
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 standard sequence
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
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 getattr(self, 'is_subcontract', False):
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,
},
}