[FIX] account: fix usage of include_base_amount taxes in invoices

Suppose 3 taxes, all with include_base_amount checked:
- 21%
- 21% TVAC
- 12%

Suppose an invoice with 3 lines, one per tax, all with quantity = 1:
- price_unit = 100 with 21%
- price_unit = 121 with 21% TVAC
- price_unit = 100 with 12%

- Validate the invoice and take a look to the taxes set on taxes journal entries:
Originator Tax: 12%
Taxes: /

Originator Tax: 21%
Taxes: 12%

Originator Tax: 21% TVAC
Taxes: 21% 12%

... instead of:
Originator Tax: 12%
Taxes: /

Originator Tax: 21%
Taxes: /

Originator Tax: 21% TVAC
Taxes: /

-opw: 1861177
This commit is contained in:
Laurent Smet
2019-05-09 11:56:39 +00:00
committed by Olivier Colson
parent 4523a11bed
commit 574295df57
5 changed files with 195 additions and 23 deletions
+2 -1
View File
@@ -1030,7 +1030,8 @@ class AccountTax(models.Model):
return str(invoice_tax_val['tax_id']) + '-' + \
str(invoice_tax_val['account_id']) + '-' + \
str(invoice_tax_val['account_analytic_id']) + '-' + \
str(invoice_tax_val.get('analytic_tag_ids', []))
str(invoice_tax_val.get('analytic_tag_ids', [])) + '-' + \
str(invoice_tax_val.get('tax_ids') or [])
def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None):
""" Returns the amount of a single tax. base_amount is the actual amount on which the tax is applied, which is
+47 -22
View File
@@ -1077,12 +1077,13 @@ class AccountInvoice(models.Model):
else:
return self.env.ref('account.invoice_form').id
def _prepare_tax_line_vals(self, line, tax):
""" Prepare values to create an account.invoice.tax line
The line parameter is an account.invoice.line, and the
tax parameter is the output of account.tax.compute_all().
"""
def _prepare_tax_line_vals(self, line, tax, tax_ids):
''' Prepare values to create an account.invoice.tax line.
:param line: An account.invoice.line record.
:param tax: Tax values outputted by compute_all() in account.tax.
:param taxes: A list of account.tax ids affecting the tax base amount.
:return: The account.invoice.tax values to create a new record.
'''
vals = {
'invoice_id': self.id,
'name': tax['name'],
@@ -1094,6 +1095,7 @@ class AccountInvoice(models.Model):
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
'analytic_tag_ids': tax['analytic'] and line.analytic_tag_ids.ids or False,
'tax_ids': tax_ids and [(6, None, tax_ids)] or False,
}
# If the taxes generate moves on the same financial account as the invoice line,
@@ -1106,6 +1108,11 @@ class AccountInvoice(models.Model):
@api.multi
def get_taxes_values(self, tax_group_fields=False):
def is_tax_affecting_base_amount(tax):
return (tax.amount_type not in ('group', 'division') and tax.include_base_amount and tax.price_include)\
or (tax.amount_type == 'division' and tax.include_base_amount and not tax.price_include)
# Avoid redundant browsing.
tax_map = dict((t.id, t) for t in self.invoice_line_ids.mapped('invoice_line_tax_ids'))
default_tax_group_fields = set(['amount', 'base'])
if tax_group_fields:
default_tax_group_fields |= set(tax_group_fields)
@@ -1114,18 +1121,38 @@ class AccountInvoice(models.Model):
for line in self.invoice_line_ids:
if not line.account_id:
continue
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
for tax in taxes:
val = self._prepare_tax_line_vals(line, tax)
key = self.env['account.tax'].browse(tax['id']).get_grouping_key(val)
affecting_base_tax_ids = []
for tax_vals in taxes:
# Retrieve the tax record (not in tax_vals when dealing with group of taxes).
if tax_map.get(tax_vals['id']):
tax = tax_map[tax_vals['id']]
else:
tax = tax_map[tax_vals['id']] = self.env['account.tax'].browse(tax_vals['id'])
val = self._prepare_tax_line_vals(line, tax_vals, affecting_base_tax_ids)
key = tax.get_grouping_key({
'tax_id': val['tax_id'],
'account_id': val['account_id'],
'account_analytic_id': val['account_analytic_id'],
'analytic_tag_ids': val['analytic_tag_ids'],
'tax_ids': affecting_base_tax_ids,
})
if key not in tax_grouped:
tax_grouped[key] = val
tax_grouped[key]['base'] = round_curr(val['base'])
else:
for field in default_tax_group_fields:
tax_grouped[key][field] += round_curr(val.get(field) or 0)
tax_grouped[key][field] += val.get(field) or 0
if tax.amount_type == "group":
affecting_base_tax_ids += tax.children_tax_ids.filtered(lambda t: is_tax_affecting_base_amount(t)).ids
elif is_tax_affecting_base_amount(tax):
affecting_base_tax_ids.append(tax.id)
return tax_grouped
@api.multi
@@ -1232,19 +1259,14 @@ class AccountInvoice(models.Model):
res.append(move_line_dict)
return res
@api.model
@api.multi
def tax_line_move_line_get(self):
self.ensure_one()
res = []
# keep track of taxes already processed
done_taxes = []
# loop the invoice.tax.line in reversal sequence
for tax_line in sorted(self.tax_line_ids, key=lambda x: -x.sequence):
if tax_line.amount_total:
tax = tax_line.tax_id
if tax.amount_type == "group":
for child_tax in tax.children_tax_ids:
done_taxes.append(child_tax.id)
analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in tax_line.analytic_tag_ids]
res.append({
'invoice_tax_line_id': tax_line.id,
@@ -1258,9 +1280,8 @@ class AccountInvoice(models.Model):
'account_analytic_id': tax_line.account_analytic_id.id,
'analytic_tag_ids': analytic_tag_ids,
'invoice_id': self.id,
'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else []
'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
})
done_taxes.append(tax.id)
return res
def inv_line_characteristic_hashcode(self, invoice_line):
@@ -2034,7 +2055,7 @@ class AccountInvoiceLine(models.Model):
class AccountInvoiceTax(models.Model):
_name = "account.invoice.tax"
_description = "Invoice Tax"
_order = 'sequence'
_order = 'sequence, id desc'
def _prepare_invoice_tax_val(self):
self.ensure_one()
@@ -2042,7 +2063,9 @@ class AccountInvoiceTax(models.Model):
'tax_id': self.tax_id.id,
'account_id': self.account_id.id,
'account_analytic_id': self.account_analytic_id.id,
'analytic_tag_ids': self.analytic_tag_ids.ids or False}
'analytic_tag_ids': self.analytic_tag_ids.ids or False,
'tax_ids': self.tax_ids and tax.tax_ids.ids,
}
@api.depends('invoice_id.invoice_line_ids')
def _compute_base_amount(self):
@@ -2072,6 +2095,8 @@ class AccountInvoiceTax(models.Model):
company_id = fields.Many2one('res.company', string='Company', related='account_id.company_id', store=True, readonly=True)
currency_id = fields.Many2one('res.currency', related='invoice_id.currency_id', store=True, readonly=True)
base = fields.Monetary(string='Base', compute='_compute_base_amount', store=True)
tax_ids = fields.Many2many('account.tax', string='Affecting Base Taxes',
help='Taxes affecting the tax base amount applied before this one.')
@api.depends('amount', 'amount_rounding')
def _compute_amount_total(self):
+1
View File
@@ -16,6 +16,7 @@ from . import test_product_id_change
from . import test_reconciliation
from . import test_search
from . import test_tax
from . import test_invoice_taxes
from . import test_account_move_taxes_edition
from . import test_templates_consistency
from . import test_account_fiscal_year
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tests.common import Form
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestInvoiceTaxes(AccountingTestCase):
def setUp(self):
super(TestInvoiceTaxes, self).setUp()
self.percent_tax_1 = self.env['account.tax'].create({
'name': '21%',
'amount_type': 'percent',
'amount': 21,
'sequence': 10,
})
self.percent_tax_1_incl = self.env['account.tax'].create({
'name': '21% incl',
'amount_type': 'percent',
'amount': 21,
'price_include': True,
'include_base_amount': True,
'sequence': 20,
})
self.percent_tax_2 = self.env['account.tax'].create({
'name': '12%',
'amount_type': 'percent',
'amount': 12,
'sequence': 30,
})
self.group_tax = self.env['account.tax'].create({
'name': 'group 12% + 21%',
'amount_type': 'group',
'amount': 21,
'children_tax_ids': [
(4, self.percent_tax_1_incl.id),
(4, self.percent_tax_2.id)
],
'sequence': 40,
})
def _create_invoice(self, taxes_per_line):
''' Create an invoice on the fly.
:param taxes_per_line: A list of tuple (price_unit, account.tax recordset)
'''
self_ctx = self.env['account.invoice'].with_context(type='out_invoice')
journal_id = self_ctx._default_journal().id
self_ctx = self_ctx.with_context(journal_id=journal_id)
with Form(self_ctx, view='account.invoice_form') as invoice_form:
invoice_form.partner_id = self.env.ref('base.partner_demo')
for amount, taxes in taxes_per_line:
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.name = 'xxxx'
invoice_line_form.quantity = 1
invoice_line_form.price_unit = amount
invoice_line_form.invoice_line_tax_ids.clear()
for tax in taxes:
invoice_line_form.invoice_line_tax_ids.add(tax)
return invoice_form.save()
def test_one_tax_per_line(self):
''' Test:
price_unit | Taxes
------------------
100 | 21%
121 | 21% incl
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% | / | 100 | 21
21% incl | / | 100 | 21
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(100, self.percent_tax_1),
(121, self.percent_tax_1_incl),
(100, self.percent_tax_2),
])
invoice.action_invoice_open()
self.assertRecordValues(invoice.tax_line_ids, [
{'name': self.percent_tax_1.name, 'base': 100, 'amount': 21, 'tax_ids': []},
{'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []},
])
def test_affecting_base_amount(self):
''' Test:
price_unit | Taxes
------------------
121 | 21% incl, 12%
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% incl | / | 100 | 21
12% | 21% incl | 121 | 14.52
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(121, self.percent_tax_1_incl + self.percent_tax_2),
(100, self.percent_tax_2),
])
invoice.action_invoice_open()
self.assertRecordValues(invoice.tax_line_ids, [
{'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]},
])
def test_group_of_taxes(self):
''' Test:
price_unit | Taxes
------------------
121 | 21% incl + 12%
100 | 12%
Expected:
Tax | Taxes | Base | Amount
--------------------------------------------
21% incl | / | 100 | 21
12% | 21% incl | 121 | 14.52
12% | / | 100 | 12
'''
invoice = self._create_invoice([
(121, self.group_tax),
(100, self.percent_tax_2),
])
invoice.action_invoice_open()
self.assertRecordValues(invoice.tax_line_ids, [
{'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []},
{'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]},
])
@@ -124,6 +124,7 @@
<field name="name"/>
<field name="account_id" groups="account.group_account_user"/>
<field name="base"/>
<field name="tax_ids"/>
<field name="amount_total" string="Amount"/>
<field name="currency_id" invisible="1"/>
</tree>
@@ -143,6 +144,7 @@
<field name="manual"/>
<field name="amount_total" string="Amount"/>
<field name="base"/>
<field name="tax_ids"/>
<field name="currency_id" invisible="1"/>
</group>
</form>
@@ -353,6 +355,7 @@
<field name="name"/>
<!--Need invisible fields for on_change to create all necessary info -->
<field name="tax_id" invisible="1"/>
<field name="tax_ids" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="manual" invisible="1"/>
<field name="account_id" groups="account.group_account_user"/>
@@ -618,6 +621,7 @@
<field name="name"/>
<!--Need invisible fields for on_change to create all necessary info -->
<field name="tax_id" invisible="1"/>
<field name="tax_ids" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="manual" invisible="1"/>
<field name="account_id" groups="account.group_account_user"/>