From 9aac0f087ea9fda5d4c8ed6f9c21ee0523eabc68 Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Fri, 25 Mar 2022 12:59:56 +0000 Subject: [PATCH] [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) Signed-off-by: Adrien Widart --- addons/stock_account/models/product.py | 10 ++- .../tests/test_stockvaluationlayer.py | 65 +++++++++++++++++-- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index cb4c247d244..f024414af2f 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -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, diff --git a/addons/stock_account/tests/test_stockvaluationlayer.py b/addons/stock_account/tests/test_stockvaluationlayer.py index b92f197f131..fb54165f2c5 100644 --- a/addons/stock_account/tests/test_stockvaluationlayer.py +++ b/addons/stock_account/tests/test_stockvaluationlayer.py @@ -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):