diff --git a/stock_available/README.rst b/stock_available/README.rst index d996ef92f..f4085a78c 100644 --- a/stock_available/README.rst +++ b/stock_available/README.rst @@ -58,6 +58,7 @@ Contributors * Lionel Sausin (Numérigraphe) * Sodexis +* Cédric Pigeon Maintainer ---------- @@ -72,4 +73,4 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. \ No newline at end of file +To contribute to this module, please visit https://odoo-community.org. diff --git a/stock_available/models/product_product.py b/stock_available/models/product_product.py index d908c2ea4..762a69169 100644 --- a/stock_available/models/product_product.py +++ b/stock_available/models/product_product.py @@ -16,39 +16,35 @@ class ProductProduct(models.Model): _inherit = 'product.product' @api.multi - @api.depends('virtual_available') - def _compute_immediately_usable_qty(self): - """No-op implementation of the stock available to promise. - - By default, available to promise = forecasted quantity. - - **Each** sub-module **must** override this method in **both** - `product.product` **and** `product.template`, because we can't - decide in advance how to compute the template's quantity from the - variants. - """ - for prod in self: - prod.immediately_usable_qty = prod.virtual_available + def _compute_available_quantities_dict(self): + res = {} + for product in self: + res[product.id] = {} + res[product.id]['immediately_usable_qty'] = \ + product.virtual_available + res[product.id]['potential_qty'] = 0.0 + return res @api.multi - @api.depends() - def _compute_potential_qty(self): - """Set potential qty to 0.0 to define the field defintion used by - other modules to inherit it - """ + @api.depends('virtual_available') + def _compute_available_quantities(self): + res = self._compute_available_quantities_dict() for product in self: - product.potential_qty = 0.0 + data = res[product.id] + for key, value in data.iteritems(): + if hasattr(product, key): + product[key] = value immediately_usable_qty = fields.Float( digits=dp.get_precision('Product Unit of Measure'), - compute='_compute_immediately_usable_qty', + compute='_compute_available_quantities', string='Available to promise', help="Stock for this Product that can be safely proposed " "for sale to Customers.\n" "The definition of this value can be configured to suit " "your needs") potential_qty = fields.Float( - compute='_compute_potential_qty', + compute='_compute_available_quantities', digits=dp.get_precision('Product Unit of Measure'), string='Potential', help="Quantity of this Product that could be produced using " diff --git a/stock_available/models/product_template.py b/stock_available/models/product_template.py index dc032f0d3..7d23c6c01 100644 --- a/stock_available/models/product_template.py +++ b/stock_available/models/product_template.py @@ -11,46 +11,44 @@ class ProductTemplate(models.Model): _inherit = 'product.template' @api.multi - @api.depends('product_variant_ids.immediately_usable_qty') - def _compute_immediately_usable_qty(self): - """No-op implementation of the stock available to promise. - - By default, available to promise = forecasted quantity. - - **Each** sub-module **must** override this method in **both** - `product.product` **and** `product.template`, because we can't - decide in advance how to compute the template's quantity from the - variants. - """ - for tmpl in self: - tmpl.immediately_usable_qty = tmpl.virtual_available + @api.depends('product_variant_ids.immediately_usable_qty', + 'product_variant_ids.potential_qty') + def _compute_available_quantities(self): + res = self._compute_available_quantities_dict() + for product in self: + data = res[product.id] + for key, value in data.iteritems(): + if key in product._fields: + product[key] = value @api.multi - @api.depends('product_variant_ids.potential_qty') - def _compute_potential_qty(self): - """Compute the potential as the max of all the variants's potential. - - We can't add the potential of variants: if they share components we - may not be able to make all the variants. - So we set the arbitrary rule that we can promise up to the biggest - variant's potential. - """ - for tmpl in self: - if not tmpl.product_variant_ids: - continue - tmpl.potential_qty = max( - [v.potential_qty for v in tmpl.product_variant_ids]) + def _compute_available_quantities_dict(self): + variants_dict = self.mapped( + 'product_variant_ids')._compute_available_quantities_dict() + res = {} + for template in self: + immediately_usable_qty = sum( + [variants_dict[p.id]["immediately_usable_qty"] for p in + template.product_variant_ids]) + potential_qty = max( + [variants_dict[p.id]["potential_qty"] for p in + template.product_variant_ids] or [0.0]) + res[template.id] = { + "immediately_usable_qty": immediately_usable_qty, + "potential_qty": potential_qty, + } + return res immediately_usable_qty = fields.Float( digits=dp.get_precision('Product Unit of Measure'), - compute='_compute_immediately_usable_qty', + compute='_compute_available_quantities', string='Available to promise', help="Stock for this Product that can be safely proposed " "for sale to Customers.\n" "The definition of this value can be configured to suit " "your needs") potential_qty = fields.Float( - compute='_compute_potential_qty', + compute='_compute_available_quantities', digits=dp.get_precision('Product Unit of Measure'), string='Potential', help="Quantity of this Product that could be produced using " diff --git a/stock_available/tests/test_stock_available.py b/stock_available/tests/test_stock_available.py index 1c9dd0626..23025500f 100644 --- a/stock_available/tests/test_stock_available.py +++ b/stock_available/tests/test_stock_available.py @@ -7,7 +7,6 @@ from odoo.tests.common import TransactionCase class TestStockLogisticsWarehouse(TransactionCase): - def test_res_config(self): """Test the config file""" stock_setting = self.env['stock.config.settings'].create({}) @@ -20,3 +19,104 @@ class TestStockLogisticsWarehouse(TransactionCase): self.assertEquals( stock_setting.stock_available_mrp_based_on, 'immediately_usable_qty') + + def test01_stock_levels(self): + """checking that immediately_usable_qty actually reflects \ + the variations in stock, both on product and template""" + moveObj = self.env['stock.move'] + productObj = self.env['product.product'] + templateObj = self.env['product.template'] + supplier_location = self.env.ref('stock.stock_location_suppliers') + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + uom_unit = self.env.ref('product.product_uom_unit') + + # Create product template + templateAB = templateObj.create( + {'name': 'templAB', + 'uom_id': uom_unit.id, + }) + + # Create product A and B + productA = productObj.create( + {'name': 'product A', + 'standard_price': 1, + 'type': 'product', + 'uom_id': uom_unit.id, + 'default_code': 'A', + 'product_tmpl_id': templateAB.id, + }) + + productB = productObj.create( + {'name': 'product B', + 'standard_price': 1, + 'type': 'product', + 'uom_id': uom_unit.id, + 'default_code': 'B', + 'product_tmpl_id': templateAB.id, + }) + + # Create a stock move from INCOMING to STOCK + stockMoveInA = moveObj.create( + {'location_id': supplier_location.id, + 'location_dest_id': stock_location.id, + 'name': 'MOVE INCOMING -> STOCK ', + 'product_id': productA.id, + 'product_uom': productA.uom_id.id, + 'product_uom_qty': 2, + }) + + stockMoveInB = moveObj.create( + {'location_id': supplier_location.id, + 'location_dest_id': stock_location.id, + 'name': 'MOVE INCOMING -> STOCK ', + 'product_id': productB.id, + 'product_uom': productB.uom_id.id, + 'product_uom_qty': 3, + }) + + def compare_product_usable_qty(product, value): + # Refresh, because the function field is not recalculated between + # transactions + product.refresh() + self.assertEqual(product.immediately_usable_qty, value) + + compare_product_usable_qty(productA, 0) + compare_product_usable_qty(templateAB, 0) + + stockMoveInA.action_confirm() + compare_product_usable_qty(productA, 2) + compare_product_usable_qty(templateAB, 2) + + stockMoveInA.action_assign() + compare_product_usable_qty(productA, 2) + compare_product_usable_qty(templateAB, 2) + + stockMoveInA.action_done() + compare_product_usable_qty(productA, 2) + compare_product_usable_qty(templateAB, 2) + + # will directly trigger action_done on productB + stockMoveInB.action_done() + compare_product_usable_qty(productA, 2) + compare_product_usable_qty(productB, 3) + compare_product_usable_qty(templateAB, 5) + + # Create a stock move from STOCK to CUSTOMER + stockMoveOutA = moveObj.create( + {'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + 'name': ' STOCK --> CUSTOMER ', + 'product_id': productA.id, + 'product_uom': productA.uom_id.id, + 'product_uom_qty': 1, + 'state': 'confirmed', + }) + + stockMoveOutA.action_done() + compare_product_usable_qty(productA, 1) + compare_product_usable_qty(templateAB, 4) + + # Potential Qty is set as 0.0 by default + self.assertEquals(templateAB.potential_qty, 0.0) + self.assertEquals(productA.potential_qty, 0.0)