From 50f0b5ebaf47f7693d92d66fd12eae7fa9fb4c64 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Thu, 28 Sep 2017 11:23:25 +0200 Subject: [PATCH] [REF] stock_account: cost method changes When changing the costing method for a FIFO product to AVCO or standard, we update the standard price of the product to the average cost in stock. --- addons/stock_account/models/product.py | 22 ++- .../tests/test_stockvaluation.py | 136 ++++++++++++++++++ 2 files changed, 153 insertions(+), 5 deletions(-) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index d35299cf0a6..10d80789938 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -58,6 +58,15 @@ class ProductTemplate(models.Model): @api.one def _set_cost_method(self): + # When going from FIFO to AVCO or to standard, we update the standard price with the + # average value in stock. + if self.property_cost_method == 'fifo' and self.cost_method in ['average', 'standard']: + # Cannot use the `stock_value` computed field as it's already invalidated when + # entering this method. + valuation = sum([variant._sum_remaining_values() for variant in self.product_variant_ids]) + qty_available = self.with_context(company_owned=True).qty_available + if qty_available: + self.standard_price = valuation / qty_available return self.write({'property_cost_method': self.cost_method}) @api.multi @@ -158,17 +167,20 @@ class ProductProduct(models.Model): candidates = self.env['stock.move'].search(domain, order='date, id') return candidates + def _sum_remaining_values(self): + StockMove = self.env['stock.move'] + domain = [('product_id', '=', self.id)] + StockMove._get_all_base_domain() + moves = StockMove.search(domain) + return sum(moves.mapped('remaining_value')) + @api.multi - @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state') + @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'product_tmpl_id.cost_method') def _compute_stock_value(self): for product in self: if product.cost_method in ['standard', 'average']: product.stock_value = product.standard_price * product.with_context(company_owned=True).qty_available elif product.cost_method == 'fifo': - StockMove = self.env['stock.move'] - domain = [('product_id', '=', product.id)] + StockMove._get_all_base_domain() - moves = StockMove.search(domain) - product.stock_value = sum(moves.mapped('remaining_value')) + product.stock_value = product._sum_remaining_values() @api.multi def action_open_product_moves(self): diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index ab8cae3b905..3059e927acc 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -2149,3 +2149,139 @@ class TestStockValuation(TransactionCase): self.assertEqual(move7.value, 100.0) self.assertEqual(self.product1.standard_price, 10) + + def test_change_cost_method_1(self): + """ Change the cost method from FIFO to AVCO. + """ + # --------------------------------------------------------------------- + # Use FIFO, make some operations + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.cost_method = 'fifo' + + # receive 10@10 + move1 = self.env['stock.move'].create({ + 'name': '10 units @ 10.00 per unit', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 10.0 + move1._action_done() + + # receive 10@15 + move2 = self.env['stock.move'].create({ + 'name': '10 units @ 10.00 per unit', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + 'price_unit': 15, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 10.0 + move2._action_done() + + # sell 1 + move3 = self.env['stock.move'].create({ + 'name': 'Sale 5 units', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move3._action_confirm() + move3._action_assign() + move3.move_line_ids.qty_done = 1.0 + move3._action_done() + + self.assertEqual(self.product1.stock_value, 240) + + # --------------------------------------------------------------------- + # Change the production valuation to AVCO + # --------------------------------------------------------------------- + self.product1.with_context(debug=True).product_tmpl_id.cost_method = 'average' + + # valuation should stay to ~240 + self.assertAlmostEqual(self.product1.stock_value, 240, delta=0.03) + + # no accounting entry should be created + + # the cost should now be 12,65 + # (9 * 10) + (15 * 10) / 19 + self.assertEqual(self.product1.standard_price, 12.63) + + def test_change_cost_method_2(self): + """ Change the cost method from FIFO to standard. + """ + # --------------------------------------------------------------------- + # Use FIFO, make some operations + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.cost_method = 'fifo' + + # receive 10@10 + move1 = self.env['stock.move'].create({ + 'name': '10 units @ 10.00 per unit', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 10.0 + move1._action_done() + + # receive 10@15 + move2 = self.env['stock.move'].create({ + 'name': '10 units @ 10.00 per unit', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + 'price_unit': 15, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 10.0 + move2._action_done() + + # sell 1 + move3 = self.env['stock.move'].create({ + 'name': 'Sale 5 units', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move3._action_confirm() + move3._action_assign() + move3.move_line_ids.qty_done = 1.0 + move3._action_done() + + self.assertEqual(self.product1.stock_value, 240) + + # --------------------------------------------------------------------- + # Change the production valuation to AVCO + # --------------------------------------------------------------------- + self.product1.with_context(debug=True).product_tmpl_id.cost_method = 'standard' + + # valuation should stay to ~240 + self.assertAlmostEqual(self.product1.stock_value, 240, delta=0.03) + + # no accounting entry should be created + + # the cost should now be 12,65 + # (9 * 10) + (15 * 10) / 19 + self.assertEqual(self.product1.standard_price, 12.63) \ No newline at end of file