diff options
| author | Laurent Smet <las@odoo.com> | 2018-09-07 07:14:51 +0000 |
|---|---|---|
| committer | Olivier Colson <oco@odoo.com> | 2019-05-09 11:56:39 +0000 |
| commit | 574295df57d8da78ca6109f277a5a2332bf38101 (patch) | |
| tree | 7a0a55ab0c9af4c176901442241f6d650da01d8f | |
| parent | 4523a11bed332d7cb1f1846d2ff6b54f83afe430 (diff) | |
[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
| -rw-r--r-- | addons/account/models/account.py | 3 | ||||
| -rw-r--r-- | addons/account/models/account_invoice.py | 69 | ||||
| -rw-r--r-- | addons/account/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/account/tests/test_invoice_taxes.py | 141 | ||||
| -rw-r--r-- | addons/account/views/account_invoice_view.xml | 4 |
5 files changed, 195 insertions, 23 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 8bc67fcf1e8..431cd15fbce 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -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 diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 28e0a4429ac..e11c31ae6a0 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -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): diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index e87ae79c27a..0eb1d84e613 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -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 diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py new file mode 100644 index 00000000000..bb8dd06b617 --- /dev/null +++ b/addons/account/tests/test_invoice_taxes.py @@ -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]}, + ]) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index e81ca343db5..b8dd9c25e05 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -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"/> |
