[FIX] stock_account: round SVL's value of incoming move

The SVL of in/out moves may be different because of rounding

To reproduce the issue:
(Need purchase,sale_management,stock)
1. In Settings, enable "Multi-Currencies"
2. Edit the USD currency:
    - Rounding factor: 1.0
3. Create a product category PC:
    - Costing Method: FIFO
4. Create a product P:
    - Type: Storable
    - Category: PC
5. Create a purchase order PO with one line:
    - Product: P
    - Quantity: 0.5
    - Unit Price: 3.0
    - Taxes: None
6. Confirm the PO and receive P
7. Create a sale order SO with 0.5 x P
8. Confirm the SO and deliver P
9. Inventory > Reporting > Inventory Valuation

Error: The quantity of P is zero but its total value is 0.50$

When receiving P, a SVL is created but does not round the value. So, we
have 0.5 x P in stock with a value equal to $1.50
Then, when deliver the product, the FIFO process is executed and create
a second SVL. However, this time the value is rounded:
https://github.com/odoo/odoo/blob/2e4fdcb84ba6375bb51fe71355168cebe2d922a0/addons/stock_account/models/product.py#L290
Therefore, the SVL of the out move has a value equal to $2. This
explains why, when checking the inventory valuation, the value is $-0.50

OPW-2724864

closes odoo/odoo#90251

X-original-commit: 306f3be81192eef504927e30483856669b36b047
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Adrien Widart
2022-05-02 10:37:12 +02:00
parent 287bd186f6
commit 9aac0f087e
2 changed files with 66 additions and 9 deletions
+7 -3
View File
@@ -156,9 +156,11 @@ class ProductProduct(models.Model):
:rtype: dict
"""
self.ensure_one()
company_id = self.env.context.get('force_company', self.env.company.id)
company = self.env['res.company'].browse(company_id)
vals = {
'product_id': self.id,
'value': unit_cost * quantity,
'value': company.currency_id.round(unit_cost * quantity),
'unit_cost': unit_cost,
'quantity': quantity,
}
@@ -175,11 +177,14 @@ class ProductProduct(models.Model):
:rtype: dict
"""
self.ensure_one()
company_id = self.env.context.get('force_company', self.env.company.id)
company = self.env['res.company'].browse(company_id)
currency = company.currency_id
# Quantity is negative for out valuation layers.
quantity = -1 * quantity
vals = {
'product_id': self.id,
'value': quantity * self.standard_price,
'value': currency.round(quantity * self.standard_price),
'unit_cost': self.standard_price,
'quantity': quantity,
}
@@ -188,7 +193,6 @@ class ProductProduct(models.Model):
vals['remaining_qty'] = fifo_vals.get('remaining_qty')
# In case of AVCO, fix rounding issue of standard price when needed.
if self.cost_method == 'average':
currency = self.env.company.currency_id
rounding_error = currency.round(self.standard_price * self.quantity_svl - self.value_svl)
if rounding_error:
# If it is bigger than the (smallest number of the currency * quantity) / 2,
@@ -30,19 +30,21 @@ class TestStockValuationCommon(TransactionCase):
# Counter automatically incremented by `_make_in_move` and `_make_out_move`.
self.days = 0
def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False):
def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False, loc_dest=None, pick_type=None):
""" Helper to create and validate a receipt move.
"""
unit_cost = unit_cost or product.standard_price
loc_dest = loc_dest or self.stock_location
pick_type = pick_type or self.picking_type_in
in_move = self.env['stock.move'].create({
'name': 'in %s units @ %s per unit' % (str(quantity), str(unit_cost)),
'product_id': product.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'location_dest_id': loc_dest.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': quantity,
'price_unit': unit_cost,
'picking_type_id': self.picking_type_in.id,
'picking_type_id': pick_type.id,
})
if create_picking:
@@ -61,17 +63,19 @@ class TestStockValuationCommon(TransactionCase):
self.days += 1
return in_move.with_context(svl=True)
def _make_out_move(self, product, quantity, force_assign=None, create_picking=False):
def _make_out_move(self, product, quantity, force_assign=None, create_picking=False, loc_src=None, pick_type=None):
""" Helper to create and validate a delivery move.
"""
loc_src = loc_src or self.stock_location
pick_type = pick_type or self.picking_type_out
out_move = self.env['stock.move'].create({
'name': 'out %s units' % str(quantity),
'product_id': product.id,
'location_id': self.stock_location.id,
'location_id': loc_src.id,
'location_dest_id': self.customer_location.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': quantity,
'picking_type_id': self.picking_type_out.id,
'picking_type_id': pick_type.id,
})
if create_picking:
@@ -301,6 +305,30 @@ class TestStockValuationStandard(TestStockValuationCommon):
self.assertTrue(product2.stock_valuation_layer_ids)
self.assertFalse(product1.stock_valuation_layer_ids)
def test_currency_precision_and_standard_svl_value(self):
currency = self.env['res.currency'].create({
'name': 'Odoo',
'symbol': 'O',
'rounding': 1,
})
new_company = self.env['res.company'].create({
'name': 'Super Company',
'currency_id': currency.id,
})
old_company = self.env.user.company_id
try:
self.env.user.company_id = new_company
warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)])
product = self.product1.with_company(new_company)
product.standard_price = 3
self._make_in_move(product, 0.5, loc_dest=warehouse.lot_stock_id, pick_type=warehouse.in_type_id)
self._make_out_move(product, 0.5, loc_src=warehouse.lot_stock_id, pick_type=warehouse.out_type_id)
self.assertEqual(product.value_svl, 0.0)
finally:
self.env.user.company_id = old_company
class TestStockValuationAVCO(TestStockValuationCommon):
@classmethod
@@ -708,6 +736,31 @@ class TestStockValuationFIFO(TestStockValuationCommon):
self.assertEqual(self.product1.value_svl, 20)
self.assertEqual(self.product1.quantity_svl, 10)
def test_currency_precision_and_fifo_svl_value(self):
currency = self.env['res.currency'].create({
'name': 'Odoo',
'symbol': 'O',
'rounding': 1,
})
new_company = self.env['res.company'].create({
'name': 'Super Company',
'currency_id': currency.id,
})
old_company = self.env.user.company_id
try:
self.env.user.company_id = new_company
product = self.product1.with_company(new_company)
product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
warehouse = self.env['stock.warehouse'].search([('company_id', '=', new_company.id)])
self._make_in_move(product, 0.5, loc_dest=warehouse.lot_stock_id, pick_type=warehouse.in_type_id, unit_cost=3)
self._make_out_move(product, 0.5, loc_src=warehouse.lot_stock_id, pick_type=warehouse.out_type_id)
self.assertEqual(product.value_svl, 0.0)
finally:
self.env.user.company_id = old_company
class TestStockValuationChangeCostMethod(TestStockValuationCommon):
def test_standard_to_fifo_1(self):