Skip to content

Commit

Permalink
[FIX] purchase_stock: consider product unit price precision
Browse files Browse the repository at this point in the history
When changing the product price precision, this can lead to incorrect
stock valuations.

To reproduce the error:
(Enable debug mode)
1. Go to Settings > Technical > Database Strucutre > Decimal Accuracy
2. Edit Product Price:
    - Digits: 4
3. Create a Product Category PC:
    - Costing Method: FIFO
4. Create a Product P:
    - Product Type: Storable Product
    - Product Category: PC
5. Create a RfQ with product P:
    - Quantity: 1000
    - Unit Price: 0.035
6. Confirm Order, Receive Products, Validate
7. Click on Valuation

Error: The total value is equal to $40 instead of $35. The calculation
was done after rounding the unit price: $0.035 becomes $0.04, then
1000*0.04=$40.

When confirming the RfQ, a stock move is created. To do so, the method
`_get_stock_move_price_unit` is called. When validating the delivery, it
recomputes the unit price thanks to method `_get_price_unit`. In both
situation, and if the line has taxes, the method `compute_all` is called
like this:
```python
price_unit = line.taxes_id.with_context(round=False).compute_all(price_unit,
	currency=line.order_id.currency_id, quantity=1.0)['total_void']
```
But here is the problem. In this method, total amount is computed with
this line:
```python
base = currency.round(price_unit * quantity)
```
However, `quantity` is equal to 1 and the multiplication is rounded
using the currency precision. As a result, `base` is equal to $0.04.
Then, all computations will use this value and will be incorrect.

This fix applies the real quantity so `base` will have the correct
value:
```
base = currency.round(price_unit * quantity)
     = currency.round(0.035 * 1000)
     = 35
```

OPW-2472192

closes #69297

Signed-off-by: William Henrotin <Whenrow@users.noreply.github.com>
  • Loading branch information
adwid committed Apr 28, 2021
1 parent 18daadc commit a57dc07
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 3 deletions.
7 changes: 5 additions & 2 deletions addons/purchase_stock/models/purchase.py
Expand Up @@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _
from odoo.tools.float_utils import float_compare
from odoo.tools.float_utils import float_compare, float_round
from dateutil import relativedelta
from odoo.exceptions import UserError

Expand Down Expand Up @@ -345,10 +345,13 @@ def _get_stock_move_price_unit(self):
line = self[0]
order = line.order_id
price_unit = line.price_unit
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
if line.taxes_id:
qty = line.product_qty or 1
price_unit = line.taxes_id.with_context(round=False).compute_all(
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
price_unit, currency=line.order_id.currency_id, quantity=qty, product=line.product_id, partner=line.order_id.partner_id
)['total_void']
price_unit = float_round(price_unit / qty, precision_digits=price_unit_prec)
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
Expand Down
6 changes: 5 additions & 1 deletion addons/purchase_stock/models/stock.py
Expand Up @@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _
from odoo.tools.float_utils import float_round


class StockPicking(models.Model):
Expand Down Expand Up @@ -36,11 +37,14 @@ def _get_price_unit(self):
""" Returns the unit price for the move"""
self.ensure_one()
if self.purchase_line_id and self.product_id.id == self.purchase_line_id.product_id.id:
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
line = self.purchase_line_id
order = line.order_id
price_unit = line.price_unit
if line.taxes_id:
price_unit = line.taxes_id.with_context(round=False).compute_all(price_unit, currency=line.order_id.currency_id, quantity=1.0)['total_void']
qty = line.product_qty or 1
price_unit = line.taxes_id.with_context(round=False).compute_all(price_unit, currency=line.order_id.currency_id, quantity=qty)['total_void']
price_unit = float_round(price_unit / qty, precision_digits=price_unit_prec)
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
Expand Down
49 changes: 49 additions & 0 deletions addons/purchase_stock/tests/test_fifo_price.py
Expand Up @@ -315,3 +315,52 @@ def test_00_test_fifo(self):
original_out_move = outgoing_shipment_neg.move_lines[0]
self.assertEquals(original_out_move.product_id.value_svl, 12000.0, 'Value of the move should be 12000')
self.assertEquals(original_out_move.product_id.qty_available, 150.0, 'Qty available should be 150')

def test_01_test_fifo(self):
"""" This test ensures that unit price keeps its decimal precision """

self._load('account', 'test', 'account_minimal_test.xml')
self._load('stock_account', 'test', 'stock_valuation_account.xml')

unit_price_precision = self.env['ir.model.data'].xmlid_to_object('product.decimal_price')
unit_price_precision.digits = 3

tax = self.env["account.tax"].create({
"name": "Dummy Tax",
"amount": "0.00",
"type_tax_use": "purchase",
})

super_product = self.env['product.product'].create({
'name': 'Super Product',
'type': 'product',
'categ_id': self.env.ref('product.product_category_1').id,
'standard_price': 0.035,
})
super_product.categ_id.property_cost_method = 'fifo'
super_product.categ_id.property_valuation = 'real_time'
super_product.categ_id.property_stock_account_input_categ_id = self.ref('purchase.o_expense')
super_product.categ_id.property_stock_account_output_categ_id = self.ref('purchase.o_income')

purchase_order = self.env['purchase.order'].create({
'partner_id': self.env.ref('base.res_partner_3').id,
'order_line': [(0, 0, {
'name': super_product.name,
'product_id': super_product.id,
'product_qty': 1000,
'product_uom': super_product.uom_id.id,
'price_unit': super_product.standard_price,
'date_planned': time.strftime('%Y-%m-%d'),
'taxes_id': [(4, tax.id)],
})],
})

purchase_order.button_confirm()
self.assertEqual(purchase_order.state, 'purchase')

picking = purchase_order.picking_ids[0]
self.env['stock.immediate.transfer'].create({'pick_ids': [(4, picking.id)]}).process()

self.assertEqual(super_product.standard_price, 0.035)
self.assertEqual(super_product.value_svl, 35.0)
self.assertEqual(picking.move_lines.price_unit, 0.035)

0 comments on commit a57dc07

Please sign in to comment.