diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 1783fe202f5b4392a8e435546395bc90c4f61cdc..bba2eb9f21561c1a36ef518ada75e30dbc286da7 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import collections from datetime import timedelta from itertools import groupby import operator as py_operator @@ -222,20 +223,27 @@ class ProductProduct(models.Model): # compute kit quantities for product in bom_kits: bom_sub_lines = bom_sub_lines_per_kit[product] + # group lines by component + bom_sub_lines_grouped = collections.defaultdict(list) + for info in bom_sub_lines: + bom_sub_lines_grouped[info[0].product_id].append(info) ratios_virtual_available = [] ratios_qty_available = [] ratios_incoming_qty = [] ratios_outgoing_qty = [] ratios_free_qty = [] - for bom_line, bom_line_data in bom_sub_lines: - component = bom_line.product_id.with_context(mrp_compute_quantities=qties).with_prefetch(prefetch_component_ids) - if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): - # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those - # to avoid a division by zero. The same logic is applied to non-storable products as those - # products have 0 qty available. - continue - uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty'] - qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False) + + for component, bom_sub_lines in bom_sub_lines_grouped.items(): + component = component.with_context(mrp_compute_quantities=qties).with_prefetch(prefetch_component_ids) + qty_per_kit = 0 + for bom_line, bom_line_data in bom_sub_lines: + if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): + # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those + # to avoid a division by zero. The same logic is applied to non-storable products as those + # products have 0 qty available. + continue + uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty'] + qty_per_kit += bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False) if not qty_per_kit: continue rounding = component.uom_id.rounding diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py index 96224ab8ecf6ec4c2a3d2b061ee024dd47cd1fc0..e8cc8ccbb29e682e34bd8596d70f3d32e8ec2e77 100644 --- a/addons/mrp/tests/common.py +++ b/addons/mrp/tests/common.py @@ -228,3 +228,32 @@ class TestMrpCommon(common2.TestStockCommon): 'tracking': 'none', 'categ_id': cls.env.ref('product.product_category_all').id, }) + + @classmethod + def make_prods(cls, n): + return [ + cls.env["product.product"].create( + {"name": f"p{k + 1}", "type": "product"} + ) + for k in range(n) + ] + + @classmethod + def make_bom(cls, p, *cs): + return cls.env["mrp.bom"].create( + { + "product_tmpl_id": p.product_tmpl_id.id, + "product_id": p.id, + "product_qty": 1, + "type": "phantom", + "product_uom_id": cls.uom_unit.id, + "bom_line_ids": [ + (0, 0, { + "product_id": c.id, + "product_qty": 1, + "product_uom_id": cls.uom_unit.id + }) + for c in cs + ], + } + ) diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index 28691a2c9b2560a00b760d378b045a1a8efa31c0..7d54adc3f1e755e905b9bd4e42d74989213cb6f9 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -1142,3 +1142,15 @@ class TestBoM(TestMrpCommon): self.assertEqual(orderpoint.route_id.id, manufacturing_route_id) self.assertEqual(orderpoint.qty_multiple, 2000.0) self.assertEqual(orderpoint.qty_to_order, 4000.0) + + def test_bom_kit_with_sub_kit(self): + p1, p2, p3, p4 = self.make_prods(4) + self.make_bom(p1, p2, p3) + self.make_bom(p2, p3, p4) + + loc = self.env.ref("stock.stock_location_stock") + self.env["stock.quant"]._update_available_quantity(p3, loc, 10) + self.env["stock.quant"]._update_available_quantity(p4, loc, 10) + self.assertEqual(p1.qty_available, 5.0) + self.assertEqual(p2.qty_available, 10.0) + self.assertEqual(p3.qty_available, 10.0)