diff --git a/stock_available_mrp/__openerp__.py b/stock_available_mrp/__openerp__.py index 64ad57aaa..2d6afa640 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.1.0', + 'version': '8.0.3.1.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 91daaab75..c6440f31a 100644 --- a/stock_available_mrp/models/product_product.py +++ b/stock_available_mrp/models/product_product.py @@ -6,7 +6,6 @@ from collections import Counter from openerp import models, fields, api from openerp.addons import decimal_precision as dp -from openerp.tools.safe_eval import safe_eval class ProductProduct(models.Model): @@ -20,6 +19,14 @@ class ProductProduct(models.Model): help="Quantity of this Product that could be produced using " "the materials already at hand.") + # Needed for fields dependencies + # When self.potential_qty is compute, we want to force the ORM + # to compute all the components potential_qty too. + component_ids = fields.Many2many( + comodel_name='product.product', + compute='_get_component_ids', + ) + @api.multi @api.depends('potential_qty') def _immediately_usable_qty(self): @@ -31,17 +38,12 @@ class ProductProduct(models.Model): product.immediately_usable_qty += product.potential_qty @api.multi + @api.depends('component_ids.potential_qty') 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'] - icp = self.env['ir.config_parameter'] - stock_available_mrp_based_on = safe_eval( - icp.get_param('stock_available_mrp_based_on', 'False')) - if not stock_available_mrp_based_on: - stock_available_mrp_based_on = 'qty_available' - for product in self: bom_id = bom_obj._bom_find(product_id=product.id) if not bom_id: @@ -60,7 +62,7 @@ class ProductProduct(models.Model): else: # Find the lowest quantity we can make with the stock at hand components_potential_qty = min( - [getattr(component, stock_available_mrp_based_on) // need + [self._get_component_qty(component) // need for component, need in component_needs.items()] ) @@ -72,6 +74,19 @@ class ProductProduct(models.Model): ) product.potential_qty = bom_qty * components_potential_qty + def _get_component_qty(self, component): + """ Return the component qty to use based en company settings. + + :type component: product_product + :rtype: float + """ + icp = self.env['ir.config_parameter'] + stock_available_mrp_based_on = icp.get_param( + 'stock_available_mrp_based_on', 'qty_available' + ) + + return component[stock_available_mrp_based_on] + def _get_components_needs(self, product, bom): """ Return the needed qty of each compoments in the *bom* of *product*. @@ -98,3 +113,15 @@ class ProductProduct(models.Model): ) return needs + + def _get_component_ids(self): + """ Compute component_ids by getting all the components for + this product. + """ + bom_obj = self.env['mrp.bom'] + + bom_id = bom_obj._bom_find(product_id=self.id) + if bom_id: + bom = bom_obj.browse(bom_id) + for bom_component in bom_obj._bom_explode(bom, self, 1.0)[0]: + self.component_ids |= self.browse(bom_component['product_id']) diff --git a/stock_available_mrp/tests/test_potential_qty.py b/stock_available_mrp/tests/test_potential_qty.py index b3d6e09de..98eb9a545 100644 --- a/stock_available_mrp/tests/test_potential_qty.py +++ b/stock_available_mrp/tests/test_potential_qty.py @@ -16,6 +16,7 @@ class TestPotentialQty(TransactionCase): 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.config = self.env['ir.config_parameter'] self.setup_demo_data() @@ -84,6 +85,24 @@ class TestPotentialQty(TransactionCase): }) inventory.action_done() + def create_simple_bom(self, product, sub_product, + product_qty=1, sub_product_qty=1): + bom = self.bom_model.create({ + 'product_tmpl_id': product.product_tmpl_id.id, + 'product_id': product.id, + 'product_qty': product_qty, + 'product_uom': self.ref('product.product_uom_unit'), + + }) + self.bom_line_model.create({ + 'bom_id': bom.id, + 'product_id': sub_product.id, + 'product_qty': sub_product_qty, + 'product_uom': self.ref('product.product_uom_unit'), + }) + + return bom + def assertPotentialQty(self, record, qty, msg): record.refresh() # Check the potential @@ -394,3 +413,103 @@ class TestPotentialQty(TransactionCase): p1.refresh() self.assertEqual(24, p1.potential_qty) + + def test_component_stock_choice(self): + # Test to change component stock for compute BOM stock + + # Get a demo product with outgoing move (qty: 3) + imac = self.browse_ref('product.product_product_8') + + # Set on hand qty + self.create_inventory(imac.id, 3) + + # Create a product with BOM + p1 = self.product_model.create({ + 'name': 'Test product with BOM', + }) + bom_p1 = self.bom_model.create({ + 'product_tmpl_id': p1.product_tmpl_id.id, + 'product_id': p1.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_unit'), + }) + + # Need 1 iMac for that + p1_bom_line = self.bom_line_model.create({ + 'bom_id': bom_p1.id, + 'product_id': imac.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_unit'), + }) + + # Default component is qty_available + p1.refresh() + self.assertEqual(3.0, p1.potential_qty) + + # Change to immediately usable + self.config.set_param('stock_available_mrp_based_on', + 'immediately_usable_qty') + + p1.refresh() + self.assertEqual(0.0, p1.potential_qty) + + # If iMac has a Bom and can be manufactured + imac_component = self.product_model.create({ + 'name': 'iMac component', + }) + self.create_inventory(imac_component.id, 5) + + imac_bom = self.bom_model.create({ + 'product_tmpl_id': imac.product_tmpl_id.id, + 'product_id': imac.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_unit'), + }) + p1_bom_line.type = 'phantom' + + # Need 1 imac_component for iMac + self.bom_line_model.create({ + 'bom_id': imac_bom.id, + 'product_id': imac_component.id, + 'product_qty': 1, + 'product_uom': self.ref('product.product_uom_unit'), + }) + + p1.refresh() + self.assertEqual(5.0, p1.potential_qty) + + # Changing to virtual (same as immediately in current config) + self.config.set_param('stock_available_mrp_based_on', + 'virtual_available') + p1.refresh() + self.assertEqual(5.0, p1.potential_qty) + + def test_potential_qty__list(self): + # Try to highlight a bug when _get_potential_qty is called on + # a recordset with multiple products + # Recursive compute is not working + + p1 = self.product_model.create({'name': 'Test P1'}) + p2 = self.product_model.create({'name': 'Test P2'}) + p3 = self.product_model.create({'name': 'Test P3'}) + + self.config.set_param('stock_available_mrp_based_on', + 'immediately_usable_qty') + + # P1 need one P2 + self.create_simple_bom(p1, p2) + # P2 need one P3 + self.create_simple_bom(p2, p3) + + self.create_inventory(p3.id, 3) + + self.product_model.invalidate_cache() + + products = self.product_model.search( + [('id', 'in', [p1.id, p2.id, p3.id])] + ) + + self.assertEqual( + {p1.id: 3.0, p2.id: 3.0, p3.id: 0.0}, + {p.id: p.potential_qty for p in products} + )