From 7fa4f71b633f649fa3aad80db930f92f46354b50 Mon Sep 17 00:00:00 2001 From: "lase@odoo.com" Date: Thu, 8 Feb 2024 15:13:43 +0100 Subject: [PATCH] [FIX] mrp: Generate MOs for BOM with product repetition Steps to reproduce: - Create 4 products: Final product (FP), Product 1,2,3 (P1,P2 and P3) - Set routes to manifacture on each product - For P1, P2, P3 add a 0:0 reordering rule. - Add a BOM for P2 with 1 unit of P1 as components - Add a BOM for P3 with 1 unit of P2 as components - Add a BOM for FP with 1 unit of P3 and of P2 as components The MO overview of a FP shoud look like this : FP /\ / \ P3 P2 | | P2 P1 | P1 - Create and confirm a manufacturing order for a FP Current behavior: As the quantity on hand is not sufficient to manufacture a FP, manufacturing orders are automatically created for P3, P2 and P1. However, the quantity on each of these MOs is of 1 unit. Expected behavior: Since 2 units of P2 and of P1 will be required to manufacture the FP the quantity of their respective MOs should be at 2. Cause of the issue: Confirming the MO for FP will call the trigger_scheduler() on its raw stock move: https://github.com/odoo/odoo/blob/79813f08e5a2f0188ac7d184d000486f25319503/addons/mrp/models/mrp_production.py#L1291 https://github.com/odoo/odoo/blob/f3ef40da0406bb0fd683dce3a08739e247fd6dfc/addons/stock/models/stock_orderpoint.py#L495 In this method, we compute the qty to order for the orderpoints of P2 and P3. Since these ones are positive, procurement will be run for both of these leading to the creation of 2 new MO's via the manufacture route. The post process of the scheduler is then run https://github.com/odoo/odoo/blob/f3ef40da0406bb0fd683dce3a08739e247fd6dfc/addons/stock/models/stock_orderpoint.py#L550 An override of this method in mrp will then find the created MO and confirm these, triggering the above process once more but now for the MO's of P3 and P2 rather than FP: we start by computing the qty to order for the orderpoints of P2 and P1 to manufacture P3 and P2. However, to compute this quantity, we look at the forecast of these quantities and this is where the problem comes in: https://github.com/odoo/odoo/blob/f3ef40da0406bb0fd683dce3a08739e247fd6dfc/addons/stock/models/stock_orderpoint.py#L284 With the orderpoint of P2: - `virtual_available` is at a value of -1 since there is currently 2 stock moves taking this product (one to manufacture FP and one to manufacture P3) and one stock move bringing one unit of this product (the one coming from the MO of P2 we are currently trying to confirm) - `orderpoint._quantity_in_progress()` has a value of 1 due to the MO of P2 that we are currently confirming. Adding these quantities to one an other, the forecasted qty of P2 is at 0. This is a mistake since the incoming stock move for P2 is counted twice in the forecast qty: once in each term. As a result no new procurement will be generated for P2 hence the issue. FIX: Since this `_quantity_in_progress()` seems to have been introduced to improve purchase flows rather than to interact with manufacturing flows: https://github.com/odoo/odoo/commit/943da6df0f1d39e8e10869b25c5ba8cc13a2aa54 We decided to ignore its contribution during our manufacturing flow for the forecast qty to be correctly computed. This requires to ignore the contribution of each mo triggering the scheduler and those that already triggered the scheduler since the confirmation of the MO of FP. Notes: The test sets a little more complex MO overview than the above use case to be sure to catch corner cases situations. opw-3689920 closes odoo/odoo#158217 X-original-commit: 4c0b9ef2ad75d3f831fce0abaa62a1d660ba5ca6 Signed-off-by: William Henrotin (whe) --- addons/mrp/models/mrp_production.py | 3 +- addons/mrp/models/stock_orderpoint.py | 7 +- .../test_warehouse_multistep_manufacturing.py | 111 ++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 17e6188d200..897c945c42e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -1426,7 +1426,8 @@ class MrpProduction(models.Model): moves_to_confirm._action_confirm(merge=False) workorder_to_confirm._action_confirm() # run scheduler for moves forecasted to not have enough in stock - self.move_raw_ids._trigger_scheduler() + ignored_mo_ids = self.env.context.get('ignore_mo_ids', []) + self.move_raw_ids.with_context(ignore_mo_ids=ignored_mo_ids + self.ids)._trigger_scheduler() self.picking_ids.filtered( lambda p: p.state not in ['cancel', 'done']).action_confirm() # Force confirm state only for draft production not for more advanced state like diff --git a/addons/mrp/models/stock_orderpoint.py b/addons/mrp/models/stock_orderpoint.py index 911df09ab21..add2fab48f3 100644 --- a/addons/mrp/models/stock_orderpoint.py +++ b/addons/mrp/models/stock_orderpoint.py @@ -104,7 +104,12 @@ class StockWarehouseOrderpoint(models.Model): bom_manufacture = self.env['mrp.bom']._bom_find(orderpoints_without_kit.product_id, bom_type='normal') bom_manufacture = self.env['mrp.bom'].concat(*bom_manufacture.values()) productions_group = self.env['mrp.production']._read_group( - [('bom_id', 'in', bom_manufacture.ids), ('state', '=', 'draft'), ('orderpoint_id', 'in', orderpoints_without_kit.ids)], + [ + ('bom_id', 'in', bom_manufacture.ids), + ('state', '=', 'draft'), + ('orderpoint_id', 'in', orderpoints_without_kit.ids), + ('id', 'not in', self.env.context.get('ignore_mo_ids', [])), + ], ['orderpoint_id', 'product_uom_id'], ['product_qty:sum']) for orderpoint, uom, product_qty_sum in productions_group: diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index 78dc3381cfb..5aac529c194 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -3,6 +3,7 @@ from odoo.tests import Form, tagged from odoo.addons.mrp.tests.common import TestMrpCommon +from odoo import Command @tagged('post_install', '-at_install') @@ -699,3 +700,113 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon): self.assertEqual(len(mo), 1) self.assertEqual(mo.product_qty, 1.0) self.assertEqual(mo.bom_id, bom_2) + + def test_manufacturing_bom_with_repetitions(self): + """ + Checks that manufacturing orders created to manufacture the components of a BOM + are set with the correct quantities when products appear with repetitions. + - Create 5 products: product 1,2,3,4 (P1,P2,P3 and P4) and a final product (FP) + - Set routes to manifacture on each product + - For P1, P2, P3, P4 add a 0:0 reordering rule. + - Add a BOM for P2 with 1 unit of P1 as components + - Add a BOM for P3 with 1 unit of P2 as components + - Add a BOM for P4 with 1 unit of P3 as components + - Add a BOM for FP with 3 unit of P4 and 2 units of P3 as components + """ + manufacturing_route = self.env['stock.rule'].search([ + ('action', '=', 'manufacture')]).route_id + products = self.env['product.product'].create([ + { + 'name': 'FP', + 'type': 'product', + 'route_ids': manufacturing_route, + }, + { + 'name': 'P1', + 'type': 'product', + 'route_ids': manufacturing_route, + }, + { + 'name': 'P2', + 'type': 'product', + 'route_ids': manufacturing_route, + }, + { + 'name': 'P3', + 'type': 'product', + 'route_ids': manufacturing_route, + }, + { + 'name': 'P4', + 'type': 'product', + 'route_ids': manufacturing_route, + }, + + ]) + self.env['stock.warehouse.orderpoint'].create([ + { + 'name': 'My orderpoint', + 'product_id': i, + 'product_min_qty': 0, + 'product_max_qty': 0, + } for i in products.ids[1:] + ]) + self.env['mrp.bom'].create([ + { + 'product_tmpl_id': products[2].product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': products[2].uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + Command.create({ + 'product_id': products[1].id, + 'product_qty': 1, + }) + ]}, + { + 'product_tmpl_id': products[3].product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': products[3].uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + Command.create({ + 'product_id': products[2].id, + 'product_qty': 1, + }) + ]}, + { + 'product_tmpl_id': products[4].product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': products[4].uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + Command.create({ + 'product_id': products[3].id, + 'product_qty': 1, + }) + ]}, + { + 'product_tmpl_id': products[0].product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': products[0].uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + Command.create({ + 'product_id': products[4].id, + 'product_qty': 3, + }), + Command.create({ + 'product_id': products[2].id, + 'product_qty': 2, + }), + ]}, + ]) + mo = self.env['mrp.production'].create({ + 'product_id': products[0].id, + 'product_uom_qty': 1, + }) + mo.action_confirm() + mo_P1 = self.env['mrp.production'].search([('product_id', '=', products[1].id)]) + mo_P2 = self.env['mrp.production'].search([('product_id', '=', products[2].id)]) + self.assertEqual(mo_P1.product_uom_qty, 5.0) + self.assertEqual(mo_P2.product_uom_qty, 5.0)