[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) <whe@odoo.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user