diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 811013f2094..71622aa660a 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -12,6 +12,7 @@ from . import account_tax_report from . import account_reconcile_model from . import account_payment_term from . import account_move +from . import account_move_line_tax_details from . import account_analytic_default from . import account_partial_reconcile from . import account_full_reconcile diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d1ad972ea91..50b49e4b3e4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -550,6 +550,7 @@ class AccountMove(models.Model): ''' return { 'tax_repartition_line_id': tax_line.tax_repartition_line_id.id, + 'group_tax_id': tax_line.group_tax_id.id, 'account_id': tax_line.account_id.id, 'currency_id': tax_line.currency_id.id, 'analytic_tag_ids': [(6, 0, tax_line.tax_line_id.analytic and tax_line.analytic_tag_ids.ids or [])], @@ -570,6 +571,7 @@ class AccountMove(models.Model): account = base_line._get_default_tax_account(tax_repartition_line) or base_line.account_id return { 'tax_repartition_line_id': tax_vals['tax_repartition_line_id'], + 'group_tax_id': tax_vals['group'].id if tax_vals['group'] else False, 'account_id': account.id, 'currency_id': base_line.currency_id.id, 'analytic_tag_ids': [(6, 0, tax_vals['analytic'] and base_line.analytic_tag_ids.ids or [])], @@ -3231,6 +3233,12 @@ class AccountMoveLine(models.Model): context={'active_test': False}, check_company=True, help="Taxes that apply on the base amount") + group_tax_id = fields.Many2one( + comodel_name='account.tax', + string="Originator Group of Taxes", + index=True, + help="The group of taxes generator of this tax line", + ) tax_line_id = fields.Many2one('account.tax', string='Originator Tax', ondelete='restrict', store=True, compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line") tax_group_id = fields.Many2one(related='tax_line_id.tax_group_id', string='Originator tax group', diff --git a/addons/account/models/account_move_line_tax_details.py b/addons/account/models/account_move_line_tax_details.py new file mode 100644 index 00000000000..ffe580adbcf --- /dev/null +++ b/addons/account/models/account_move_line_tax_details.py @@ -0,0 +1,479 @@ +# -*- coding: utf-8 -*- + +from odoo import api, models + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + @api.model + def _get_query_tax_details_from_domain(self, domain, fallback=True): + """ Create the tax details sub-query based on the orm domain passed as parameter. + + :param domain: An orm domain on account.move.line. + :param fallback: Fallback on an approximated mapping if the mapping failed. + :return: A tuple . + """ + self.env['account.move.line'].check_access_rights('read') + + query = self.env['account.move.line']._where_calc(domain) + + # Wrap the query with 'company_id IN (...)' to avoid bypassing company access rights. + self.env['account.move.line']._apply_ir_rules(query) + + tables, where_clause, where_params = query.get_sql() + return self._get_query_tax_details(tables, where_clause, where_params, fallback=fallback) + + @api.model + def _get_query_tax_details(self, tables, where_clause, where_params, fallback=True): + """ Create the tax details sub-query based on the orm domain passed as parameter. + + :param tables: The 'tables' query to inject after the FROM. + :param where_clause: The 'where_clause' query computed based on an orm domain. + :param where_params: The params to fill the 'where_clause' query. + :param fallback: Fallback on an approximated mapping if the mapping failed. + :return: A tuple . + """ + group_taxes = self.env['account.tax'].search([('amount_type', '=', 'group')]) + + group_taxes_query_list = [] + group_taxes_params = [] + for group_tax in group_taxes: + children_taxes = group_tax.children_tax_ids + if not children_taxes: + continue + + children_taxes_in_query = ','.join('%s' for dummy in children_taxes) + group_taxes_query_list.append(f'WHEN tax.id = %s THEN ARRAY[{children_taxes_in_query}]') + group_taxes_params.append(group_tax.id) + group_taxes_params.extend(children_taxes.ids) + + if group_taxes_query_list: + group_taxes_query = f'''UNNEST(CASE {' '.join(group_taxes_query_list)} ELSE ARRAY[tax.id] END)''' + else: + group_taxes_query = 'tax.id' + + if fallback: + fallback_query = f''' + UNION ALL + + SELECT + account_move_line.id AS tax_line_id, + base_line.id AS base_line_id, + base_line.id AS src_line_id, + base_line.balance AS base_amount, + base_line.amount_currency AS base_amount_currency + FROM {tables} + LEFT JOIN base_tax_line_mapping ON + base_tax_line_mapping.tax_line_id = account_move_line.id + JOIN account_move_line_account_tax_rel tax_rel ON + tax_rel.account_tax_id = COALESCE(account_move_line.group_tax_id, account_move_line.tax_line_id) + JOIN account_move_line base_line ON + base_line.id = tax_rel.account_move_line_id + AND base_line.tax_repartition_line_id IS NULL + AND base_line.move_id = account_move_line.move_id + AND base_line.currency_id = account_move_line.currency_id + WHERE base_tax_line_mapping.tax_line_id IS NULL + AND {where_clause} + ''' + fallback_params = where_params + else: + fallback_query = '' + fallback_params = [] + + return f''' + /* + As example to explain the different parts of the query, we'll consider a move with the following lines: + Name Tax_line_id Tax_ids Debit Credit Base lines + --------------------------------------------------------------------------------------------------- + base_line_1 10_affect_base, 20 1000 + base_line_2 10_affect_base, 5 2000 + base_line_3 10_affect_base, 5 3000 + tax_line_1 10_affect_base 20 100 base_line_1 + tax_line_2 20 220 base_line_1 + tax_line_3 10_affect_base 5 500 base_line_2/3 + tax_line_4 5 275 base_line_2/3 + */ + + WITH affecting_base_tax_ids AS ( + + /* + This CTE builds a reference table based on the tax_ids field, with the following changes: + - flatten the group of taxes + - exclude the taxes having 'is_base_affected' set to False. + Those allow to match only base_line_1 when finding the base lines of tax_line_1, as we need to find + base lines having a 'affecting_base_tax_ids' ending with [10_affect_base, 20], not only containing + '10_affect_base'. Otherwise, base_line_2/3 would also be matched. + In our example, as all the taxes are set to be affected by previous ones affecting the base, the + result is similar to the table 'account_move_line_account_tax_rel': + Id Tax_ids + ------------------------------------------- + base_line_1 [10_affect_base, 20] + base_line_2 [10_affect_base, 5] + base_line_3 [10_affect_base, 5] + */ + + SELECT + sub.line_id AS id, + ARRAY_AGG(sub.tax_id ORDER BY sub.sequence, sub.tax_id) AS tax_ids + FROM ( + SELECT + tax_rel.account_move_line_id AS line_id, + {group_taxes_query} AS tax_id, + tax.sequence + FROM {tables} + JOIN account_move_line_account_tax_rel tax_rel ON account_move_line.id = tax_rel.account_move_line_id + JOIN account_tax tax ON tax.id = tax_rel.account_tax_id + WHERE tax.is_base_affected + AND {where_clause} + ) AS sub + GROUP BY sub.line_id + ), + + base_tax_line_mapping AS ( + + /* + Create the mapping of each tax lines with their corresponding base lines. + + In the example, it will give the following values: + base_line_id tax_line_id base_amount + ------------------------------------------- + base_line_1 tax_line_1 1000 + base_line_1 tax_line_2 1000 + base_line_2 tax_line_3 2000 + base_line_2 tax_line_4 2000 + base_line_3 tax_line_3 3000 + base_line_3 tax_line_4 3000 + */ + + SELECT + account_move_line.id AS tax_line_id, + base_line.id AS base_line_id, + base_line.balance AS base_amount, + base_line.amount_currency AS base_amount_currency + + FROM {tables} + JOIN account_tax_repartition_line tax_rep ON + tax_rep.id = account_move_line.tax_repartition_line_id + JOIN account_tax tax ON + tax.id = account_move_line.tax_line_id + JOIN res_currency curr ON + curr.id = account_move_line.currency_id + JOIN res_currency comp_curr ON + comp_curr.id = account_move_line.company_currency_id + JOIN account_move_line_account_tax_rel tax_rel ON + tax_rel.account_tax_id = COALESCE(account_move_line.group_tax_id, account_move_line.tax_line_id) + JOIN account_move_line base_line ON + base_line.id = tax_rel.account_move_line_id + AND base_line.tax_repartition_line_id IS NULL + AND base_line.move_id = account_move_line.move_id + AND COALESCE(base_line.partner_id, 0) = COALESCE(account_move_line.partner_id, 0) + AND base_line.currency_id = account_move_line.currency_id + AND ( + COALESCE(tax_rep.account_id, base_line.account_id) = account_move_line.account_id + OR (tax.tax_exigibility = 'on_payment' AND tax.cash_basis_transition_account_id IS NOT NULL) + ) + AND ( + NOT tax.analytic + OR (base_line.analytic_account_id IS NULL AND account_move_line.analytic_account_id IS NULL) + OR base_line.analytic_account_id = account_move_line.analytic_account_id + ) + LEFT JOIN affecting_base_tax_ids tax_line_tax_ids ON tax_line_tax_ids.id = account_move_line.id + JOIN affecting_base_tax_ids base_line_tax_ids ON base_line_tax_ids.id = base_line.id + WHERE account_move_line.tax_repartition_line_id IS NOT NULL + AND {where_clause} + AND ( + -- keeping only the rows from affecting_base_tax_lines that end with the same taxes applied (see comment in affecting_base_tax_ids) + NOT tax.include_base_amount + OR tax_line_tax_ids.tax_ids IS NULL + OR base_line_tax_ids.tax_ids[ARRAY_LENGTH(base_line_tax_ids.tax_ids, 1) - COALESCE(ARRAY_LENGTH(tax_line_tax_ids.tax_ids, 1), 0):ARRAY_LENGTH(base_line_tax_ids.tax_ids, 1)] + = ARRAY[account_move_line.tax_line_id] || COALESCE(tax_line_tax_ids.tax_ids, ARRAY[]::INTEGER[]) + ) + ), + + + tax_amount_affecting_base_to_dispatch AS ( + + /* + Computes the total amount to dispatch in case of tax lines affecting the base of subsequent taxes. + Such tax lines are an additional base amount for others lines, that will be truly dispatch in next + CTE. + + In the example: + - tax_line_1 is an additional base of 100.0 from base_line_1 for tax_line_2. + - tax_line_3 is an additional base of 2/5 * 500.0 = 200.0 from base_line_2 for tax_line_4. + - tax_line_3 is an additional base of 3/5 * 500.0 = 300.0 from base_line_3 for tax_line_4. + + src_line_id base_line_id tax_line_id total_base_amount + ------------------------------------------------------------- + tax_line_1 base_line_1 tax_line_2 1000 + tax_line_3 base_line_2 tax_line_4 5000 + tax_line_3 base_line_3 tax_line_4 5000 + */ + + SELECT + tax_line.id AS tax_line_id, + base_line.id AS base_line_id, + account_move_line.id AS src_line_id, + + tax_line.company_id, + comp_curr.id AS company_currency_id, + comp_curr.decimal_places AS comp_curr_prec, + curr.id AS currency_id, + curr.decimal_places AS curr_prec, + + tax_line.tax_line_id AS tax_id, + + base_line.balance AS base_amount, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.balance < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE base_line.balance + END + ) OVER (PARTITION BY tax_line.id, account_move_line.id ORDER BY tax_line.tax_line_id, base_line.id) AS cumulated_base_amount, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.balance < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE base_line.balance + END + ) OVER (PARTITION BY tax_line.id, account_move_line.id) AS total_base_amount, + account_move_line.balance AS total_tax_amount, + + base_line.amount_currency AS base_amount_currency, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.amount_currency < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE base_line.amount_currency + END + ) OVER (PARTITION BY tax_line.id, account_move_line.id ORDER BY tax_line.tax_line_id, base_line.id) AS cumulated_base_amount_currency, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.amount_currency < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE base_line.amount_currency + END + ) OVER (PARTITION BY tax_line.id, account_move_line.id) AS total_base_amount_currency, + account_move_line.amount_currency AS total_tax_amount_currency + + FROM {tables} + JOIN account_tax tax_include_base_amount ON + tax_include_base_amount.include_base_amount + AND tax_include_base_amount.id = account_move_line.tax_line_id + JOIN base_tax_line_mapping base_tax_line_mapping ON + base_tax_line_mapping.tax_line_id = account_move_line.id + JOIN account_move_line_account_tax_rel tax_rel ON + tax_rel.account_move_line_id = base_tax_line_mapping.tax_line_id + JOIN account_tax tax ON + tax.id = tax_rel.account_tax_id + JOIN base_tax_line_mapping tax_line_matching ON + tax_line_matching.base_line_id = base_tax_line_mapping.base_line_id + JOIN account_move_line tax_line ON + tax_line.id = tax_line_matching.tax_line_id + AND tax_line.tax_line_id = tax_rel.account_tax_id + JOIN res_currency curr ON + curr.id = tax_line.currency_id + JOIN res_currency comp_curr ON + comp_curr.id = tax_line.company_currency_id + JOIN account_move_line base_line ON + base_line.id = base_tax_line_mapping.base_line_id + WHERE {where_clause} + ), + + + base_tax_matching_base_amounts AS ( + + /* + Build here the full mapping tax lines <=> base lines containing the final base amounts. + This is done in a 3-parts union. + + Note: src_line_id is used only to build a unique ID. + */ + + /* + PART 1: raw mapping computed in base_tax_line_mapping. + */ + + SELECT + tax_line_id, + base_line_id, + base_line_id AS src_line_id, + base_amount, + base_amount_currency + FROM base_tax_line_mapping + + UNION ALL + + /* + PART 2: Dispatch the tax amount of tax lines affecting the base of subsequent ones, using + tax_amount_affecting_base_to_dispatch. + + This will effectively add the following rows: + base_line_id tax_line_id src_line_id base_amount + ------------------------------------------------------------- + base_line_1 tax_line_2 tax_line_1 100 + base_line_2 tax_line_4 tax_line_3 200 + base_line_3 tax_line_4 tax_line_3 300 + */ + + SELECT + sub.tax_line_id, + sub.base_line_id, + sub.src_line_id, + + ROUND( + COALESCE(sub.total_tax_amount * ABS(sub.cumulated_base_amount) / ABS(NULLIF(sub.total_base_amount, 0.0)), 0.0), + sub.comp_curr_prec + ) + - LAG(ROUND( + COALESCE(sub.total_tax_amount * ABS(sub.cumulated_base_amount) / ABS(NULLIF(sub.total_base_amount, 0.0)), 0.0), + sub.comp_curr_prec + ), 1, 0.0) + OVER ( + PARTITION BY sub.tax_line_id, sub.src_line_id ORDER BY sub.tax_id, sub.base_line_id + ) AS base_amount, + + ROUND( + COALESCE(sub.total_tax_amount_currency * ABS(sub.cumulated_base_amount_currency) / ABS(NULLIF(sub.total_base_amount_currency, 0.0)), 0.0), + sub.curr_prec + ) + - LAG(ROUND( + COALESCE(sub.total_tax_amount_currency * ABS(sub.cumulated_base_amount_currency) / ABS(NULLIF(sub.total_base_amount_currency, 0.0)), 0.0), + sub.curr_prec + ), 1, 0.0) + OVER ( + PARTITION BY sub.tax_line_id, sub.src_line_id ORDER BY sub.tax_id, sub.base_line_id + ) AS base_amount_currency + FROM tax_amount_affecting_base_to_dispatch sub + JOIN account_move_line tax_line ON + tax_line.id = sub.tax_line_id + + /* + PART 3: In case of the matching failed because the configuration changed or some journal entries + have been imported, construct a simple mapping as a fallback. This mapping is super naive and only + build based on the 'tax_ids' and 'tax_line_id' fields, nothing else. Hence, the mapping will not be + exact but will give an acceptable approximation. + + Skipped if the 'fallback' method parameter is False. + */ + {fallback_query} + ), + + + base_tax_matching_all_amounts AS ( + + /* + Complete base_tax_matching_base_amounts with the tax amounts (prorata): + base_line_id tax_line_id src_line_id base_amount tax_amount + -------------------------------------------------------------------------- + base_line_1 tax_line_1 base_line_1 1000 100 + base_line_1 tax_line_2 base_line_1 1000 (1000 / 1100) * 220 = 200 + base_line_1 tax_line_2 tax_line_1 100 (100 / 1100) * 220 = 20 + base_line_2 tax_line_3 base_line_2 2000 (2000 / 5000) * 500 = 200 + base_line_2 tax_line_4 base_line_2 2000 (2000 / 5500) * 275 = 100 + base_line_2 tax_line_4 tax_line_3 200 (200 / 5500) * 275 = 10 + base_line_3 tax_line_3 base_line_3 3000 (3000 / 5000) * 500 = 300 + base_line_3 tax_line_4 base_line_3 3000 (3000 / 5500) * 275 = 150 + base_line_3 tax_line_4 tax_line_3 300 (300 / 5500) * 275 = 15 + */ + + SELECT + sub.tax_line_id, + sub.base_line_id, + sub.src_line_id, + + tax_line.tax_line_id AS tax_id, + tax_line.group_tax_id, + + tax_line.company_id, + comp_curr.id AS company_currency_id, + comp_curr.decimal_places AS comp_curr_prec, + curr.id AS currency_id, + curr.decimal_places AS curr_prec, + tax_line.tax_exigible, + base_line.account_id AS base_account_id, + + sub.base_amount, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.balance < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE sub.base_amount + END + ) OVER (PARTITION BY tax_line.id ORDER BY tax_line.tax_line_id, sub.base_line_id, sub.src_line_id) AS cumulated_base_amount, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.balance < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE sub.base_amount + END + ) OVER (PARTITION BY tax_line.id) AS total_base_amount, + tax_line.balance AS total_tax_amount, + + sub.base_amount_currency, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.amount_currency < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE sub.base_amount_currency + END + ) OVER (PARTITION BY tax_line.id ORDER BY tax_line.tax_line_id, sub.base_line_id, sub.src_line_id) AS cumulated_base_amount_currency, + SUM( + CASE WHEN tax.amount_type = 'fixed' + THEN CASE WHEN base_line.amount_currency < 0 THEN -1 ELSE 1 END * ABS(COALESCE(base_line.quantity, 1.0)) + ELSE sub.base_amount_currency + END + ) OVER (PARTITION BY tax_line.id) AS total_base_amount_currency, + tax_line.amount_currency AS total_tax_amount_currency + + FROM base_tax_matching_base_amounts sub + JOIN account_move_line tax_line ON + tax_line.id = sub.tax_line_id + JOIN account_move_line base_line ON + base_line.id = sub.base_line_id + JOIN account_tax tax ON + tax.id = tax_line.tax_line_id + JOIN res_currency curr ON + curr.id = tax_line.currency_id + JOIN res_currency comp_curr ON + comp_curr.id = tax_line.company_currency_id + + ) + + + /* Final select that makes sure to deal with rounding errors, using LAG to dispatch the last cents. */ + + SELECT + sub.tax_line_id || '-' || sub.base_line_id || '-' || sub.src_line_id AS id, + + sub.base_line_id, + sub.tax_line_id, + sub.src_line_id, + + sub.tax_id, + sub.group_tax_id, + sub.tax_exigible, + sub.base_account_id, + + sub.base_amount, + ROUND( + COALESCE(sub.total_tax_amount * ABS(sub.cumulated_base_amount) / ABS(NULLIF(sub.total_base_amount, 0.0)), 0.0), + sub.comp_curr_prec + ) + - LAG(ROUND( + COALESCE(sub.total_tax_amount * ABS(sub.cumulated_base_amount) / ABS(NULLIF(sub.total_base_amount, 0.0)), 0.0), + sub.comp_curr_prec + ), 1, 0.0) + OVER ( + PARTITION BY sub.tax_line_id ORDER BY sub.tax_id, sub.base_line_id + ) AS tax_amount, + + sub.base_amount_currency, + ROUND( + COALESCE(sub.total_tax_amount_currency * ABS(sub.cumulated_base_amount_currency) / ABS(NULLIF(sub.total_base_amount_currency, 0.0)), 0.0), + sub.curr_prec + ) + - LAG(ROUND( + COALESCE(sub.total_tax_amount_currency * ABS(sub.cumulated_base_amount_currency) / ABS(NULLIF(sub.total_base_amount_currency, 0.0)), 0.0), + sub.curr_prec + ), 1, 0.0) + OVER ( + PARTITION BY sub.tax_line_id ORDER BY sub.tax_id, sub.base_line_id + ) AS tax_amount_currency + FROM base_tax_matching_all_amounts sub + ''', group_taxes_params + where_params + where_params + where_params + fallback_params diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index ffd45bb1449..303a7993877 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -17,6 +17,7 @@ from . import test_account_bank_statement from . import test_account_move_partner_count from . import test_account_move_rounding from . import test_account_invoice_report +from . import test_account_move_line_tax_details from . import test_account_journal_dashboard from . import test_fiscal_position from . import test_reconciliation diff --git a/addons/account/tests/test_account_move_line_tax_details.py b/addons/account/tests/test_account_move_line_tax_details.py new file mode 100644 index 00000000000..fef3055960a --- /dev/null +++ b/addons/account/tests/test_account_move_line_tax_details.py @@ -0,0 +1,1086 @@ +# -*- coding: utf-8 -*- +#pylint: disable=too-many-lines +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import tagged +from odoo import Command + + +@tagged('post_install', '-at_install') +class TestAccountTaxDetailsReport(AccountTestInvoicingCommon): + + def _dispatch_move_lines(self, moves): + base_lines = moves.line_ids\ + .filtered(lambda x: x.tax_ids and not x.tax_line_id)\ + .sorted(lambda x: (x.move_id, x.id, -abs(x.amount_currency))) + tax_lines = moves.line_ids\ + .filtered(lambda x: x.tax_line_id)\ + .sorted(lambda x: (x.move_id, x.tax_line_id, x.tax_ids, x.tax_repartition_line_id)) + return base_lines, tax_lines + + def _get_tax_details(self, fallback=False): + domain = [('company_id', '=', self.env.company.id)] + tax_details_query, tax_details_params = self.env['account.move.line']._get_query_tax_details_from_domain(domain, fallback=fallback) + self.cr.execute(tax_details_query, tax_details_params) + tax_details_res = self.cr.dictfetchall() + return sorted(tax_details_res, key=lambda x: (x['base_line_id'], abs(x['base_amount']), abs(x['tax_amount']))) + + def assertTaxDetailsValues(self, tax_details, expected_values_list): + self.assertEqual(len(tax_details), len(expected_values_list)) + + for i, expected_values in enumerate(expected_values_list): + keys = set(expected_values.keys()) + tax_detail = tax_details[i] + self.assertDictEqual({k: v for k, v in tax_detail.items() if k in keys}, expected_values) + + def assertTotalAmounts(self, moves, tax_details): + tax_lines = moves.line_ids.filtered('tax_line_id') + taxes = tax_lines.mapped(lambda x: x.group_tax_id or x.tax_line_id) + for tax in taxes: + lines = tax_lines.filtered(lambda x: (x.group_tax_id or x.tax_line_id) == tax) + tax_amount = sum(lines.mapped('balance')) + tax_details_amount = sum(x['tax_amount'] + for x in tax_details + if (x['group_tax_id'] or x['tax_id']) == tax.id) + self.assertAlmostEqual(tax_amount, tax_details_amount) + + def test_affect_base_amount_1(self): + tax_20_affect = self.env['account.tax'].create({ + 'name': "tax_20_affect", + 'amount_type': 'percent', + 'amount': 20.0, + 'include_base_amount': True, + }) + tax_10 = self.env['account.tax'].create({ + 'name': "tax_10", + 'amount_type': 'percent', + 'amount': 10.0, + }) + tax_5 = self.env['account.tax'].create({ + 'name': "tax_5", + 'amount_type': 'percent', + 'amount': 5.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set((tax_20_affect + tax_10 + tax_5).ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(tax_10.ids)], + }), + Command.create({ + 'name': 'line3', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(tax_10.ids)], + }), + Command.create({ + 'name': 'line4', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 2000.0, + 'tax_ids': [Command.set((tax_20_affect + tax_10).ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues(tax_details, [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -200.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -200.0, + 'tax_amount': -20.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -1000.0, + 'tax_amount': -50.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -200.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -400.0, + 'tax_amount': -40.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -2000.0, + 'tax_amount': -200.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[3].id, + 'base_amount': -2000.0, + 'tax_amount': -400.0, + }, + ]) + self.assertTotalAmounts(invoice, tax_details) + + # Same with a group of taxes + + tax_group = self.env['account.tax'].create({ + 'name': "tax_group", + 'amount_type': 'group', + 'children_tax_ids': [Command.set((tax_20_affect + tax_10 + tax_5).ids)], + }) + + invoice.write({ + 'invoice_line_ids': [Command.update(base_lines[0].id, { + 'tax_ids': [Command.set(tax_group.ids)], + })], + }) + + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues(tax_details, [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[4].id, + 'base_amount': -200.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[3].id, + 'base_amount': -200.0, + 'tax_amount': -20.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[4].id, + 'base_amount': -1000.0, + 'tax_amount': -50.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[3].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -1000.0, + 'tax_amount': -200.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -400.0, + 'tax_amount': -40.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -2000.0, + 'tax_amount': -200.0, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -2000.0, + 'tax_amount': -400.0, + }, + ]) + self.assertTotalAmounts(invoice, tax_details) + + def test_affect_base_amount_2(self): + taxes_10_affect = self.env['account.tax'].create([{ + 'name': "tax_10_affect_%s" % i, + 'amount_type': 'percent', + 'amount': 10.0, + 'include_base_amount': True, + } for i in range(3)]) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(taxes_10_affect.ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set((taxes_10_affect[0] + taxes_10_affect[2]).ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -100.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -100.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -110.0, + 'tax_amount': -11.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -100.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[3].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_affect_base_amount_3(self): + eco_tax = self.env['account.tax'].create({ + 'name': "eco_tax", + 'amount_type': 'fixed', + 'amount': 5.0, + 'include_base_amount': True, + }) + tax_20 = self.env['account.tax'].create({ + 'name': "tax_20", + 'amount_type': 'percent', + 'amount': 20.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 95.0, + 'tax_ids': [Command.set((eco_tax + tax_20).ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -5.0, + 'tax_amount': -1.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -95.0, + 'tax_amount': -5.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -95.0, + 'tax_amount': -19.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_affect_base_amount_4(self): + tax_10 = self.env['account.tax'].create({ + 'name': "eco_tax", + 'amount_type': 'percent', + 'amount': 10.0, + 'include_base_amount': True, + }) + tax_20 = self.env['account.tax'].create({ + 'name': "tax_20", + 'amount_type': 'percent', + 'amount': 20.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'tax_ids': [Command.set((tax_10 + tax_20).ids)], + }), + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'tax_ids': [Command.set(tax_10.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -10.0, + 'tax_amount': -2.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -100.0, + 'tax_amount': -5.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -100.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -100.0, + 'tax_amount': -20.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[2].id, + 'base_amount': -100.0, + 'tax_amount': -5.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_round_globally_rounding(self): + self.env.company.tax_calculation_rounding_method = 'round_globally' + + tax_50 = self.env['account.tax'].create({ + 'name': "tax_50", + 'amount_type': 'percent', + 'amount': 50.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line%s' % i, + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 0.01, + 'tax_ids': [Command.set(tax_50.ids)], + }) + for i in range(7)] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': -0.01, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': 0.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': -0.01, + }, + { + 'base_line_id': base_lines[3].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': 0.00, + }, + { + 'base_line_id': base_lines[4].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': -0.01, + }, + { + 'base_line_id': base_lines[5].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': 0.0, + }, + { + 'base_line_id': base_lines[6].id, + 'tax_line_id': tax_lines.id, + 'base_amount': -0.01, + 'tax_amount': -0.01, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_partitioning_lines_by_moves(self): + tax_20_affect = self.env['account.tax'].create({ + 'name': "tax_20_affect", + 'amount_type': 'percent', + 'amount': 20.0, + 'include_base_amount': True, + }) + tax_10 = self.env['account.tax'].create({ + 'name': "tax_10", + 'amount_type': 'percent', + 'amount': 10.0, + }) + + invoices = self.env['account.move'] + expected_values_list = [] + for i in range(1, 6): + invoice = invoices.create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': i * 1000.0, + 'tax_ids': [Command.set((tax_20_affect + tax_10).ids)], + }), + ] + }) + invoices |= invoice + base_lines, tax_lines = self._dispatch_move_lines(invoice) + expected_values_list += [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -200.0 * i, + 'tax_amount': -20.0 * i, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0 * i, + 'tax_amount': -100.0 * i, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0 * i, + 'tax_amount': -200.0 * i, + }, + ] + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues(tax_details, expected_values_list) + self.assertTotalAmounts(invoices, tax_details) + + def test_fixed_tax_with_negative_quantity(self): + fixed_tax = self.env['account.tax'].create({ + 'name': "fixed_tax", + 'amount_type': 'fixed', + 'amount': 10.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'quantity': 5, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'quantity': 9, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + Command.create({ + 'name': 'line3', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'quantity': -4, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -500.0, + 'tax_amount': -50.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -900.0, + 'tax_amount': -90.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 400.0, + 'tax_amount': 40.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_percent_tax_with_negative_balance(self): + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 500.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 900.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + Command.create({ + 'name': 'line3', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': -400.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -500.0, + 'tax_amount': -50.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -900.0, + 'tax_amount': -90.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 400.0, + 'tax_amount': 40.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_fixed_tax_with_negative_balance(self): + fixed_tax = self.env['account.tax'].create({ + 'name': "fixed_tax", + 'amount_type': 'fixed', + 'amount': 10.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 500.0, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 900.0, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + Command.create({ + 'name': 'line3', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': -400.0, + 'tax_ids': [Command.set(fixed_tax.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -500.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -900.0, + 'tax_amount': -10.0, + }, + { + 'base_line_id': base_lines[2].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 400.0, + 'tax_amount': 10.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_multiple_same_tax_lines(self): + """ In expense, the same tax line could be generated multiple times. """ + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + }) + tax_rep = percent_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax') + + move = self.env['account.move'].create({ + 'date': '2019-01-01', + 'line_ids': [ + # Base lines + Command.create({ + 'name': 'base1', + 'debit': 1000.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + Command.create({ + 'name': 'base2', + 'debit': 10000.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + # Tax lines + Command.create({ + 'name': 'tax1', + 'debit': 100.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_repartition_line_id': tax_rep.id, + }), + Command.create({ + 'name': 'tax1', + 'debit': 1000.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_repartition_line_id': tax_rep.id, + }), + # Balance + Command.create({ + 'name': 'balance', + 'debit': 0.0, + 'credit': 12100.0, + 'account_id': self.company_data['default_account_receivable'].id, + }), + ], + }) + base_lines, tax_lines = self._dispatch_move_lines(move) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 1000.0, + 'tax_amount': 9.09, + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': 1000.0, + 'tax_amount': 90.91, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 10000.0, + 'tax_amount': 90.91, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': 10000.0, + 'tax_amount': 909.09, + }, + ], + ) + self.assertTotalAmounts(move, tax_details) + + def test_multiple_same_tax_lines_multi_currencies_manual_edition(self): + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + }) + tax_rep = percent_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax') + + move = self.env['account.move'].create({ + 'date': '2019-01-01', + 'line_ids': [ + # Base lines + Command.create({ + 'name': 'base1', + 'debit': 1200.0, + 'credit': 0.0, + 'amount_currency': 2400.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': self.currency_data['currency'].id, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + Command.create({ + 'name': 'base2', + 'debit': 12000.0, + 'credit': 0.0, + 'amount_currency': 6000.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': self.currency_data['currency'].id, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + # Tax lines + Command.create({ + 'name': 'tax1', + 'debit': 120.0, + 'credit': 0.0, + 'amount_currency': 360.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': self.currency_data['currency'].id, + 'tax_repartition_line_id': tax_rep.id, + }), + Command.create({ + 'name': 'tax1', + 'debit': 1200.0, + 'credit': 0.0, + 'amount_currency': 200.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': self.currency_data['currency'].id, + 'tax_repartition_line_id': tax_rep.id, + }), + # Balance + Command.create({ + 'name': 'balance', + 'debit': 0.0, + 'credit': 14520.0, + 'account_id': self.company_data['default_account_receivable'].id, + }), + ], + }) + base_lines, tax_lines = self._dispatch_move_lines(move) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 1200.0, + 'tax_amount': 10.91, + 'base_amount_currency': 2400.0, + 'tax_amount_currency': 102.857, # (2400.0 / 8400.0) * (360.0 / 560.0) * 560.0 + }, + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': 1200.0, + 'tax_amount': 109.09, + 'base_amount_currency': 2400.0, + 'tax_amount_currency': 57.143, # (2400.0 / 8400.0) * (200.0 / 560.0) * 560.0 + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': 12000.0, + 'tax_amount': 109.09, + 'base_amount_currency': 6000.0, + 'tax_amount_currency': 257.143, # (6000.0 / 8400.0) * (360.0 / 560.0) * 560.0 + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': 12000.0, + 'tax_amount': 1090.91, + 'base_amount_currency': 6000.0, + 'tax_amount_currency': 142.857, # (6000.0 / 8400.0) * (200.0 / 560.0) * 560.0 + }, + ], + ) + self.assertTotalAmounts(move, tax_details) + + def test_mixing_tax_inside_and_outside_a_group_of_taxes(self): + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + }) + tax_group = self.env['account.tax'].create({ + 'name': "tax_group", + 'amount_type': 'group', + 'children_tax_ids': [Command.set(percent_tax.ids)], + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + Command.create({ + 'name': 'line2', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(tax_group.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + { + 'base_line_id': base_lines[1].id, + 'tax_line_id': tax_lines[1].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_broken_configuration(self): + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + # Break the configuration + tax_lines.account_id = self.company_data['default_account_assets'] + + tax_details = self._get_tax_details(fallback=True) + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) + + def test_tax_on_payment(self): + percent_tax = self.env['account.tax'].create({ + 'name': "percent_tax", + 'amount_type': 'percent', + 'amount': 10.0, + 'tax_exigibility': 'on_payment', + 'cash_basis_transition_account_id': self.company_data['default_account_assets'].id, + }) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'invoice_date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line1', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(percent_tax.ids)], + }), + ] + }) + base_lines, tax_lines = self._dispatch_move_lines(invoice) + + tax_details = self._get_tax_details() + self.assertTaxDetailsValues( + tax_details, + [ + { + 'base_line_id': base_lines[0].id, + 'tax_line_id': tax_lines[0].id, + 'base_amount': -1000.0, + 'tax_amount': -100.0, + }, + ], + ) + self.assertTotalAmounts(invoice, tax_details) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index dcb85845425..f963070e31c 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=C0326 from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged, Form @@ -164,9 +165,10 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon): (100, self.percent_tax_2), ]) invoice.action_post() - self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: x.price_unit), [ - {'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'price_unit': 21, 'tax_ids': [self.percent_tax_2.id]}, - {'name': self.percent_tax_2.name, 'tax_base_amount': 221, 'price_unit': 26.52, 'tax_ids': []}, + self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted('balance'), [ + {'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100.0, 'balance': -21.0, 'tax_ids': [self.percent_tax_2.id]}, + {'name': self.percent_tax_2.name, 'tax_base_amount': 121.0, 'balance': -14.52, 'tax_ids': []}, + {'name': self.percent_tax_2.name, 'tax_base_amount': 100.0, 'balance': -12.0, 'tax_ids': []}, ]) def _create_tax_tag(self, tag_name): diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 4ef394717e0..25c7b7a924a 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -899,6 +899,7 @@ + @@ -975,6 +976,7 @@ + @@ -1126,6 +1128,7 @@ + @@ -1143,6 +1146,7 @@
+ diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py index 6bd63eb0a33..c868b32b2d8 100644 --- a/addons/account_edi/models/account_move.py +++ b/addons/account_edi/models/account_move.py @@ -135,7 +135,7 @@ class AccountMove(models.Model): #################################################### @api.model - def _add_edi_tax_values(self, results, grouping_key, serialized_grouping_key, tax_values): + def _add_edi_tax_values(self, results, grouping_key, serialized_grouping_key, tax_values, key_by_tax): # Add to global results. results['tax_amount'] += tax_values['tax_amount'] results['tax_amount_currency'] += tax_values['tax_amount_currency'] @@ -151,6 +151,10 @@ class AccountMove(models.Model): }) else: tax_details = results['tax_details'][serialized_grouping_key] + if key_by_tax[tax_values['tax_id']] != key_by_tax.get(tax_values['src_line_id'].tax_line_id): + tax_details['base_amount'] += tax_values['base_amount'] + tax_details['base_amount_currency'] += tax_values['base_amount_currency'] + tax_details['tax_amount'] += tax_values['tax_amount'] tax_details['tax_amount_currency'] += tax_values['tax_amount_currency'] tax_details['group_tax_details'].append(tax_values) @@ -223,31 +227,31 @@ class AccountMove(models.Model): return {'tax': tax_values['tax_id']} # Compute the taxes values for each invoice line. - invoice_lines = self.invoice_line_ids.filtered(lambda line: not line.display_type) - invoice_lines_tax_values_dict = {} - sign = -1 if self.is_inbound() else 1 - for invoice_line in invoice_lines: - taxes_res = invoice_line.tax_ids.compute_all( - invoice_line.price_unit * (1 - (invoice_line.discount / 100.0)), - currency=invoice_line.currency_id, - quantity=invoice_line.quantity, - product=invoice_line.product_id, - partner=invoice_line.partner_id, - is_refund=invoice_line.move_id.move_type in ('in_refund', 'out_refund'), - ) - tax_values_list = invoice_lines_tax_values_dict[invoice_line] = [] - rate = abs(invoice_line.balance) / abs(invoice_line.amount_currency) if invoice_line.amount_currency else 0.0 - for tax_res in taxes_res['taxes']: - tax_values_list.append({ - 'base_line_id': invoice_line, - 'tax_id': self.env['account.tax'].browse(tax_res['id']), - 'tax_repartition_line_id': self.env['account.tax.repartition.line'].browse(tax_res['tax_repartition_line_id']), - 'base_amount': sign * invoice_line.company_currency_id.round(tax_res['base'] / rate if rate else 0.0), - 'tax_amount': sign * invoice_line.company_currency_id.round(tax_res['amount'] / rate if rate else 0.0), - 'base_amount_currency': sign * tax_res['base'], - 'tax_amount_currency': sign * tax_res['amount'], - }) + invoice_lines_tax_values_dict = defaultdict(list) + + domain = [('move_id', '=', self.id)] + tax_details_query, tax_details_params = invoice_lines._get_query_tax_details_from_domain(domain) + self._cr.execute(tax_details_query, tax_details_params) + for row in self._cr.dictfetchall(): + invoice_line = invoice_lines.browse(row['base_line_id']) + tax_line = invoice_lines.browse(row['tax_line_id']) + src_line = invoice_lines.browse(row['src_line_id']) + + tax = self.env['account.tax'].browse(row['tax_id']) + + invoice_lines_tax_values_dict[invoice_line].append({ + 'base_line_id': invoice_line, + 'tax_line_id': tax_line, + 'src_line_id': src_line, + 'tax_id': tax, + 'src_tax_id': self.env['account.tax'].browse(row['group_tax_id']) if row['group_tax_id'] else tax, + 'tax_repartition_line_id': tax_line.tax_repartition_line_id, + 'base_amount': row['base_amount'], + 'tax_amount': row['tax_amount'], + 'base_amount_currency': row['base_amount_currency'], + 'tax_amount_currency': row['tax_amount_currency'], + }) grouping_key_generator = grouping_key_generator or default_grouping_key_generator # Apply 'filter_to_apply'. @@ -290,7 +294,9 @@ class AccountMove(models.Model): # Apply 'grouping_key_generator' to 'invoice_lines_tax_values_list' and add all values to the final results. for invoice_line in invoice_lines: - tax_values_list = invoice_lines_tax_values_dict[invoice_line] + tax_values_list = invoice_lines_tax_values_dict.get(invoice_line, []) + + key_by_tax = {} # Add to invoice global tax amounts. invoice_global_tax_details['base_amount'] += invoice_line.balance @@ -299,6 +305,7 @@ class AccountMove(models.Model): for tax_values in tax_values_list: grouping_key = grouping_key_generator(tax_values) serialized_grouping_key = _serialize_python_dictionary(grouping_key) + key_by_tax[tax_values['tax_id']] = serialized_grouping_key # Add to invoice line global tax amounts. if serialized_grouping_key not in invoice_global_tax_details['invoice_line_tax_details'][invoice_line]: @@ -310,8 +317,8 @@ class AccountMove(models.Model): else: invoice_line_global_tax_details = invoice_global_tax_details['invoice_line_tax_details'][invoice_line] - self._add_edi_tax_values(invoice_global_tax_details, grouping_key, serialized_grouping_key, tax_values) - self._add_edi_tax_values(invoice_line_global_tax_details, grouping_key, serialized_grouping_key, tax_values) + self._add_edi_tax_values(invoice_global_tax_details, grouping_key, serialized_grouping_key, tax_values, key_by_tax) + self._add_edi_tax_values(invoice_line_global_tax_details, grouping_key, serialized_grouping_key, tax_values, key_by_tax) return invoice_global_tax_details