From 099f20ab7c58e303fbc74a6f826efb0ea4e37839 Mon Sep 17 00:00:00 2001 From: anhe-odoo Date: Mon, 7 Mar 2022 08:19:11 +0000 Subject: [PATCH] [FIX] stock_account: don't update inventory valuation for negative quantity Expected Behaviour When updating the cost of a product with a negative quantity and then making an inventory adjustment, the final value should be corrected according to the current cost of the product Observed Behaviour In the case we had a negative quantity when changing the cost, the compensation layer will be ignored when doing an inventory adjustment afterward, leading to an incorrect total value in the inventory valuation Reproducibility This bug can be reproduced following these steps: - Create Storable product with costing method = AVCO Automated, Update cost to 100$ - Sell 10 units of this product (Inventory valuation at -1000 $ for -10 units) - Update cost of the product to 10 $ (Inventory valuation at -100 $ for -10 units) - Create an inventory adjustment to set the quantity of the product to 0. ==> Inventory valuation is a 900 $ for 0 unit of our product Related Issues/PR - opw-2635692 closes odoo/odoo#86389 X-original-commit: 673a6b69e335b29d011b7c5743c3fe101445a5e1 Signed-off-by: William Henrotin (whe) Signed-off-by: Hendrickx Anthony (anhe) --- addons/stock_account/models/product.py | 4 +- .../tests/test_stockvaluation.py | 88 +++++++++++++++---- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 3da76270bf1..9b792186481 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -3,7 +3,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_is_zero, float_repr +from odoo.tools import float_is_zero, float_repr, float_compare from odoo.exceptions import ValidationError from collections import defaultdict @@ -221,7 +221,7 @@ class ProductProduct(models.Model): if product.cost_method not in ('standard', 'average'): continue quantity_svl = product.sudo().quantity_svl - if float_is_zero(quantity_svl, precision_rounding=product.uom_id.rounding): + if float_compare(quantity_svl, 0.0, precision_rounding=product.uom_id.rounding) <= 0: continue diff = new_price - product.standard_price value = company_id.currency_id.round(quantity_svl * diff) diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index d1037ad2b76..ea64311af14 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -2827,6 +2827,60 @@ class TestStockValuation(TransactionCase): self.assertEqual(move7.stock_valuation_layer_ids.value, 100.0) self.assertEqual(self.product1.standard_price, 10) + def test_average_automated_with_cost_change(self): + """ Test of the handling of a cost change with a negative stock quantity with FIFO+AVCO costing method""" + self.product1.categ_id.property_cost_method = 'average' + self.product1.categ_id.property_valuation = 'real_time' + + # Step 1: Sell (and confirm) 10 units we don't have @ 100 + self.product1.standard_price = 100 + move1 = self.env['stock.move'].create({ + 'name': 'Sale 10 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': 10.0, + }) + move1._action_confirm() + move1.quantity_done = 10.0 + move1._action_done() + + self.assertAlmostEqual(self.product1.quantity_svl, -10.0) + self.assertEqual(move1.stock_valuation_layer_ids.value, -1000.0) + self.assertAlmostEqual(self.product1.value_svl, -1000.0) + + # Step2: Change product cost from 100 to 10 -> Nothing should appear in inventory + # valuation as the quantity is negative + self.product1.standard_price = 10 + self.assertEqual(self.product1.value_svl, -1000.0) + + # Step 3: Make an inventory adjustment to set to total counted value at 0 -> Inventory + # valuation should be at 0 with a compensation layer at 900 (1000 - 100) + inventory_location = self.product1.property_stock_inventory + inventory_location.company_id = self.env.company.id + + move2 = self.env['stock.move'].create({ + 'name': 'Adjustment of 10 units', + 'location_id': inventory_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, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 10.0 + move2._action_done() + + # Check if the move adjustment has correctly been done + self.assertAlmostEqual(self.product1.quantity_svl, 0.0) + self.assertAlmostEqual(move2.stock_valuation_layer_ids.value, 100.0) + + # Check if the compensation layer is as expected, with final inventory value being 0 + self.assertAlmostEqual(self.product1.stock_valuation_layer_ids.sorted()[-1].value, 900.0) + self.assertAlmostEqual(self.product1.value_svl, 0.0) + def test_average_manual_1(self): ''' Set owner on incoming move => no valuation ''' self.product1.categ_id.property_cost_method = 'average' @@ -3331,41 +3385,41 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product1.quantity_svl, 15) self.assertEqual(self.product1.value_svl, 75) - # send 20 + # send 10 move4 = self.env['stock.move'].create({ 'name': 'out 10', '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': 20, + 'product_uom_qty': 10, }) move4._action_confirm() move4._action_assign() - move4.move_line_ids.qty_done = 20 + move4.move_line_ids.qty_done = 10 move4._action_done() move4.date = date6 move4.stock_valuation_layer_ids._write({'create_date': date6}) - self.assertEqual(self.product1.quantity_svl, -5) - self.assertEqual(self.product1.value_svl, -25) + self.assertEqual(self.product1.quantity_svl, 5) + self.assertEqual(self.product1.value_svl, 25.0) # set the standard price to 7.5 self.product1.standard_price = 7.5 self.product1.stock_valuation_layer_ids.sorted()[-1]._write({'create_date': date7}) - # receive 100 + # receive 90 move5 = self.env['stock.move'].create({ 'name': 'in 10', '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': 100, + 'product_uom_qty': 90, }) move5._action_confirm() move5._action_assign() - move5.move_line_ids.qty_done = 100 + move5.move_line_ids.qty_done = 90 move5._action_done() move5.date = date8 move5.stock_valuation_layer_ids._write({'create_date': date8}) @@ -3379,8 +3433,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).quantity_svl, 30) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).quantity_svl, 15) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).quantity_svl, 15) - self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).quantity_svl, -5) - self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date7)).quantity_svl, -5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).quantity_svl, 5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date7)).quantity_svl, 5) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).quantity_svl, 95) # Valuation at date @@ -3389,7 +3443,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).value_svl, 300) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).value_svl, 150) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).value_svl, 75) - self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, -25) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, 25) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).value_svl, 712.5) # edit the done quantity of move1, decrease it @@ -3404,16 +3458,16 @@ class TestStockValuation(TransactionCase): # but the change is still only visible right now self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 100) - # edit move 4, send 15 instead of 20 + # edit move 4, send 15 instead of 10 move4.quantity_done = 15 - # -(20*5) + (5*7.5) - self.assertEqual(sum(move4.stock_valuation_layer_ids.mapped('value')), -62.5) + # -(10*5) - (5*7.5) + self.assertEqual(sum(move4.stock_valuation_layer_ids.mapped('value')), -87.5) # the change is only visible right now - self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, -25) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, 25) - self.assertEqual(self.product1.quantity_svl, 95) - self.assertEqual(self.product1.value_svl, 712.5) + self.assertEqual(self.product1.quantity_svl, 85) + self.assertEqual(self.product1.value_svl, 637.5) def test_at_date_fifo_1(self): """ Make some operations at different dates, check that the results of the valuation at