[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:
anhe-odoo
2022-03-16 07:08:44 +01:00
parent 1520a98d69
commit 099f20ab7c
2 changed files with 73 additions and 19 deletions
+2 -2
View File
@@ -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