[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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user