diff --git a/stock_available_mrp/__openerp__.py b/stock_available_mrp/__openerp__.py index 7523830a0..0e10d6a6e 100644 --- a/stock_available_mrp/__openerp__.py +++ b/stock_available_mrp/__openerp__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { 'name': 'Consider the production potential is available to promise', - 'version': '8.0.3.0.0', + 'version': '8.0.3.0.1', "author": u"Numérigraphe," u"Odoo Community Association (OCA)", 'category': 'Hidden', diff --git a/stock_available_mrp/models/product_product.py b/stock_available_mrp/models/product_product.py index b486e3389..b9ade817e 100644 --- a/stock_available_mrp/models/product_product.py +++ b/stock_available_mrp/models/product_product.py @@ -33,6 +33,7 @@ class ProductProduct(models.Model): def _get_potential_qty(self): """Compute the potential qty based on the available components.""" bom_obj = self.env['mrp.bom'] + uom_obj = self.env['product.uom'] for product in self: bom_id = bom_obj._bom_find(product_id=product.id) @@ -40,18 +41,53 @@ class ProductProduct(models.Model): product.potential_qty = 0.0 continue + bom = bom_obj.browse(bom_id) + # Need by product (same product can be in many BOM lines/levels) - component_needs = Counter() - for component in bom_obj._bom_explode(bom_obj.browse(bom_id), - product, 1.0,)[0]: - component_needs += Counter( - {component['product_id']: component['product_qty']}) + component_needs = self._get_components_needs(product, bom) + if not component_needs: # The BoM has no line we can use product.potential_qty = 0.0 - continue - # Find the lowest quantity we can make with the stock at hand - product.potential_qty = min( - [self.browse(component_id).qty_available // need - for component_id, need in component_needs.items()]) + else: + # Find the lowest quantity we can make with the stock at hand + components_potential_qty = min( + [component.qty_available // need + for component, need in component_needs.items()] + ) + + # Compute with bom quantity + bom_qty = uom_obj._compute_qty_obj( + bom.product_uom, + bom.product_qty, + bom.product_tmpl_id.uom_id + ) + product.potential_qty = bom_qty * components_potential_qty + + def _get_components_needs(self, product, bom): + """ Return the needed qty of each compoments in the *bom* of *product*. + + :type product: product_product + :type bom: mrp_bom + :rtype: collections.Counter + """ + bom_obj = self.env['mrp.bom'] + uom_obj = self.env['product.uom'] + product_obj = self.env['product.product'] + + needs = Counter() + for bom_component in bom_obj._bom_explode(bom, product, 1.0)[0]: + product_uom = uom_obj.browse(bom_component['product_uom']) + component = product_obj.browse(bom_component['product_id']) + + component_qty = uom_obj._compute_qty_obj( + product_uom, + bom_component['product_qty'], + component.uom_id, + ) + needs += Counter( + {component: component_qty} + ) + + return needs diff --git a/stock_available_mrp/tests/test_potential_qty.py b/stock_available_mrp/tests/test_potential_qty.py index b27203b9b..b3d6e09de 100644 --- a/stock_available_mrp/tests/test_potential_qty.py +++ b/stock_available_mrp/tests/test_potential_qty.py @@ -12,6 +12,14 @@ class TestPotentialQty(TransactionCase): def setUp(self): super(TestPotentialQty, self).setUp() + self.product_model = self.env["product.product"] + self.bom_model = self.env["mrp.bom"] + self.bom_line_model = self.env["mrp.bom.line"] + self.stock_quant_model = self.env["stock.quant"] + + self.setup_demo_data() + + def setup_demo_data(self): #  An interesting product (multi-line BoM, variants) self.tmpl = self.browse_ref( 'product.product_product_4_product_template') @@ -57,6 +65,25 @@ class TestPotentialQty(TransactionCase): self.wh_main = self.browse_ref('stock.warehouse0') self.wh_ch = self.browse_ref('stock.stock_warehouse_shop0') + def create_inventory(self, product_id, qty, location_id=None): + if location_id is None: + location_id = self.wh_main.lot_stock_id.id + + inventory = self.env['stock.inventory'].create({ + 'name': 'Test inventory', + 'location_id': location_id, + 'filter': 'partial' + }) + inventory.prepare_inventory() + + self.env['stock.inventory.line'].create({ + 'inventory_id': inventory.id, + 'product_id': product_id, + 'location_id': location_id, + 'product_qty': qty + }) + inventory.action_done() + def assertPotentialQty(self, record, qty, msg): record.refresh() # Check the potential @@ -186,7 +213,9 @@ class TestPotentialQty(TransactionCase): "Receiving variant 1's component should not change " "variant 2's potential") - # Receive enough components to make 500x the 2nd variant at Chicago + # Receive enough components to make 42X the 2nd variant at Chicago + # need 13 dozens of HDD with 50% efficiency to build 42 RAM + # So 313 HDD (with rounding) for 42 RAM inventory = self.env['stock.inventory'].create( {'name': 'components for 2nd variant', 'location_id': self.wh_ch.lot_stock_id.id, @@ -201,7 +230,7 @@ class TestPotentialQty(TransactionCase): {'inventory_id': inventory.id, 'product_id': self.ref('product.product_product_18'), 'location_id': self.wh_ch.lot_stock_id.id, - 'product_qty': 310.0}) + 'product_qty': 313.0}) inventory.action_done() self.assertPotentialQty( self.tmpl, 1000.0, @@ -211,14 +240,14 @@ class TestPotentialQty(TransactionCase): "Receiving variant 2's component should not change " "variant 1's potential") self.assertPotentialQty( - self.var2, 500.0, + self.var2, 42.0, "Wrong variant 2 potential after receiving components") # Check by warehouse self.assertPotentialQty( self.tmpl.with_context(warehouse=self.wh_main.id), 1000.0, "Wrong potential quantity in main WH") self.assertPotentialQty( - self.tmpl.with_context(warehouse=self.wh_ch.id), 500.0, + self.tmpl.with_context(warehouse=self.wh_ch.id), 42.0, "Wrong potential quantity in Chicago WH") # Check by location self.assertPotentialQty( @@ -228,5 +257,140 @@ class TestPotentialQty(TransactionCase): self.assertPotentialQty( self.tmpl.with_context( location=self.wh_ch.lot_stock_id.id), - 500.0, + 42.0, "Wrong potential quantity in Chicago WH location") + + def test_multi_unit_recursive_bom(self): + # Test multi-level and multi-units BOM + + p1 = self.product_model.create({ + 'name': 'Test product with BOM', + }) + + p2 = self.product_model.create({ + 'name': 'Test sub product with BOM', + }) + + p3 = self.product_model.create({ + 'name': 'Test component' + }) + + bom_p1 = self.bom_model.create({ + 'product_tmpl_id': p1.product_tmpl_id.id, + 'product_id': p1.id, + }) + + # 1 dozen of component + self.bom_line_model.create({ + 'bom_id': bom_p1.id, + 'product_id': p3.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_dozen'), + }) + + # Two p2 which have a bom + self.bom_line_model.create({ + 'bom_id': bom_p1.id, + 'product_id': p2.id, + 'product_qty': 2, + 'product_uom': self.ref('product.product_uom_unit'), + 'type': 'phantom', + }) + + bom_p2 = self.bom_model.create({ + 'product_tmpl_id': p2.product_tmpl_id.id, + 'product_id': p2.id, + }) + + # p2 need 2 unit of component + self.bom_line_model.create({ + 'bom_id': bom_p2.id, + 'product_id': p3.id, + 'product_qty': 2, + 'product_uom': self.ref('product.product_uom_unit'), + }) + + p1.refresh() + + # Need a least 1 dozen + 2 * 2 = 16 units for one P1 + self.assertEqual(0, p1.potential_qty) + + self.create_inventory(p3.id, 1) + + p1.refresh() + self.assertEqual(0, p1.potential_qty) + + self.create_inventory(p3.id, 15) + p1.refresh() + self.assertEqual(0, p1.potential_qty) + + self.create_inventory(p3.id, 16) + p1.refresh() + self.assertEqual(1.0, p1.potential_qty) + + self.create_inventory(p3.id, 25) + p1.refresh() + self.assertEqual(1.0, p1.potential_qty) + + self.create_inventory(p3.id, 32) + p1.refresh() + self.assertEqual(2.0, p1.potential_qty) + + def test_bom_qty_and_efficiency(self): + + p1 = self.product_model.create({ + 'name': 'Test product with BOM', + }) + + p2 = self.product_model.create({ + 'name': 'Test sub product with BOM', + }) + + p3 = self.product_model.create({ + 'name': 'Test component' + }) + + # A bom produce 2 dozen of P1 + bom_p1 = self.bom_model.create({ + 'product_tmpl_id': p1.product_tmpl_id.id, + 'product_id': p1.id, + 'product_qty': 2, + 'product_uom': self.ref('product.product_uom_dozen'), + }) + + # Need 5 p2 for that + self.bom_line_model.create({ + 'bom_id': bom_p1.id, + 'product_id': p2.id, + 'product_qty': 5, + 'product_uom': self.ref('product.product_uom_unit'), + 'product_efficiency': 0.8, + }) + + # Which need 1 dozen of P3 + bom_p2 = self.bom_model.create({ + 'product_tmpl_id': p2.product_tmpl_id.id, + 'product_id': p2.id, + 'type': 'phantom', + }) + self.bom_line_model.create({ + 'bom_id': bom_p2.id, + 'product_id': p3.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_dozen'), + }) + + p1.refresh() + self.assertEqual(0, p1.potential_qty) + + self.create_inventory(p3.id, 60) + + p1.refresh() + self.assertEqual(0, p1.potential_qty) + + # Need 5 * 1 dozen => 60 + # But 80% lost each dozen, need 3 more by dozen => 60 + 5 *3 => 75 + self.create_inventory(p3.id, 75) + + p1.refresh() + self.assertEqual(24, p1.potential_qty)