[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:
JF Aubert
2022-04-27 11:06:03 +02:00
parent 9e14c112ee
commit 24fa2495e5
3 changed files with 118 additions and 11 deletions
+21 -1
View File
@@ -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)