From 24fa2495e59ee5e6f78ef76ec7ae2f195e0e7962 Mon Sep 17 00:00:00 2001 From: JF Aubert Date: Tue, 1 Mar 2022 14:36:07 +0000 Subject: [PATCH] [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 --- addons/mrp/models/mrp_production.py | 22 ++++- .../models/stock_picking.py | 16 ++-- .../tests/test_subcontracting.py | 91 +++++++++++++++++++ 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 0d92bbe6aae..7934f98a9f9 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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 diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index 4662125086a..85905e35f46 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -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() diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index 30be5ce9a00..63ad4447376 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -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 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)