[FIX] account: Fix price_unit rounding issue with fpos/price included 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/odoo#70276 X-original-commit: 3470a3c94e06d92afcd8a8129f80e4dfea14efe4 Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com> Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
@@ -3264,23 +3264,75 @@ class AccountMoveLine(models.Model):
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
return price_unit
|
||||
# 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']
|
||||
|
||||
# Apply currency rate.
|
||||
if currency and currency != company_currency:
|
||||
product_price_unit = company_currency._convert(product_price_unit, currency, company, move_date)
|
||||
|
||||
return product_price_unit
|
||||
|
||||
def _get_computed_account(self):
|
||||
self.ensure_one()
|
||||
@@ -3581,33 +3633,21 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
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):
|
||||
|
||||
@@ -831,7 +831,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
'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.
|
||||
'''
|
||||
@@ -905,6 +905,108 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user