Skip to content

Commit

Permalink
[FIX] account: Fix price_unit rounding issue with fpos/price included…
Browse files Browse the repository at this point in the history
… tax

- Create an invoice with an empty fiscal position
- Create a line with a product having 100.0 as sale price and 21.0% price-included tax
=> price_unit equals 99.99

This is because 100 / 1.21 ~= 82.64 but 82.64 * 1.21 ~= 99.99 != 100.0.
The bug only appears when managing a fiscal position because the code is trying to adapt the product price_unit to the newly computed taxes.

closes odoo#70276

X-original-commit: 3470a3c
Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com>
Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
  • Loading branch information
smetl committed May 4, 2021
1 parent a419272 commit b80e214
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 30 deletions.
98 changes: 69 additions & 29 deletions addons/account/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -3264,23 +3264,75 @@ def _get_computed_name(self):
return '\n'.join(values)

def _get_computed_price_unit(self):
''' Helper to get the default price unit based on the product by taking care of the taxes
set on the product and the fiscal position.
:return: The price unit.
'''
self.ensure_one()

if not self.product_id:
return self.price_unit
elif self.move_id.is_sale_document(include_receipts=True):
# Out invoice.
price_unit = self.product_id.lst_price
return 0.0

company = self.move_id.company_id
currency = self.move_id.currency_id
company_currency = company.currency_id
product_uom = self.product_id.uom_id
fiscal_position = self.move_id.fiscal_position_id
is_refund_document = self.move_id.move_type in ('out_refund', 'in_refund')
move_date = self.move_id.date or fields.Date.context_today(self)

if self.move_id.is_sale_document(include_receipts=True):
product_price_unit = self.product_id.lst_price
product_taxes = self.product_id.taxes_id
elif self.move_id.is_purchase_document(include_receipts=True):
# In invoice.
price_unit = self.product_id.standard_price
product_price_unit = self.product_id.standard_price
product_taxes = self.product_id.supplier_taxes_id
else:
return self.price_unit
return 0.0
product_taxes = product_taxes.filtered(lambda tax: tax.company_id == company)

# Apply unit of measure.
if self.product_uom_id and self.product_uom_id != product_uom:
product_price_unit = product_uom._compute_price(product_price_unit, self.product_uom_id)

# Apply fiscal position.
if product_taxes and fiscal_position:
product_taxes_after_fp = fiscal_position.map_tax(product_taxes, partner=self.partner_id)

if set(product_taxes.ids) != set(product_taxes_after_fp.ids):
flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy()
if any(tax.price_include for tax in flattened_taxes_before_fp):
taxes_res = flattened_taxes_before_fp.compute_all(
product_price_unit,
quantity=1.0,
currency=company_currency,
product=self.product_id,
partner=self.partner_id,
is_refund=is_refund_document,
)
product_price_unit = company_currency.round(taxes_res['total_excluded'])

flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy()
if any(tax.price_include for tax in flattened_taxes_after_fp):
taxes_res = flattened_taxes_after_fp.compute_all(
product_price_unit,
quantity=1.0,
currency=company_currency,
product=self.product_id,
partner=self.partner_id,
is_refund=is_refund_document,
handle_price_include=False,
)
for tax_res in taxes_res['taxes']:
tax = self.env['account.tax'].browse(tax_res['id'])
if tax.price_include:
product_price_unit += tax_res['amount']

if self.product_uom_id != self.product_id.uom_id:
price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom_id)
# Apply currency rate.
if currency and currency != company_currency:
product_price_unit = company_currency._convert(product_price_unit, currency, company, move_date)

return price_unit
return product_price_unit

def _get_computed_account(self):
self.ensure_one()
Expand Down Expand Up @@ -3581,33 +3633,21 @@ def _onchange_product_id(self):

line.name = line._get_computed_name()
line.account_id = line._get_computed_account()
line.tax_ids = line._get_computed_taxes()
taxes = line._get_computed_taxes()
if taxes and line.move_id.fiscal_position_id:
taxes = line.move_id.fiscal_position_id.map_tax(taxes, partner=line.partner_id)
line.tax_ids = taxes
line.product_uom_id = line._get_computed_uom()
line.price_unit = line._get_computed_price_unit()

# price_unit and taxes may need to be adapted following Fiscal Position
line._set_price_and_tax_after_fpos()

# Convert the unit price to the invoice's currency.
company = line.move_id.company_id
line.price_unit = company.currency_id._convert(line.price_unit, line.move_id.currency_id, company, line.move_id.date, round=False)

