[IMP] mrp_subcontracting: Mass Receipt
Adapt db775ffd308638a390be52eba3ca11c9c2d1602d to subcontracting flow, to avoid performance issues when receiving a large amount of SNs closes odoo/odoo#85603 Task: 2777451 Signed-off-by: Arnold Moyaux <arm@odoo.com>
This commit is contained in:
@@ -1506,6 +1506,10 @@ class MrpProduction(models.Model):
|
||||
the amounts each production split should produce including the original production,
|
||||
e.g. {mrp.production(1,): [3, 2]} will result in mrp.production(1,) having a product_qty=3
|
||||
and a new backorder with product_qty=2.
|
||||
:param bool cancel_remaining_qty: whether to cancel remaining quantities or generate
|
||||
an additional backorder, e.g. having product_qty=5 if mrp.production(1,) product_qty was 10.
|
||||
:param bool set_consumed_qty: whether to set qty_done on move lines to the reserved quantity
|
||||
or the initial demand if no reservation, except for the remaining backorder.
|
||||
:return: mrp.production records in order of [orig_prod_1, backorder_prod_1,
|
||||
backorder_prod_2, orig_prod_2, backorder_prod_2, etc.]
|
||||
"""
|
||||
@@ -1514,6 +1518,7 @@ class MrpProduction(models.Model):
|
||||
|
||||
if not amounts:
|
||||
amounts = {}
|
||||
has_backorder_to_ignore = defaultdict(lambda: False)
|
||||
for production in self:
|
||||
mo_amounts = amounts.get(production)
|
||||
if not mo_amounts:
|
||||
@@ -1522,6 +1527,7 @@ class MrpProduction(models.Model):
|
||||
total_amount = sum(mo_amounts)
|
||||
if total_amount < production.product_qty and not cancel_remaining_qty:
|
||||
amounts[production].append(production.product_qty - total_amount)
|
||||
has_backorder_to_ignore[production] = True
|
||||
elif total_amount > production.product_qty or production.state in ['done', 'cancel']:
|
||||
raise UserError(_("Unable to split with more than the quantity to produce."))
|
||||
|
||||
@@ -1567,10 +1573,12 @@ class MrpProduction(models.Model):
|
||||
# Split the `stock.move` among new backorders.
|
||||
new_moves_vals = []
|
||||
moves = []
|
||||
move_to_backorder_moves = {}
|
||||
for production in self:
|
||||
for move in production.move_raw_ids | production.move_finished_ids:
|
||||
if move.additional:
|
||||
continue
|
||||
move_to_backorder_moves[move] = self.env['stock.move']
|
||||
unit_factor = move.product_uom_qty / initial_qty_by_production[production]
|
||||
initial_move_vals = move.copy_data(move._get_backorder_move_vals())[0]
|
||||
move.with_context(do_not_unreserve=True).product_uom_qty = production.product_qty * unit_factor
|
||||
@@ -1595,7 +1603,6 @@ class MrpProduction(models.Model):
|
||||
# However it could be slower (due to `stock.quant` update) and could
|
||||
# create inconsistencies in mass production if a new lot higher in a
|
||||
# FIFO strategy arrives between the reservation and the backorder creation
|
||||
move_to_backorder_moves = defaultdict(lambda: self.env['stock.move'])
|
||||
for move, backorder_move in zip(moves, backorder_moves):
|
||||
move_to_backorder_moves[move] |= backorder_move
|
||||
|
||||
@@ -1604,6 +1611,19 @@ class MrpProduction(models.Model):
|
||||
partially_assigned_moves = set()
|
||||
move_lines_to_unlink = set()
|
||||
|
||||
for initial_move, backorder_moves in move_to_backorder_moves.items():
|
||||
# Create `stock.move.line` for consumed but non-reserved components
|
||||
if initial_move.raw_material_production_id and not initial_move.move_line_ids and set_consumed_qty:
|
||||
ml_vals = initial_move._prepare_move_line_vals()
|
||||
backorder_move_to_ignore = backorder_moves[-1] if has_backorder_to_ignore[initial_move.raw_material_production_id] else self.env['stock.move']
|
||||
for move in list(initial_move + backorder_moves - backorder_move_to_ignore):
|
||||
new_ml_vals = dict(
|
||||
ml_vals,
|
||||
qty_done=move.product_uom_qty,
|
||||
move_id=move.id
|
||||
)
|
||||
move_lines_vals.append(new_ml_vals)
|
||||
|
||||
for initial_move, backorder_moves in move_to_backorder_moves.items():
|
||||
ml_by_move = []
|
||||
product_uom = initial_move.product_id.uom_id
|
||||
|
||||
@@ -60,18 +60,14 @@ class StockPicking(models.Model):
|
||||
})
|
||||
change_qty.with_context(skip_activity=True).change_prod_qty()
|
||||
# Create backorder MO for each move lines
|
||||
for move_line in move.move_line_ids:
|
||||
amounts = [move_line.qty_done for move_line in move.move_line_ids]
|
||||
len_amounts = len(amounts)
|
||||
productions = production._split_productions({production: amounts}, set_consumed_qty=True)
|
||||
for production, move_line in zip(productions, move.move_line_ids):
|
||||
if move_line.lot_id:
|
||||
production.lot_producing_id = move_line.lot_id
|
||||
production.qty_producing = move_line.product_uom_id._compute_quantity(move_line.qty_done, production.product_uom_id)
|
||||
production._set_qty_producing()
|
||||
production.subcontracting_has_been_recorded = True
|
||||
if move_line != move.move_line_ids[-1]:
|
||||
backorder = production._split_productions()[1:]
|
||||
# The move_dest_ids won't be set because the _split filter out done move
|
||||
backorder.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids = production.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids
|
||||
production.product_qty = production.qty_producing
|
||||
production = backorder
|
||||
production.qty_producing = production.product_qty
|
||||
productions[:len_amounts].subcontracting_has_been_recorded = True
|
||||
|
||||
for picking in self:
|
||||
productions_to_done = picking._get_subcontract_production()._subcontracting_filter_to_done()
|
||||
|
||||
@@ -1100,3 +1100,94 @@ class TestSubcontractingPortal(TransactionCase):
|
||||
self.assertEqual(mo.move_line_raw_ids[1].qty_done, 1)
|
||||
self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3)
|
||||
self.assertEqual(mo.move_line_raw_ids[2].qty_done, 2)
|
||||
class TestSubcontractingSerialMassReceipt(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.subcontractor = self.env['res.partner'].create({
|
||||
'name': 'Subcontractor',
|
||||
})
|
||||
self.resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
|
||||
self.raw_material = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
'route_ids': [Command.link(self.resupply_route.id)],
|
||||
})
|
||||
self.finished = self.env['product.product'].create({
|
||||
'name': 'Finished',
|
||||
'type': 'product',
|
||||
'tracking': 'serial'
|
||||
})
|
||||
self.bom = self.env['mrp.bom'].create({
|
||||
'product_id': self.finished.id,
|
||||
'product_tmpl_id': self.finished.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'subcontract',
|
||||
'subcontractor_ids': [Command.link(self.subcontractor.id)],
|
||||
'consumption': 'strict',
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': self.raw_material.id, 'product_qty': 1}),
|
||||
]
|
||||
})
|
||||
|
||||
def test_receive_after_resupply(self):
|
||||
quantities = [5, 4, 1]
|
||||
# Make needed component stock
|
||||
self.env['stock.quant']._update_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock'), sum(quantities))
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = sum(quantities)
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
# Process the delivery of the components
|
||||
picking_deliver = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)]).picking_ids
|
||||
picking_deliver.action_assign()
|
||||
picking_deliver.button_validate()
|
||||
wizard_data = picking_deliver.button_validate()
|
||||
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
|
||||
wizard.process()
|
||||
# Receive
|
||||
for quantity in quantities:
|
||||
# Receive <quantity> finished products
|
||||
Form(self.env['stock.assign.serial'].with_context(
|
||||
default_move_id=picking_receipt.move_ids[0].id,
|
||||
default_next_serial_number=self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1',
|
||||
default_next_serial_count=quantity,
|
||||
)).save().generate_serial_numbers()
|
||||
wizard_data = picking_receipt.button_validate()
|
||||
if wizard_data is not True:
|
||||
# Create backorder
|
||||
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(picking_receipt.state, 'done')
|
||||
picking_receipt = picking_receipt.backorder_ids[-1]
|
||||
self.assertEqual(picking_receipt.state, 'assigned')
|
||||
self.assertEqual(picking_receipt.state, 'done')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor), 0)
|
||||
|
||||
def test_receive_no_resupply(self):
|
||||
quantity = 5
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = quantity
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
# Receive finished products
|
||||
Form(self.env['stock.assign.serial'].with_context(
|
||||
default_move_id=picking_receipt.move_ids[0].id,
|
||||
default_next_serial_number=self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1',
|
||||
default_next_serial_count=quantity,
|
||||
)).save().generate_serial_numbers()
|
||||
picking_receipt.button_validate()
|
||||
self.assertEqual(picking_receipt.state, 'done')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor, allow_negative=True), -quantity)
|
||||
|
||||
Reference in New Issue
Block a user