[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) <whe@odoo.com> Signed-off-by: Hendrickx Anthony (anhe) <anhe@odoo.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user