@api.onchange('product_uom_id')
def _onchange_uom_id(self):
''' Recompute the 'price_unit' depending of the unit of measure. '''
price_unit = self._get_computed_price_unit()

# See '_onchange_product_id' for details.
taxes = self._get_computed_taxes()
if taxes and self.move_id.fiscal_position_id:
price_subtotal = self._get_price_total_and_subtotal(price_unit=price_unit, taxes=taxes)['price_subtotal']
accounting_vals = self._get_fields_onchange_subtotal(price_subtotal=price_subtotal, currency=self.move_id.company_currency_id)
amount_currency = accounting_vals['amount_currency']
price_unit = self._get_fields_onchange_balance(amount_currency=amount_currency, force_computation=True).get('price_unit', price_unit)

# Convert the unit price to the invoice's currency.
company = self.move_id.company_id
self.price_unit = company.currency_id._convert(price_unit, self.move_id.currency_id, company, self.move_id.date, round=False)
taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
self.tax_ids = taxes
self.price_unit = self._get_computed_price_unit()

@api.onchange('account_id')
def _onchange_account_id(self):
Expand Down
104 changes: 103 additions & 1 deletion addons/account/tests/test_account_move_out_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ def test_out_invoice_line_onchange_taxes_1(self):
'amount_total': 1730.0,
})

def test_out_invoice_line_onchange_rounding_price_subtotal(self):
def test_out_invoice_line_onchange_rounding_price_subtotal_1(self):
''' Seek for rounding issue on the price_subtotal when dealing with a price_unit having more digits than the
foreign currency one.
'''
Expand Down Expand Up @@ -905,6 +905,108 @@ def check_invoice_values(invoice):

check_invoice_values(invoice_2)

def test_out_invoice_line_onchange_rounding_price_subtotal_2(self):
""" Ensure the cyclic computations implemented using onchanges are not leading to rounding issues when using
price-included taxes.
For example:
100 / 1.21 ~= 82.64 but 82.64 * 1.21 ~= 99.99 != 100.0.
"""

def check_invoice_values(invoice):
self.assertInvoiceValues(invoice, [
{
'price_unit': 100.0,
'price_subtotal': 82.64,
'debit': 0.0,
'credit': 82.64,
},
{
'price_unit': 17.36,
'price_subtotal': 17.36,
'debit': 0.0,
'credit': 17.36,
},
{
'price_unit': -100.0,
'price_subtotal': -100.0,
'debit': 100.0,
'credit': 0.0,
},
], {
'amount_untaxed': 82.64,
'amount_tax': 17.36,
'amount_total': 100.0,
})

tax = self.env['account.tax'].create({
'name': '21%',
'amount': 21.0,
'price_include': True,
'include_base_amount': True,
})

# == Test assigning tax directly ==

invoice_create = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': '2017-01-01',
'date': '2017-01-01',
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {
'name': 'test line',
'price_unit': 100.0,
'account_id': self.company_data['default_account_revenue'].id,
'tax_ids': [(6, 0, tax.ids)],
})],
})

check_invoice_values(invoice_create)

move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
move_form.invoice_date = fields.Date.from_string('2017-01-01')
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.name = 'test line'
line_form.price_unit = 100.0
line_form.account_id = self.company_data['default_account_revenue']
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
invoice_onchange = move_form.save()

check_invoice_values(invoice_onchange)

# == Test when the tax is set on a product ==

product = self.env['product.product'].create({
'name': 'product',
'lst_price': 100.0,
'property_account_income_id': self.company_data['default_account_revenue'].id,
'taxes_id': [(6, 0, tax.ids)],
})

move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
move_form.invoice_date = fields.Date.from_string('2017-01-01')
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = product
invoice_onchange = move_form.save()

check_invoice_values(invoice_onchange)

# == Test with a fiscal position ==

fiscal_position = self.env['account.fiscal.position'].create({'name': 'fiscal_position'})

move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
move_form.invoice_date = fields.Date.from_string('2017-01-01')
move_form.partner_id = self.partner_a
move_form.fiscal_position_id = fiscal_position
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = product
invoice_onchange = move_form.save()

check_invoice_values(invoice_onchange)

def test_out_invoice_line_onchange_taxes_2_price_unit_tax_included(self):
''' Seek for rounding issue in the price unit. Suppose a price_unit of 2300 with a 5.5% price-included tax
applied on it.
Expand Down

0 comments on commit b80e214

Please sign in to comment.