# -*- coding: utf-8 -*-
# pylint: disable=bad-whitespace
from datetime import date
from unittest.mock import patch
from freezegun import freeze_time

from .common import TestAccountReportsCommon
from odoo import fields, Command
from odoo.tests import Form, tagged
from odoo.exceptions import UserError


@tagged('post_install', '-at_install')
class TestTaxReport(TestAccountReportsCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # Create country data

        cls.fiscal_country = cls.env['res.country'].create({
            'name': "Discworld",
            'code': 'DW',
        })

        cls.country_state_1 = cls.env['res.country.state'].create({
            'name': "Ankh Morpork",
            'code': "AM",
            'country_id': cls.fiscal_country.id,
        })

        cls.country_state_2 = cls.env['res.country.state'].create({
            'name': "Counterweight Continent",
            'code': "CC",
            'country_id': cls.fiscal_country.id,
        })

        cls.foreign_country = cls.env['res.country'].create({
            'name': "The Principality of Zeon",
            'code': 'PZ',
        })

        # Setup fiscal data
        cls.company_data['company'].write({
            'state_id': cls. country_state_1.id, # Not necessary at the moment; put there for consistency and robustness with possible future changes
            'account_tax_periodicity': 'trimester',
        })
        cls.change_company_country(cls.company_data['company'], cls.fiscal_country)

        # Prepare tax groups
        cls.tax_group_1 = cls._instantiate_basic_test_tax_group()
        cls.tax_group_2 = cls._instantiate_basic_test_tax_group()
        cls.tax_group_3 = cls._instantiate_basic_test_tax_group(country=cls.foreign_country)

        # Prepare tax accounts
        cls.tax_account_1 = cls.env['account.account'].create({
            'name': 'Tax Account',
            'code': '250000',
            'account_type': 'liability_current',
        })

        cls.tax_account_2 = cls.env['account.account'].create({
            'name': 'Tax Account',
            'code': '250001',
            'account_type': 'liability_current',
        })

        # ==== Sale taxes: group of two taxes having type_tax_use = 'sale' ====
        cls.sale_tax_percentage_incl_1 = cls.env['account.tax'].create({
            'name': 'sale_tax_percentage_incl_1',
            'amount': 20.0,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'price_include_override': 'tax_included',
            'tax_group_id': cls.tax_group_1.id,
        })

        cls.sale_tax_percentage_excl = cls.env['account.tax'].create({
            'name': 'sale_tax_percentage_excl',
            'amount': 10.0,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'tax_group_id': cls.tax_group_1.id,
        })

        cls.sale_tax_group = cls.env['account.tax'].create({
            'name': 'sale_tax_group',
            'amount_type': 'group',
            'type_tax_use': 'sale',
            'children_tax_ids': [Command.set((cls.sale_tax_percentage_incl_1 + cls.sale_tax_percentage_excl).ids)],
        })

        cls.move_sale = cls.env['account.move'].create({
            'move_type': 'entry',
            'date': '2016-01-01',
            'journal_id': cls.company_data['default_journal_sale'].id,
            'line_ids': [
                Command.create({
                    'debit': 1320.0,
                    'credit': 0.0,
                    'account_id': cls.company_data['default_account_receivable'].id,
                }),
                Command.create({
                    'debit': 0.0,
                    'credit': 120.0,
                    'account_id': cls.tax_account_1.id,
                    'tax_repartition_line_id': cls.sale_tax_percentage_excl.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
                }),
                Command.create({
                    'debit': 0.0,
                    'credit': 200.0,
                    'account_id': cls.tax_account_1.id,
                    'tax_repartition_line_id': cls.sale_tax_percentage_incl_1.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
                    'tax_ids': [Command.set(cls.sale_tax_percentage_excl.ids)]
                }),
                Command.create({
                    'debit': 0.0,
                    'credit': 1000.0,
                    'account_id': cls.company_data['default_account_revenue'].id,
                    'tax_ids': [Command.set(cls.sale_tax_group.ids)]
                }),
            ],
        })
        cls.move_sale.action_post()

        # ==== Purchase taxes: group of taxes having type_tax_use = 'none' ====

        cls.none_tax_percentage_incl_2 = cls.env['account.tax'].create({
            'name': 'none_tax_percentage_incl_2',
            'amount': 20.0,
            'amount_type': 'percent',
            'type_tax_use': 'none',
            'price_include_override': 'tax_included',
            'tax_group_id': cls.tax_group_2.id,
        })

        cls.none_tax_percentage_excl = cls.env['account.tax'].create({
            'name': 'none_tax_percentage_excl',
            'amount': 30.0,
            'amount_type': 'percent',
            'type_tax_use': 'none',
            'tax_group_id': cls.tax_group_2.id,
        })

        cls.purchase_tax_group = cls.env['account.tax'].create({
            'name': 'purchase_tax_group',
            'amount_type': 'group',
            'type_tax_use': 'purchase',
            'children_tax_ids': [Command.set((cls.none_tax_percentage_incl_2 + cls.none_tax_percentage_excl).ids)],
        })

        cls.move_purchase = cls.env['account.move'].create({
            'move_type': 'entry',
            'date': '2016-01-01',
            'journal_id': cls.company_data['default_journal_purchase'].id,
            'line_ids': [
                Command.create({
                    'debit': 0.0,
                    'credit': 3120.0,
                    'account_id': cls.company_data['default_account_payable'].id,
                }),
                Command.create({
                    'debit': 720.0,
                    'credit': 0.0,
                    'account_id': cls.tax_account_1.id,
                    'tax_repartition_line_id': cls.none_tax_percentage_excl.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
                }),
                Command.create({
                    'debit': 400.0,
                    'credit': 0.0,
                    'account_id': cls.tax_account_1.id,
                    'tax_repartition_line_id': cls.none_tax_percentage_incl_2.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
                    'tax_ids': [Command.set(cls.none_tax_percentage_excl.ids)]
                }),
                Command.create({
                    'debit': 2000.0,
                    'credit': 0.0,
                    'account_id': cls.company_data['default_account_expense'].id,
                    'tax_ids': [Command.set(cls.purchase_tax_group.ids)]
                }),
            ],
        })
        cls.move_purchase.action_post()

        #Instantiate test data for fiscal_position option of the tax report (both for checking the report and VAT closing)

        # Create a foreign partner
        cls.test_fpos_foreign_partner = cls.env['res.partner'].create({
            'name': "Mare Cel",
            'country_id': cls.fiscal_country.id,
            'state_id': cls.country_state_2.id,
        })

        # Create a tax report and some taxes for it
        cls.basic_tax_report = cls.env['account.report'].create({
            'name': "The Unseen Tax Report",
            'country_id': cls.fiscal_country.id,
            'root_report_id': cls.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance',})],
        })

        cls.test_fpos_tax_sale = cls._add_basic_tax_for_report(
            cls.basic_tax_report, 50, 'sale', cls.tax_group_1,
            [(30, cls.tax_account_1, False), (70, cls.tax_account_1, True), (-100, cls.tax_account_2, True)]
        )

        cls.test_fpos_tax_purchase = cls._add_basic_tax_for_report(
            cls.basic_tax_report, 50, 'purchase', cls.tax_group_2,
            [(40, cls.tax_account_1, False), (60, cls.tax_account_1, True), (-100, cls.tax_account_2, True)]
        )

        # Create a fiscal_position to automatically map the default tax for partner "Mare Cel" to our test tax
        cls.foreign_vat_fpos = cls.env['account.fiscal.position'].create({
            'name': "Test fpos",
            'auto_apply': True,
            'country_id': cls.fiscal_country.id,
            'state_ids': cls.country_state_2.ids,
            'foreign_vat': '12345',
        })

        # Create some domestic invoices (not all in the same closing period)
        cls.init_invoice('out_invoice', partner=cls.partner_a, invoice_date='2020-12-22', post=True, amounts=[28000], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('out_invoice', partner=cls.partner_a, invoice_date='2021-01-22', post=True, amounts=[200], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('out_refund', partner=cls.partner_a, invoice_date='2021-01-12', post=True, amounts=[20], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('in_invoice', partner=cls.partner_a, invoice_date='2021-03-12', post=True, amounts=[400], taxes=cls.test_fpos_tax_purchase)
        cls.init_invoice('in_refund', partner=cls.partner_a, invoice_date='2021-03-20', post=True, amounts=[60], taxes=cls.test_fpos_tax_purchase)
        cls.init_invoice('in_invoice', partner=cls.partner_a, invoice_date='2021-04-07', post=True, amounts=[42000], taxes=cls.test_fpos_tax_purchase)

        # Create some foreign invoices (not all in the same closing period)
        cls.init_invoice('out_invoice', partner=cls.test_fpos_foreign_partner, invoice_date='2020-12-13', post=True, amounts=[26000], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('out_invoice', partner=cls.test_fpos_foreign_partner, invoice_date='2021-01-16', post=True, amounts=[800], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('out_refund', partner=cls.test_fpos_foreign_partner, invoice_date='2021-01-30', post=True, amounts=[200], taxes=cls.test_fpos_tax_sale)
        cls.init_invoice('in_invoice', partner=cls.test_fpos_foreign_partner, invoice_date='2021-02-01', post=True, amounts=[1000], taxes=cls.test_fpos_tax_purchase)
        cls.init_invoice('in_refund', partner=cls.test_fpos_foreign_partner, invoice_date='2021-03-02', post=True, amounts=[600], taxes=cls.test_fpos_tax_purchase)
        cls.init_invoice('in_refund', partner=cls.test_fpos_foreign_partner, invoice_date='2021-05-02', post=True, amounts=[10000], taxes=cls.test_fpos_tax_purchase)

    @classmethod
    def _instantiate_basic_test_tax_group(cls, company=None, country=None):
        company = company or cls.env.company
        vals = {
            'name': 'Test tax group',
            'company_id': company.id,
            'tax_receivable_account_id': cls.company_data['default_tax_account_receivable'].sudo().copy({'company_ids': company.ids}).id,
            'tax_payable_account_id': cls.company_data['default_tax_account_payable'].sudo().copy({'company_ids': company.ids}).id,
        }
        if country:
            vals['country_id'] = country.id
        return cls.env['account.tax.group'].sudo().create(vals)

    @classmethod
    def _add_basic_tax_for_report(cls, tax_report, percentage, type_tax_use, tax_group, tax_repartition, company=None):
        """ Creates a basic test tax, as well as tax report lines and tags, connecting them all together.

        A tax report line will be created within tax report for each of the elements in tax_repartition,
        for both invoice and refund, so that the resulting repartition lines each reference their corresponding
        report line. Negative tags will be assign for refund lines; postive tags for invoice ones.

        :param tax_report: The report to create lines for.
        :param percentage: The created tax has amoun_type='percent'. This parameter contains its amount.
        :param type_tax_use: type_tax_use of the tax to create
        :param tax_repartition: List of tuples in the form [(factor_percent, account, use_in_tax_closing)], one tuple
                                for each tax repartition line to create (base lines will be automatically created).
        """
        tax = cls.env['account.tax'].create({
            'name': f"{type_tax_use} - {percentage} - {tax_report.name}",
            'amount': percentage,
            'amount_type': 'percent',
            'type_tax_use': type_tax_use,
            'tax_group_id': tax_group.id,
            'country_id': tax_report.country_id.id,
            'company_id': company.id if company else cls.env.company.id,
        })

        to_write = {}
        for move_type_suffix in ('invoice', 'refund'):
            sign = "-" if move_type_suffix == 'refund' else "+"
            report_line_sequence = tax_report.line_ids[-1].sequence + 1 if tax_report.line_ids else 0


            # Create a report line for the base
            base_report_line_name = f"{tax.id}-{move_type_suffix}-base"
            base_report_line = cls._create_tax_report_line(base_report_line_name, tax_report, tag_name=base_report_line_name, sequence=report_line_sequence)
            report_line_sequence += 1

            base_tag = base_report_line.expression_ids._get_matching_tags(sign)

            repartition_vals = [
                Command.clear(),
                Command.create({'repartition_type': 'base', 'tag_ids': base_tag.ids}),
            ]

            for (factor_percent, account, use_in_tax_closing) in tax_repartition:
                # Create a report line for the repartition line
                tax_report_line_name = f"{tax.id}-{move_type_suffix}-{factor_percent}"
                tax_report_line = cls._create_tax_report_line(tax_report_line_name, tax_report, tag_name=tax_report_line_name, sequence=report_line_sequence)
                report_line_sequence += 1

                tax_tag = tax_report_line.expression_ids._get_matching_tags(sign)

                repartition_vals.append(Command.create({
                    'account_id': account.id if account else None,
                    'factor_percent': factor_percent,
                    'use_in_tax_closing': use_in_tax_closing,
                    'tag_ids': tax_tag.ids,
                }))

            to_write[f"{move_type_suffix}_repartition_line_ids"] = repartition_vals

        tax.write(to_write)

        return tax

    def _assert_vat_closing(self, report, options, closing_vals_by_fpos):
        """ Checks the result of the VAT closing

        :param options: the tax report options to make the closing for
        :param closing_vals_by_fpos: A list of dict(fiscal_position: [dict(line_vals)], where fiscal_position is (possibly empty)
                                     account.fiscal.position record, and line_vals, the expected values for each closing move lines.
                                     In case the option 'companies' contains more than 1 company, a tuple (company, fiscal_position)
                                     replaces the fiscal_position key
        """
        with patch.object(type(self.env['account.move']), '_get_vat_report_attachments', autospec=True, side_effect=lambda *args, **kwargs: []):
            vat_closing_moves = self.env['account.generic.tax.report.handler']._generate_tax_closing_entries(report, options)

            if len(options['companies']) > 1:
                closing_moves_by_fpos = {(move.company_id, move.fiscal_position_id): move for move in vat_closing_moves}
            else:
                closing_moves_by_fpos = {move.fiscal_position_id: move for move in vat_closing_moves}

            for key, closing_vals in closing_vals_by_fpos.items():
                vat_closing_move = closing_moves_by_fpos[key]
                self.assertRecordValues(vat_closing_move.line_ids, closing_vals)
            self.assertEqual(len(closing_vals_by_fpos), len(vat_closing_moves), "Exactly one move should have been generated per fiscal position; nothing else.")

    def test_vat_closing_single_fpos(self):
        """ Tests the VAT closing when a foreign VAT fiscal position is selected on the tax report
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-15'), fields.Date.from_string('2021-02-01'),
            {'fiscal_position': self.foreign_vat_fpos.id}
        )

        self._assert_vat_closing(self.basic_tax_report, options, {
            self.foreign_vat_fpos: [
                # sales: 800 * 0.5 * 0.7 - 200 * 0.5 * 0.7
                {'debit': 210,      'credit': 0.0,      'account_id': self.tax_account_1.id},
                # sales: 800 * 0.5 * -1 - 200 * 0.5 * -1
                {'debit': 0,        'credit': 300,      'account_id': self.tax_account_2.id},
                # purchases: 1000 * 0.5 * 0.6 - 600 * 0.5 * 0.6
                {'debit': 0,        'credit': 120,      'account_id': self.tax_account_1.id},
                # purchases: 1000 * 0.5 * -1 - 600 * 0.5 * -1
                {'debit': 200,      'credit': 0,        'account_id': self.tax_account_2.id},
                # For sales operations
                {'debit': 90,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                # For purchase operations
                {'debit': 0,        'credit': 80,       'account_id': self.tax_group_2.tax_payable_account_id.id},
            ]
        })

    def test_vat_closing_moves_with_lock_date(self):
        """
        Check that we are still able to create a tax closing event if the lock date is set as the closing move
        is not dependent of the lock date.
        This also ensures that if we try to close again this period with a move already posted we get the same
        """
        self.env.company.tax_lock_date = fields.Date.from_string('2021-12-31')

        options = self._generate_options(
            self.basic_tax_report, '2021-01-15', '2021-02-01',
            {'fiscal_position': 'domestic'}
        )

        action = self.env['account.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        move = self.env['account.move'].browse(action['res_id'])
        with patch.object(self.registry['account.move'], '_get_vat_report_attachments', autospec=True, side_effect=lambda *args, **kwargs: []):
            move.action_post()

        action = self.env['account.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        same_move = self.env['account.move'].browse(action['res_id'])
        self.assertEqual(move.id, same_move.id)

    def test_vat_closing_domestic(self):
        """ Tests the VAT closing when a foreign VAT fiscal position is selected on the tax report
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-15'), fields.Date.from_string('2021-02-01'),
            {'fiscal_position': 'domestic'}
        )

        self._assert_vat_closing(self.basic_tax_report, options, {
            self.env['account.fiscal.position']: [
                # sales: 200 * 0.5 * 0.7 - 20 * 0.5 * 0.7
                {'debit': 63,       'credit': 0.0,      'account_id': self.tax_account_1.id},
                # sales: 200 * 0.5 * -1 - 20 * 0.5 * -1
                {'debit': 0,        'credit': 90,       'account_id': self.tax_account_2.id},
                # purchases: 400 * 0.5 * 0.6 - 60 * 0.5 * 0.6
                {'debit': 0,        'credit': 102,      'account_id': self.tax_account_1.id},
                # purchases: 400 * 0.5 * - 60 * 0.5
                {'debit': 170,      'credit': 0,        'account_id': self.tax_account_2.id},
                # For sales operations
                {'debit': 27,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                # For purchase operations
                {'debit': 0,        'credit': 68,       'account_id': self.tax_group_2.tax_payable_account_id.id},
            ]
        })

    def test_vat_closing_everything(self):
        """ Tests the VAT closing when the option to show all foreign VAT fiscal positions is activated.
        One closing move should then be generated per fiscal position.
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-15'), fields.Date.from_string('2021-02-01'),
            {'fiscal_position': 'all'}
        )

        self._assert_vat_closing(self.basic_tax_report, options, {
            # From test_vat_closing_domestic
            self.env['account.fiscal.position']: [
                # sales: 200 * 0.5 * 0.7 - 20 * 0.5 * 0.7
                {'debit': 63,       'credit': 0.0,      'account_id': self.tax_account_1.id},
                # sales: 200 * 0.5 * -1 - 20 * 0.5 * -1
                {'debit': 0,        'credit': 90,       'account_id': self.tax_account_2.id},
                # purchases: 400 * 0.5 * 0.6 - 60 * 0.5 * 0.6
                {'debit': 0,        'credit': 102,      'account_id': self.tax_account_1.id},
                # purchases: 400 * 0.5 * -1 - 60 * 0.5 * -1
                {'debit': 170,      'credit': 0,        'account_id': self.tax_account_2.id},
                # For sales operations
                {'debit': 27,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                # For purchase operations
                {'debit': 0,        'credit': 68,       'account_id': self.tax_group_2.tax_payable_account_id.id},
            ],

            # From test_vat_closing_single_fpos
            self.foreign_vat_fpos: [
                # sales: 800 * 0.5 * 0.7 - 200 * 0.5 * 0.7
                {'debit': 210,      'credit': 0.0,      'account_id': self.tax_account_1.id},
                # sales: 800 * 0.5 * -1 - 200 * 0.5 * -1
                {'debit': 0,        'credit': 300,      'account_id': self.tax_account_2.id},
                # purchases: 1000 * 0.5 * 0.6 - 600 * 0.5 * 0.6
                {'debit': 0,        'credit': 120,      'account_id': self.tax_account_1.id},
                # purchases: 1000 * 0.5 * -1 - 600 * 0.5 * -1
                {'debit': 200,      'credit': 0,        'account_id': self.tax_account_2.id},
                # For sales operations
                {'debit': 90,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                # For purchase operations
                {'debit': 0,        'credit': 80,       'account_id': self.tax_group_2.tax_payable_account_id.id},
            ],
        })

    def test_vat_closing_generic(self):
        """ VAT closing for the generic report should create one closing move per fiscal position + a domestic one.
        One closing move should then be generated per fiscal position.
        """
        for generic_report_xml_id in ('account.generic_tax_report', 'account.generic_tax_report_account_tax', 'account.generic_tax_report_tax_account'):
            generic_report = self.env.ref(generic_report_xml_id)
            options = self._generate_options(generic_report, fields.Date.from_string('2021-01-15'), fields.Date.from_string('2021-02-01'))

            self._assert_vat_closing(generic_report, options, {
                # From test_vat_closing_domestic
                self.env['account.fiscal.position']: [
                    # sales: 200 * 0.5 * 0.7 - 20 * 0.5 * 0.7
                    {'debit': 63,       'credit': 0.0,      'account_id': self.tax_account_1.id},
                    # sales: 200 * 0.5 * -1 - 20 * 0.5 * -1
                    {'debit': 0,        'credit': 90,       'account_id': self.tax_account_2.id},
                    # purchases: 400 * 0.5 * 0.6 - 60 * 0.5 * 0.6
                    {'debit': 0,        'credit': 102,      'account_id': self.tax_account_1.id},
                    # purchases: 400 * 0.5 * -1 - 60 * 0.5 * -1
                    {'debit': 170,      'credit': 0,        'account_id': self.tax_account_2.id},
                    # For sales operations
                    {'debit': 27,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                    # For purchase operations
                    {'debit': 0,        'credit': 68,       'account_id': self.tax_group_2.tax_payable_account_id.id},
                ],

                # From test_vat_closing_single_fpos
                self.foreign_vat_fpos: [
                    # sales: 800 * 0.5 * 0.7 - 200 * 0.5 * 0.7
                    {'debit': 210,      'credit': 0.0,      'account_id': self.tax_account_1.id},
                    # sales: 800 * 0.5 * -1 - 200 * 0.5 * -1
                    {'debit': 0,        'credit': 300,      'account_id': self.tax_account_2.id},
                    # purchases: 1000 * 0.5 * 0.6 - 600 * 0.5 * 0.6
                    {'debit': 0,        'credit': 120,      'account_id': self.tax_account_1.id},
                    # purchases: 1000 * 0.5 * -1 - 600 * 0.5 * -1
                    {'debit': 200,      'credit': 0,        'account_id': self.tax_account_2.id},
                    # For sales operations
                    {'debit': 90,       'credit': 0,        'account_id': self.tax_group_1.tax_receivable_account_id.id},
                    # For purchase operations
                    {'debit': 0,        'credit': 80,       'account_id': self.tax_group_2.tax_payable_account_id.id},
                ],
            })

    def test_vat_closing_button_availability(self):
        def assertTaxClosingAvailable(is_enabled, active_companies, export_main_company=None):
            options = tax_report.with_context(allowed_company_ids=active_companies.ids).get_options({})
            closing_button_dict = next(filter(lambda x: x['action'] == 'action_periodic_vat_entries', options['buttons']))
            self.assertEqual(closing_button_dict.get('error_action'), None if is_enabled else 'show_error_branch_allowed')
            if is_enabled:
                self.assertEqual(tax_report._get_sender_company_for_export(options), export_main_company)

        tax_report = self.env.ref('account.generic_tax_report')

        main_company = self.company_data['company']
        main_company.vat = '123'
        branch_1 = self.env['res.company'].create({'name': "Branch 1", 'parent_id': main_company.id})
        branch_1_1 = self.env['res.company'].create({'name': "Branch 1 sub-branch 1", 'parent_id': branch_1.id})
        branch_2 = self.env['res.company'].create({'name': "Branch 2", 'parent_id': main_company.id, 'vat': '456'})
        branch_2_1 = self.env['res.company'].create({'name': "Branch 2 sub-branch 1", 'parent_id': branch_2.id})

        assertTaxClosingAvailable(False, main_company)
        assertTaxClosingAvailable(True, main_company + branch_1 + branch_1_1 + branch_2 + branch_2_1, export_main_company=main_company)
        assertTaxClosingAvailable(True, branch_2 + branch_2_1 + main_company + branch_1 + branch_1_1, export_main_company=branch_2)
        assertTaxClosingAvailable(True, main_company + branch_1 + branch_1_1, export_main_company=main_company)
        assertTaxClosingAvailable(False, main_company + branch_1)
        assertTaxClosingAvailable(False, branch_1 + branch_1_1)
        assertTaxClosingAvailable(True, branch_1 + main_company + branch_1_1, export_main_company=main_company)
        assertTaxClosingAvailable(True, branch_2 + main_company + branch_2_1, export_main_company=branch_2)
        assertTaxClosingAvailable(False, branch_2_1)

    def test_tax_report_fpos_domestic(self):
        """ Test tax report's content for 'domestic' foreign VAT fiscal position option.
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-01'), fields.Date.from_string('2021-03-31'),
            {'fiscal_position': 'domestic'}
        )
        self.assertLinesValues(
            self.basic_tax_report._get_lines(options),
            #   Name                                                          Balance
            [0,                                                               1],
            [
                # out_invoice
                (f'{self.test_fpos_tax_sale.id}-invoice-base',             200),
                (f'{self.test_fpos_tax_sale.id}-invoice-30',                30),
                (f'{self.test_fpos_tax_sale.id}-invoice-70',                70),
                (f'{self.test_fpos_tax_sale.id}-invoice--100',            -100),

                # out_refund
                (f'{self.test_fpos_tax_sale.id}-refund-base',              -20),
                (f'{self.test_fpos_tax_sale.id}-refund-30',                 -3),
                (f'{self.test_fpos_tax_sale.id}-refund-70',                 -7),
                (f'{self.test_fpos_tax_sale.id}-refund--100',               10),

                # in_invoice
                (f'{self.test_fpos_tax_purchase.id}-invoice-base',         400),
                (f'{self.test_fpos_tax_purchase.id}-invoice-40',            80),
                (f'{self.test_fpos_tax_purchase.id}-invoice-60',           120),
                (f'{self.test_fpos_tax_purchase.id}-invoice--100',        -200),

                # in_refund
                (f'{self.test_fpos_tax_purchase.id}-refund-base',          -60),
                (f'{self.test_fpos_tax_purchase.id}-refund-40',            -12),
                (f'{self.test_fpos_tax_purchase.id}-refund-60',            -18),
                (f'{self.test_fpos_tax_purchase.id}-refund--100',           30),
            ],
            options,
        )

    def test_tax_report_fpos_foreign(self):
        """ Test tax report's content with a foreign VAT fiscal position.
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-01'), fields.Date.from_string('2021-03-31'),
            {'fiscal_position': self.foreign_vat_fpos.id}
        )
        self.assertLinesValues(
            self.basic_tax_report._get_lines(options),
            #   Name                                                          Balance
            [0,                                                               1],
            [
                # out_invoice
                (f'{self.test_fpos_tax_sale.id}-invoice-base',              800),
                (f'{self.test_fpos_tax_sale.id}-invoice-30',                120),
                (f'{self.test_fpos_tax_sale.id}-invoice-70',                280),
                (f'{self.test_fpos_tax_sale.id}-invoice--100',             -400),

                # out_refund
                (f'{self.test_fpos_tax_sale.id}-refund-base',              -200),
                (f'{self.test_fpos_tax_sale.id}-refund-30',                 -30),
                (f'{self.test_fpos_tax_sale.id}-refund-70',                 -70),
                (f'{self.test_fpos_tax_sale.id}-refund--100',               100),

                # in_invoice
                (f'{self.test_fpos_tax_purchase.id}-invoice-base',         1000),
                (f'{self.test_fpos_tax_purchase.id}-invoice-40',            200),
                (f'{self.test_fpos_tax_purchase.id}-invoice-60',            300),
                (f'{self.test_fpos_tax_purchase.id}-invoice--100',         -500),

                # in_refund
                (f'{self.test_fpos_tax_purchase.id}-refund-base',          -600),
                (f'{self.test_fpos_tax_purchase.id}-refund-40',            -120),
                (f'{self.test_fpos_tax_purchase.id}-refund-60',            -180),
                (f'{self.test_fpos_tax_purchase.id}-refund--100',           300),
            ],
            options,
        )

    def test_tax_report_fpos_everything(self):
        """ Test tax report's content for 'all' foreign VAT fiscal position option.
        """
        options = self._generate_options(
            self.basic_tax_report, fields.Date.from_string('2021-01-01'), fields.Date.from_string('2021-03-31'),
            {'fiscal_position': 'all'}
        )
        self.assertLinesValues(
            self.basic_tax_report._get_lines(options),
            #   Name                                                          Balance
            [0,                                                               1],
            [
                # out_invoice
                (f'{self.test_fpos_tax_sale.id}-invoice-base',            1000),
                (f'{self.test_fpos_tax_sale.id}-invoice-30',               150),
                (f'{self.test_fpos_tax_sale.id}-invoice-70',               350),
                (f'{self.test_fpos_tax_sale.id}-invoice--100',            -500),

                # out_refund
                (f'{self.test_fpos_tax_sale.id}-refund-base',             -220),
                (f'{self.test_fpos_tax_sale.id}-refund-30',                -33),
                (f'{self.test_fpos_tax_sale.id}-refund-70',                -77),
                (f'{self.test_fpos_tax_sale.id}-refund--100',              110),

                # in_invoice
                (f'{self.test_fpos_tax_purchase.id}-invoice-base',        1400),
                (f'{self.test_fpos_tax_purchase.id}-invoice-40',           280),
                (f'{self.test_fpos_tax_purchase.id}-invoice-60',           420),
                (f'{self.test_fpos_tax_purchase.id}-invoice--100',        -700),

                # in_refund
                (f'{self.test_fpos_tax_purchase.id}-refund-base',         -660),
                (f'{self.test_fpos_tax_purchase.id}-refund-40',           -132),
                (f'{self.test_fpos_tax_purchase.id}-refund-60',           -198),
                (f'{self.test_fpos_tax_purchase.id}-refund--100',          330),
            ],
            options,
        )

    def test_tax_report_single_fpos(self):
        """ When opening the tax report from a foreign country for which there exists only one
        foreing VAT fiscal position, this fiscal position should be selected by default in the
        report's options.
        """
        new_tax_report = self.env['account.report'].create({
            'name': "",
            'country_id': self.foreign_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})]
        })
        foreign_vat_fpos = self.env['account.fiscal.position'].create({
            'name': "Test fpos",
            'country_id': self.foreign_country.id,
            'foreign_vat': '422211',
        })
        options = self._generate_options(new_tax_report, fields.Date.from_string('2021-01-01'), fields.Date.from_string('2021-03-31'))
        self.assertEqual(options['fiscal_position'], foreign_vat_fpos.id, "When only one VAT fiscal position is available for a non-domestic country, it should be chosen by default")

    def test_tax_report_grid(self):
        company = self.company_data['company']

        # We generate a tax report with the following layout
        #/Base
        #   - Base 42%
        #   - Base 11%
        #/Tax
        #   - Tax 42%
        #       - 10.5%
        #       - 31.5%
        #   - Tax 11%
        #/Tax difference (42% - 11%)

        tax_report = self.env['account.report'].create({
            'name': 'Test',
            'country_id': company.account_fiscal_country_id.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})]
        })

        # We create the lines in a different order from the one they have in report,
        # so that we ensure sequence is taken into account properly when rendering the report
        tax_section = self._create_tax_report_line('Tax', tax_report, sequence=4, formula="tax_42.balance + tax_11.balance + tax_neg_100.balance")
        base_section = self._create_tax_report_line('Base', tax_report, sequence=1, formula="base_11.balance + base_42.balance")
        base_42_line = self._create_tax_report_line('Base 42%', tax_report, sequence=2, parent_line=base_section, code='base_42', tag_name='base_42')
        base_11_line = self._create_tax_report_line('Base 11%', tax_report, sequence=3, parent_line=base_section, code='base_11', tag_name='base_11')
        tax_42_section = self._create_tax_report_line('Tax 42%', tax_report, sequence=5, parent_line=tax_section, code='tax_42', formula='tax_31_5.balance + tax_10_5.balance')
        tax_31_5_line = self._create_tax_report_line('Tax 31.5%', tax_report, sequence=7, parent_line=tax_42_section, code='tax_31_5', tag_name='tax_31_5')
        tax_10_5_line = self._create_tax_report_line('Tax 10.5%', tax_report, sequence=6, parent_line=tax_42_section, code='tax_10_5', tag_name='tax_10_5')
        tax_11_line = self._create_tax_report_line('Tax 11%', tax_report, sequence=8, parent_line=tax_section, code='tax_11', tag_name='tax_11')
        tax_neg_100_line = self._create_tax_report_line('Tax -100%', tax_report, sequence=9, parent_line=tax_section, code='tax_neg_100', tag_name='tax_neg_100')
        self._create_tax_report_line('Tax difference (42%-11%)', tax_report, sequence=10, formula='tax_42.balance - tax_11.balance')

        # Create two taxes linked to report lines
        tax_11 = self.env['account.tax'].create({
            'name': 'Impôt sur les revenus',
            'amount': 11,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'invoice_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': self._get_tag_ids("+", base_11_line.expression_ids),
                }),
                Command.create({
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("+", tax_11_line.expression_ids),
                }),
            ],
            'refund_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': self._get_tag_ids("-", base_11_line.expression_ids),
                }),
                Command.create({
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("-", tax_11_line.expression_ids),
                }),
            ],
        })

        tax_42 = self.env['account.tax'].create({
            'name': 'Impôt sur les revenants',
            'amount': 42,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'invoice_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': self._get_tag_ids("+", base_42_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': 25,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("+", tax_10_5_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': 75,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("+", tax_31_5_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': -100,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("-", tax_neg_100_line.expression_ids),
                }),
            ],
            'refund_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': self._get_tag_ids("-", base_42_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': 25,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("-", tax_10_5_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': 75,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("-", tax_31_5_line.expression_ids),
                }),

                Command.create({
                    'factor_percent': -100,
                    'repartition_type': 'tax',
                    'tag_ids': self._get_tag_ids("+", tax_neg_100_line.expression_ids),
                }),
            ],
        })

        # Create an invoice using the tax we just made
        invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_a.id,
            'invoice_line_ids': [Command.create({
                'name': 'Turlututu',
                'price_unit': 100.0,
                'quantity': 1,
                'account_id': self.company_data['default_account_revenue'].id,
                'tax_ids': [Command.set((tax_11 + tax_42).ids)],
            })],
        })
        invoice.action_post()

        # Generate the report and check the results
        report = tax_report
        options = self._generate_options(report, invoice.date, invoice.date)

        # Invalidate the cache to ensure the lines will be fetched in the right order.
        self.env.invalidate_all()

        lines = report._get_lines(options)
        self.assertLinesValues(
            lines,
            #   Name                                        Balance
            [   0,                                             1  ],
            [
                ('Base',                                    200),
                ('Base 42%',                                100),
                ('Base 11%',                                100),
                ('Total Base',                              200),

                ('Tax',                                      95),
                ('Tax 42%',                                  42),
                ('Tax 10.5%',                                10.5),
                ('Tax 31.5%',                                31.5),
                ('Total Tax 42%',                            42),

                ('Tax 11%',                                  11),
                ('Tax -100%',                                42),
                ('Total Tax',                                95),

                ('Tax difference (42%-11%)',                 31),
            ],
            options,
        )

        # We refund the invoice
        refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
            'reason': 'Test refund tax repartition',
            'journal_id': invoice.journal_id.id,
            'date': invoice.date,
        })
        refund_wizard.modify_moves()

        # We check the taxes on refund have impacted the report properly (everything should be 0)
        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                         Balance
            [   0,                                               1],
            [
                ('Base',                                       0.0),
                ('Base 42%',                                   0.0),
                ('Base 11%',                                   0.0),
                ('Total Base',                                 0.0),

                ('Tax',                                        0.0),
                ('Tax 42%',                                    0.0),
                ('Tax 10.5%',                                  0.0),
                ('Tax 31.5%',                                  0.0),
                ('Total Tax 42%',                              0.0),

                ('Tax 11%',                                    0.0),
                ('Tax -100%',                                  0.0),
                ('Total Tax',                                  0.0),

                ('Tax difference (42%-11%)',                   0.0),
            ],
            options,
        )

    def _create_caba_taxes_for_report_lines(self, report_lines_dict, company):
        """ Creates cash basis taxes with a specific test repartition and maps them to
        the provided tax_report lines.

        :param report_lines_dict:  A dictionnary mapping tax_type_use values to
                                   tax report lines records
        :param company:            The company to create the test tags for

        :return:                   The created account.tax objects
        """
        return self.env['account.tax'].create([
            {
                'name': 'Impôt sur tout ce qui bouge',
                'amount': '20',
                'amount_type': 'percent',
                'type_tax_use': tax_type,
                'tax_exigibility': 'on_payment',
                'invoice_repartition_line_ids': [
                    Command.create({
                        'repartition_type': 'base',
                        'tag_ids': self._get_tag_ids("+", report_line.expression_ids),
                    }),
                    Command.create({
                        'factor_percent': 25,
                        'repartition_type': 'tax',
                        'tag_ids': self._get_tag_ids("+", report_line.expression_ids),
                    }),
                    Command.create({
                        'factor_percent': 75,
                        'repartition_type': 'tax',
                        'tag_ids': self._get_tag_ids("+", report_line.expression_ids),
                    }),
                ],
                'refund_repartition_line_ids': [
                    Command.create({
                        'repartition_type': 'base',
                        'tag_ids': self._get_tag_ids("-", report_line.expression_ids),
                    }),
                    Command.create({
                        'factor_percent': 25,
                        'repartition_type': 'tax',
                        'tag_ids': self._get_tag_ids("-", report_line.expression_ids),
                    }),
                    Command.create({
                        'factor_percent': 75,
                        'repartition_type': 'tax',
                    }),
                ],
            }
            for tax_type, report_line in report_lines_dict.items()
        ])

    def _create_taxes_for_report_lines(self, report_lines_dict, company):
        return self.env['account.tax'].create([
            {
                'name': 'Impôt sur tout ce qui bouge',
                'amount': '20',
                'amount_type': 'percent',
                'type_tax_use': tax_type,
                'invoice_repartition_line_ids': [
                    Command.create({
                        'repartition_type': 'base',
                        'tag_ids': self._get_tag_ids("+", report_line[0].expression_ids),
                    }),
                    Command.create({
                        'repartition_type': 'tax',
                        'tag_ids': self._get_tag_ids("+", report_line[1].expression_ids),
                    }),
                ],
                'refund_repartition_line_ids': [
                    Command.create({
                        'repartition_type': 'base',
                        'tag_ids': self._get_tag_ids("+", report_line[0].expression_ids),
                    }),
                    Command.create({
                        'repartition_type': 'tax',
                        'tag_ids': self._get_tag_ids("+", report_line[1].expression_ids),
                    }),
                ],
            }
            for tax_type, report_line in report_lines_dict.items()
        ])


    def _run_caba_generic_test(self, expected_columns, expected_lines, on_invoice_created=None, on_all_invoices_created=None, invoice_generator=None):
        """ Generic test function called by several cash basis tests.

        This function creates a new sale and purchase tax, each associated with
        a new tax report line using _create_caba_taxes_for_report_lines.
        It then creates an invoice AND a refund for each of these tax, and finally
        compare the tax report to the expected values, passed in parameters.

        Since _create_caba_taxes_for_report_lines creates asymmetric taxes (their 75%
        repartition line does not impact the report line at refund), we can be sure this
        function helper gives a complete coverage, and does not shadow any result due, for
        example, to some undesired swapping between debit and credit.

        :param expected_columns:          The columns we want the final tax report to contain

        :param expected_lines:            The lines we want the final tax report to contain

        :param on_invoice_created:        A function to be called when a single invoice has
                                          just been created, taking the invoice as a parameter
                                          (This can be used to reconcile the invoice with something, for example)

        :param on_all_invoices_created:   A function to be called when all the invoices corresponding
                                          to a tax type have been created, taking the
                                          recordset of all these invoices as a parameter
                                          (Use it to reconcile invoice and credit note together, for example)

        :param invoice_generator:         A function used to generate an invoice. A default
                                          one is called if none is provided, creating
                                          an invoice with a single line amounting to 100,
                                          with the provided tax set on it.
        """
        def default_invoice_generator(inv_type, partner, account, date, tax):
            return self.env['account.move'].create({
                'move_type': inv_type,
                'partner_id': partner.id,
                'invoice_date': date,
                'invoice_line_ids': [Command.create({
                    'name': 'test',
                    'quantity': 1,
                    'account_id': account.id,
                    'price_unit': 100,
                    'tax_ids': [Command.set(tax.ids)],
                })],
            })

        today = fields.Date.today()

        company = self.company_data['company']
        company.tax_exigibility = True
        partner = self.env['res.partner'].create({'name': 'Char Aznable'})

        # Create a tax report
        tax_report = self.env['account.report'].create({
            'name': 'Test',
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})]
        })

        # We create some report lines
        report_lines_dict = {
            'sale': self._create_tax_report_line('Sale', tax_report, sequence=1, tag_name='sale'),
            'purchase': self._create_tax_report_line('Purchase', tax_report, sequence=2, tag_name='purchase'),
        }

        # We create a sale and a purchase tax, linked to our report lines' tags
        taxes = self._create_caba_taxes_for_report_lines(report_lines_dict, company)


        # Create invoice and refund using the tax we just made
        invoice_types = {
            'sale': ('out_invoice', 'out_refund'),
            'purchase': ('in_invoice', 'in_refund')
        }

        account_types = {
            'sale': 'income',
            'purchase': 'expense',
        }
        for tax in taxes:
            invoices = self.env['account.move']
            account = self.env['account.account'].search([('company_ids', '=', company.id), ('account_type', '=', account_types[tax.type_tax_use])], limit=1)
            for inv_type in invoice_types[tax.type_tax_use]:
                invoice = (invoice_generator or default_invoice_generator)(inv_type, partner, account, today, tax)
                invoice.action_post()
                invoices += invoice

                if on_invoice_created:
                    on_invoice_created(invoice)

            if on_all_invoices_created:
                on_all_invoices_created(invoices)

        # Generate the report and check the results
        # We check the taxes on invoice have impacted the report properly
        options = self._generate_options(tax_report, date_from=today, date_to=today)
        inv_report_lines = tax_report._get_lines(options)
        self.assertLinesValues(inv_report_lines, expected_columns, expected_lines, options)

    def _register_full_payment_for_invoice(self, invoice):
        """ Fully pay the invoice, so that the cash basis entries are created
        """
        self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
            'payment_date': invoice.date,
        })._create_payments()

    @freeze_time('2023-10-05 02:00:00')
    def test_tax_report_grid_cash_basis(self):
        """ Cash basis moves create for taxes based on payments are handled differently
        by the report; we want to ensure their sign is managed properly.
        """
        # 100 (base, invoice) - 100 (base, refund) + 20 (tax, invoice) - 5 (25% tax, refund) = 15
        self._run_caba_generic_test(
            #   Name                      Balance
            [   0,                            1],
            [
                ('Sale',                     15),
                ('Purchase',                 15),
            ],
            on_invoice_created=self._register_full_payment_for_invoice
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_tax_report_grid_cash_basis_refund(self):
        """ Cash basis moves create for taxes based on payments are handled differently
        by the report; we want to ensure their sign is managed properly. This
        test runs the case where an invoice is reconciled with a refund (created
        separetely, so not cancelling it).
        """
        def reconcile_opposite_types(invoices):
            """ Reconciles the created invoices with their matching refund.
            """
            invoices.mapped('line_ids').filtered(lambda x: x.account_type in ('asset_receivable', 'liability_payable')).reconcile()

        # 100 (base, invoice) - 100 (base, refund) + 20 (tax, invoice) - 5 (25% tax, refund) = 15
        self._run_caba_generic_test(
            #   Name                      Balance
            [   0,                        1],
            [
                ('Sale',                     15),
                ('Purchase',                 15),
            ],
            on_all_invoices_created=reconcile_opposite_types
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_tax_report_grid_cash_basis_misc_pmt(self):
        """ Cash basis moves create for taxes based on payments are handled differently
        by the report; we want to ensure their sign is managed properly. This
        test runs the case where the invoice is paid with a misc operation instead
        of a payment.
        """
        def reconcile_with_misc_pmt(invoice):
            """ Create a misc operation equivalent to a full payment and reconciles
            the invoice with it.
            """
            # Pay the invoice with a misc operation simulating a payment, so that the cash basis entries are created
            invoice_reconcilable_line = invoice.line_ids.filtered(lambda x: x.account_type in ('liability_payable', 'asset_receivable'))
            account = (invoice.line_ids - invoice_reconcilable_line).account_id
            pmt_move = self.env['account.move'].create({
                'move_type': 'entry',
                'date': invoice.date,
                'line_ids': [Command.create({
                                'account_id': invoice_reconcilable_line.account_id.id,
                                'debit': invoice_reconcilable_line.credit,
                                'credit': invoice_reconcilable_line.debit,
                            }),
                            Command.create({
                                'account_id': account.id,
                                'credit': invoice_reconcilable_line.credit,
                                'debit': invoice_reconcilable_line.debit,
                            })],
            })
            pmt_move.action_post()
            payment_reconcilable_line = pmt_move.line_ids.filtered(lambda x: x.account_type in ('liability_payable', 'asset_receivable'))
            (invoice_reconcilable_line + payment_reconcilable_line).reconcile()

        # 100 (base, invoice) - 100 (base, refund) + 20 (tax, invoice) - 5 (25% tax, refund) = 15
        self._run_caba_generic_test(
            #   Name                      Balance
            [   0,                            1],
            [
                ('Sale',                     15),
                ('Purchase',                 15),
            ],
            on_invoice_created=reconcile_with_misc_pmt
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_caba_no_payment(self):
        """ The cash basis taxes of an unpaid invoice should
        never impact the report.
        """
        self._run_caba_generic_test(
            #   Name                      Balance
            [   0,                            1],
            [
                ('Sale',                    0.0),
                ('Purchase',                0.0),
            ]
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_caba_half_payment(self):
        """ Paying half the amount of the invoice should report half the
        base and tax amounts.
        """
        def register_half_payment_for_invoice(invoice):
            """ Fully pay the invoice, so that the cash basis entries are created
            """
            payment_method_id = self.inbound_payment_method_line if invoice.is_inbound() else self.outbound_payment_method_line
            self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
                'amount': invoice.amount_residual / 2,
                'payment_date': invoice.date,
                'payment_method_line_id': payment_method_id.id,
            })._create_payments()

        # 50 (base, invoice) - 50 (base, refund) + 10 (tax, invoice) - 2.5 (25% tax, refund) = 7.5
        self._run_caba_generic_test(
            #   Name                     Balance
            [   0,                            1],
            [
                ('Sale',                    7.5),
                ('Purchase',                7.5),
            ],
            on_invoice_created=register_half_payment_for_invoice
        )

    def test_caba_mixed_generic_report(self):
        """ Tests mixing taxes with different tax exigibilities displays correct amounts
        in the generic tax report.
        """
        self.env.company.tax_exigibility = True
        # Create taxes
        regular_tax = self.env['account.tax'].create({
            'name': 'Regular',
            'amount': 42,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            # We use default repartition: 1 base line, 1 100% tax line
        })

        caba_tax = self.env['account.tax'].create({
            'name': 'Cash Basis',
            'amount': 10,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'tax_exigibility': 'on_payment',
            # We use default repartition: 1 base line, 1 100% tax line
        })

        # Create an invoice using them, and post it
        invoice = self.init_invoice(
            'out_invoice',
            invoice_date='2021-07-01',
            post=True,
            amounts=[100],
            taxes=regular_tax + caba_tax,
            company=self.company_data['company'],
        )

        # Check the report only contains non-caba things
        report = self.env.ref("account.generic_tax_report")
        options = self._generate_options(report, invoice.date, invoice.date)
        self.assertLinesValues(
            report._get_lines(options),
            #   Name                         Net               Tax
            [   0,                             1,                2],
            [
                ("Sales",                     '',               42),
                ("Regular (42.0%)",          100,               42),
                ("Total Sales",               '',               42),
            ],
            options,
        )

        # Pay half of the invoice
        self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
            'amount': 76,
            'payment_date': invoice.date,
            'payment_method_line_id': self.outbound_payment_method_line.id,
        })._create_payments()

        # Check the report again: half the cash basis should be there
        self.assertLinesValues(
            report._get_lines(options),
            #   Name                          Net               Tax
            [   0,                              1,               2],
            [
                ("Sales",                      '',              47),
                ("Regular (42.0%)",           100,              42),
                ("Cash Basis (10.0%)",         50,               5),
                ("Total Sales",                '',              47),
            ],
            options,
        )

        # Pay the rest
        self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
            'amount': 76,
            'payment_date': invoice.date,
            'payment_method_line_id': self.outbound_payment_method_line.id,
        })._create_payments()

        # Check everything is in the report
        self.assertLinesValues(
            report._get_lines(options),
            #   Name                          Net              Tax
            [   0,                              1,               2],
            [
                ("Sales",                      '',              52),
                ("Regular (42.0%)",           100,              42),
                ("Cash Basis (10.0%)",        100,              10),
                ("Total Sales",                '',              52),
            ],
            options,
        )

    def test_tax_report_mixed_exigibility_affect_base_generic_invoice(self):
        """ Tests mixing caba and non-caba taxes with one of them affecting the base
        of the other worcs properly on invoices for generic report.
        """
        self.env.company.tax_exigibility = True
        # Create taxes
        regular_tax = self.env['account.tax'].create({
            'name': 'Regular',
            'amount': 42,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'include_base_amount': True,
            'sequence': 0,
            # We use default repartition: 1 base line, 1 100% tax line
        })

        caba_tax = self.env['account.tax'].create({
            'name': 'Cash Basis',
            'amount': 10,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'tax_exigibility': 'on_payment',
            'include_base_amount': True,
            'sequence': 1,
            # We use default repartition: 1 base line, 1 100% tax line
        })

        report = self.env.ref("account.generic_tax_report")
        # Case 1: on_invoice tax affecting on_payment tax's base
        self._run_check_suite_mixed_exigibility_affect_base(
            regular_tax + caba_tax,
            '2021-07-01',
            report,
            # Name,                          Net,               Tax
            [   0,                             1,                2],
            # Before payment
            [
                ("Sales",                     '',            42),
                ("Regular (42.0%)",          100,            42),
                ("Total Sales",               '',            42),
            ],
            # After paying 30%
            [
                ("Sales",                     '',            46.26),
                ("Regular (42.0%)",          100,            42),
                ("Cash Basis (10.0%)",        42.6,           4.26),
                ("Total Sales",               '',            46.26),
            ],
            # After full payment
            [
                ("Sales",                     '',             56.2),
                ("Regular (42.0%)",          100,             42),
                ("Cash Basis (10.0%)",       142,             14.2),
                ("Total Sales",               '',             56.2),
            ]
        )

        # Change sequence
        caba_tax.sequence = 0
        regular_tax.sequence = 1

        # Case 2: on_payment tax affecting on_invoice tax's base
        self._run_check_suite_mixed_exigibility_affect_base(
            regular_tax + caba_tax,
            '2021-07-02',
            report,
            #   Name                         Net                Tax
            [   0,                             1,                2],
            # Before payment
            [
                ("Sales",                     '',             46.2),
                ("Regular (42.0%)",          110,             46.2),
                ("Total Sales",               '',             46.2),
            ],
            # After paying 30%
            [
                ("Sales",                     '',             49.2),
                ("Cash Basis (10.0%)",        30,              3),
                ("Regular (42.0%)",          110,             46.2),
                ("Total Sales",               '',             49.2),
            ],
            # After full payment
            [
                ("Sales",                     '',             56.2),
                ("Cash Basis (10.0%)",       100,             10),
                ("Regular (42.0%)",          110,             46.2),
                ("Total Sales",               '',             56.2),
            ]
        )

    def test_tax_report_mixed_exigibility_affect_base_tags(self):
        """ Tests mixing caba and non-caba taxes with one of them affecting the base
        of the other worcs properly on invoices for tax report.
        """
        self.env.company.tax_exigibility = True
        # Create taxes
        tax_report = self.env['account.report'].create({
            'name': "Sokovia Accords",
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        regular_tax = self._add_basic_tax_for_report(tax_report, 42, 'sale', self.tax_group_1, [(100, None, True)])
        caba_tax = self._add_basic_tax_for_report(tax_report, 10, 'sale', self.tax_group_1, [(100, None, True)])

        regular_tax.write({
            'include_base_amount': True,
            'sequence': 0,
        })
        caba_tax.write({
            'include_base_amount': True,
            'tax_exigibility': 'on_payment',
            'sequence': 1,
        })

        # Case 1: on_invoice tax affecting on_payment tax's base
        self._run_check_suite_mixed_exigibility_affect_base(
            regular_tax + caba_tax,
            '2021-07-01',
            tax_report,
            #   Name                                       Balance
            [   0,                                               1],
            # Before payment
            [
                (f'{regular_tax.id}-invoice-base',          100),
                (f'{regular_tax.id}-invoice-100',            42),
                (f'{regular_tax.id}-refund-base',             0.0),
                (f'{regular_tax.id}-refund-100',              0.0),

                (f'{caba_tax.id}-invoice-base',               0.0),
                (f'{caba_tax.id}-invoice-100',                0.0),
                (f'{caba_tax.id}-refund-base',                0.0),
                (f'{caba_tax.id}-refund-100',                 0.0),
            ],
            # After paying 30%
            [
                (f'{regular_tax.id}-invoice-base',          100),
                (f'{regular_tax.id}-invoice-100',            42),
                (f'{regular_tax.id}-refund-base',             0.0),
                (f'{regular_tax.id}-refund-100',              0.0),

                (f'{caba_tax.id}-invoice-base',              42.6),
                (f'{caba_tax.id}-invoice-100',                4.26),
                (f'{caba_tax.id}-refund-base',                0.0),
                (f'{caba_tax.id}-refund-100',                 0.0),
            ],
            # After full payment
            [
                (f'{regular_tax.id}-invoice-base',          100),
                (f'{regular_tax.id}-invoice-100',            42),
                (f'{regular_tax.id}-refund-base',             0.0),
                (f'{regular_tax.id}-refund-100',              0.0),

                (f'{caba_tax.id}-invoice-base',             142),
                (f'{caba_tax.id}-invoice-100',               14.2),
                (f'{caba_tax.id}-refund-base',                0.0),
                (f'{caba_tax.id}-refund-100',                 0.0),
            ],
        )

        # Change sequence
        caba_tax.sequence = 0
        regular_tax.sequence = 1

        # Case 2: on_payment tax affecting on_invoice tax's base
        self._run_check_suite_mixed_exigibility_affect_base(
            regular_tax + caba_tax,
            '2021-07-02',
            tax_report,
            #   Name                                       Balance
            [   0,                                               1],
            # Before payment
            [
                (f'{regular_tax.id}-invoice-base',           110),
                (f'{regular_tax.id}-invoice-100',             46.2),
                (f'{regular_tax.id}-refund-base',              0.0),
                (f'{regular_tax.id}-refund-100',               0.0),

                (f'{caba_tax.id}-invoice-base',                0.0),
                (f'{caba_tax.id}-invoice-100',                 0.0),
                (f'{caba_tax.id}-refund-base',                 0.0),
                (f'{caba_tax.id}-refund-100',                  0.0),
            ],
            # After paying 30%
            [
                (f'{regular_tax.id}-invoice-base',           110),
                (f'{regular_tax.id}-invoice-100',             46.2),
                (f'{regular_tax.id}-refund-base',              0.0),
                (f'{regular_tax.id}-refund-100',               0.0),

                (f'{caba_tax.id}-invoice-base',               30),
                (f'{caba_tax.id}-invoice-100',                 3),
                (f'{caba_tax.id}-refund-base',                 0.0),
                (f'{caba_tax.id}-refund-100',                  0.0),
            ],
            # After full payment
            [
                (f'{regular_tax.id}-invoice-base',          110),
                (f'{regular_tax.id}-invoice-100',            46.2),
                (f'{regular_tax.id}-refund-base',             0.0),
                (f'{regular_tax.id}-refund-100',              0.0),

                (f'{caba_tax.id}-invoice-base',             100),
                (f'{caba_tax.id}-invoice-100',               10),
                (f'{caba_tax.id}-refund-base',                0.0),
                (f'{caba_tax.id}-refund-100',                 0.0),
            ],
        )

    def _run_check_suite_mixed_exigibility_affect_base(self, taxes, invoice_date, report, report_columns, vals_not_paid, vals_30_percent_paid, vals_fully_paid):
        # Create an invoice using them
        invoice = self.init_invoice(
            'out_invoice',
            invoice_date=invoice_date,
            post=True,
            amounts=[100],
            taxes=taxes,
            company=self.company_data['company'],
        )

        # Check the report
        report_options = self._generate_options(report, invoice.date, invoice.date)
        self.assertLinesValues(report._get_lines(report_options), report_columns, vals_not_paid, report_options)

        # Pay 30% of the invoice
        self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
            'amount': invoice.amount_residual * 0.3,
            'payment_date': invoice.date,
            'payment_method_line_id': self.outbound_payment_method_line.id,
        })._create_payments()

        # Check the report again: 30% of the caba amounts should be there
        self.assertLinesValues(report._get_lines(report_options), report_columns, vals_30_percent_paid, report_options)

        # Pay the rest: total caba amounts should be there
        self.env['account.payment.register'].with_context(active_ids=invoice.ids, active_model='account.move').create({
            'payment_date': invoice.date,
            'payment_method_line_id': self.outbound_payment_method_line.id,
        })._create_payments()

        # Check the report
        self.assertLinesValues(report._get_lines(report_options), report_columns, vals_fully_paid, report_options)

    def test_caba_always_exigible(self):
        """ Misc operations without payable nor receivable lines must always be exigible,
        whatever the tax_exigibility configured on their taxes.
        """
        tax_report = self.env['account.report'].create({
            'name': "Laplace's Box",
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        regular_tax = self._add_basic_tax_for_report(tax_report, 42, 'sale', self.tax_group_1, [(100, None, True)])
        caba_tax = self._add_basic_tax_for_report(tax_report, 10, 'sale', self.tax_group_1, [(100, None, True)])

        regular_tax.write({
            'include_base_amount': True,
            'sequence': 0,
        })
        caba_tax.write({
            'tax_exigibility': 'on_payment',
            'sequence': 1,
        })

        # Create a misc operation using various combinations of our taxes
        move = self.env['account.move'].create({
            'date': '2021-08-01',
            'journal_id': self.company_data['default_journal_misc'].id,
            'line_ids': [
                Command.create({
                    'name': "Test with %s" % ', '.join(taxes.mapped('name')),
                    'account_id': self.company_data['default_account_revenue'].id,
                    'credit': 100,
                    'tax_ids': [Command.set(taxes.ids)],
                })
                for taxes in (caba_tax, regular_tax, caba_tax + regular_tax)
            ] + [
                Command.create({
                    'name': "Balancing line",
                    'account_id': self.company_data['default_account_assets'].id,
                    'debit': 408.2,
                    'tax_ids': [],
                })
            ]
        })

        move.action_post()

        self.assertTrue(move.always_tax_exigible, "A move without payable/receivable line should always be exigible, whatever its taxes.")

        # Check tax report by grid
        report_options = self._generate_options(tax_report, move.date, move.date)
        self.assertLinesValues(
            tax_report._get_lines(report_options),
            #   Name                                        Balance
            [   0,                                               1],
            [
                (f'{regular_tax.id}-invoice-base',           200),
                (f'{regular_tax.id}-invoice-100',             84),
                (f'{regular_tax.id}-refund-base',              0.0),
                (f'{regular_tax.id}-refund-100',               0.0),

                (f'{caba_tax.id}-invoice-base',              242),
                (f'{caba_tax.id}-invoice-100',                24.2),
                (f'{caba_tax.id}-refund-base',                 0.0),
                (f'{caba_tax.id}-refund-100',                  0.0),
            ],
            report_options,
        )


        # Check generic tax report
        tax_report = self.env.ref("account.generic_tax_report")
        report_options = self._generate_options(tax_report, move.date, move.date)
        self.assertLinesValues(
            tax_report._get_lines(report_options),
            #   Name                               Net           Tax
            [   0,                                   1,           2],
            [
                ("Sales",                           '',       108.2),
                (f"{regular_tax.name} (42.0%)",    200,        84),
                (f"{caba_tax.name} (10.0%)",       242,        24.2),
                ("Total Sales",                     '',       108.2),
            ],
            report_options,
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_tax_report_grid_caba_negative_inv_line(self):
        """ Tests cash basis taxes work properly in case a line of the invoice
        has been made with a negative quantities and taxes (causing debit and
        credit to be inverted on the base line).
        """
        def neg_line_invoice_generator(inv_type, partner, account, date, tax):
            """ Invoices created here have a line at 100 with a negative quantity of -1.
            They also required a second line (here 200), so that the invoice doesn't
            have a negative total, but we don't put any tax on it.
            """
            return self.env['account.move'].create({
                'move_type': inv_type,
                'partner_id': partner.id,
                'invoice_date': date,
                'invoice_line_ids': [
                    Command.create({
                        'name': 'test',
                        'quantity': -1,
                        'account_id': account.id,
                        'price_unit': 100,
                        'tax_ids': [Command.set(tax.ids)],
                    }),

                    # Second line, so that the invoice doesn't have a negative total
                    Command.create({
                        'name': 'test',
                        'quantity': 1,
                        'account_id': account.id,
                        'price_unit': 200,
                        'tax_ids': [],
                    }),
                ],
            })

        # -100 (base, invoice) + 100 (base, refund) - 20 (tax, invoice) + 5 (25% tax, refund) = -15
        self._run_caba_generic_test(
            #   Name                      Balance
            [   0,                        1],
            [
                ('Sale',                     -15),
                ('Purchase',                 -15),
            ],
            on_invoice_created=self._register_full_payment_for_invoice,
            invoice_generator=neg_line_invoice_generator,
        )

    def test_fiscal_position_switch_all_option_flow(self):
        """ 'all' fiscal position option sometimes must be reset or enforced in order to keep
        the report consistent. We check those cases here.
        """
        foreign_tax_report = self.env['account.report'].create({
            'name': "",
            'country_id': self.foreign_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })
        foreign_vat_fpos = self.env['account.fiscal.position'].create({
            'name': "Test fpos",
            'country_id': self.foreign_country.id,
            'foreign_vat': '422211',
        })

        # Case 1: 'all' allowed if multiple fpos
        to_check = self.basic_tax_report.get_options({'fiscal_position': 'all', 'selected_variant_id': self.basic_tax_report.id})
        self.assertEqual(to_check['fiscal_position'], 'all', "Opening the report with 'all' fiscal_position option should work if there are fiscal positions for different states in that country")

        # Case 2: 'all' not allowed if domestic and no fpos
        self.foreign_vat_fpos.foreign_vat = None # No unlink because setupClass created some moves with it
        to_check = self.basic_tax_report.get_options({'fiscal_position': 'all', 'selected_variant_id': self.basic_tax_report.id})
        self.assertEqual(to_check['fiscal_position'], 'domestic', "Opening the domestic report with 'all' should change to 'domestic' if there's no state-specific fiscal position in the country")

        # Case 3: 'all' not allowed on foreign report with 1 fpos
        to_check = foreign_tax_report.get_options({'fiscal_position': 'all', 'selected_variant_id': foreign_tax_report.id})
        self.assertEqual(to_check['fiscal_position'], foreign_vat_fpos.id, "Opening a foreign report with only one single fiscal position with 'all' option should change if to only select this fiscal position")

        # Case 4: always 'all' on generic report
        generic_tax_report = self.env.ref("account.generic_tax_report")
        to_check = generic_tax_report.get_options({'fiscal_position': foreign_vat_fpos.id, 'selected_variant_id': generic_tax_report.id})
        self.assertEqual(to_check['fiscal_position'], 'all', "The generic report should always use 'all' fiscal position option.")

    def test_tax_report_multi_inv_line_no_rep_account(self):
        """ Tests the behavior of the tax report when using a tax without any
        repartition account (hence doing its tax lines on the base account),
        and using the tax on two lines (to make sure grouping is handled
        properly by the report).
        We do that for both regular and cash basis taxes.
        """
        # Create taxes
        regular_tax = self.env['account.tax'].create({
            'name': 'Regular',
            'amount': 42,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            # We use default repartition: 1 base line, 1 100% tax line
        })

        caba_tax = self.env['account.tax'].create({
            'name': 'Cash Basis',
            'amount': 42,
            'amount_type': 'percent',
            'type_tax_use': 'sale',
            'tax_exigibility': 'on_payment',
            # We use default repartition: 1 base line, 1 100% tax line
        })
        self.env.company.tax_exigibility = True

        # Make one invoice of 2 lines for each of our taxes
        invoice_date = fields.Date.from_string('2021-04-01')
        other_account_revenue = self.company_data['default_account_revenue'].copy()

        regular_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_a.id,
            'invoice_date': invoice_date,
            'invoice_line_ids': [
                Command.create({
                    'name': 'line 1',
                    'account_id': self.company_data['default_account_revenue'].id,
                    'price_unit': 100,
                    'tax_ids': [Command.set(regular_tax.ids)],
                }),

                Command.create({
                    'name': 'line 2',
                    'account_id': other_account_revenue.id,
                    'price_unit': 100,
                    'tax_ids': [Command.set(regular_tax.ids)],
                })
            ],
        })

        caba_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_a.id,
            'invoice_date': invoice_date,
            'invoice_line_ids': [
                Command.create({
                    'name': 'line 1',
                    'account_id': self.company_data['default_account_revenue'].id,
                    'price_unit': 100,
                    'tax_ids': [Command.set(caba_tax.ids)],
                }),

                Command.create({
                    'name': 'line 2',
                    'account_id': other_account_revenue.id,
                    'price_unit': 100,
                    'tax_ids': [Command.set(caba_tax.ids)],
                })
            ],
        })

        # Post the invoices
        regular_invoice.action_post()
        caba_invoice.action_post()

        # Pay cash basis invoice
        self.env['account.payment.register'].with_context(active_ids=caba_invoice.ids, active_model='account.move').create({
            'payment_date': invoice_date,
        })._create_payments()

        # Check the generic report
        report = self.env.ref("account.generic_tax_report")
        options = self._generate_options(report, invoice_date, invoice_date)
        self.assertLinesValues(
            report._get_lines(options),
            #   Name                         Net               Tax
            [   0,                             1,                2],
            [
                ("Sales",                     '',              168),
                ("Regular (42.0%)",          200,               84),
                ("Cash Basis (42.0%)",       200,               84),
                ("Total Sales",               '',              168),
            ],
            options,
        )

    def test_tax_unit(self):
        tax_unit_report = self.env['account.report'].create({
            'name': "And now for something completely different",
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        company_1 = self.company_data['company']
        company_2 = self.company_data_2['company']
        company_3 = self.setup_other_company(name="Company 3")['company']
        unit_companies = company_1 + company_2
        all_companies = unit_companies + company_3

        company_2.currency_id = company_1.currency_id

        tax_unit = self.env['account.tax.unit'].create({
            'name': "One unit to rule them all",
            'country_id': self.fiscal_country.id,
            'vat': "DW1234567890",
            'company_ids': [Command.set(unit_companies.ids)],
            'main_company_id': company_1.id,
        })
        tax_group_2 = self._instantiate_basic_test_tax_group(company_2)
        self._instantiate_basic_test_tax_group(company_3)

        created_taxes = {}
        tax_accounts = {}
        invoice_date = fields.Date.from_string('2018-01-01')
        for index, company in enumerate(all_companies):
            # Make sure the fiscal country is what we want
            self.change_company_country(company, self.fiscal_country)

            # Create a tax for this report
            tax_account = self.env['account.account'].create({
                'name': 'Tax unit test tax account',
                'code': 'test.tax.unit',
                'account_type': 'asset_current',
                'company_ids': [Command.link(company.id)],
            })
            tax_group = self.env['account.tax.group'].search([('company_id', '=', company.id), ('name', '=', 'Test tax group')], limit=1)

            test_tax = self._add_basic_tax_for_report(tax_unit_report, 42, 'sale', tax_group, [(100, tax_account, True)], company=company)
            created_taxes[company] = test_tax
            tax_accounts[company] = tax_account

            # Create an invoice with this tax
            self.init_invoice(
                'out_invoice',
                partner=self.partner_a,
                invoice_date=invoice_date,
                post=True,
                amounts=[100 * (index + 1)],
                taxes=test_tax, company=company
            )

        # Check report content, with various scenarios of active companies
        for active_companies in (company_1, company_2, company_3, unit_companies, all_companies, company_2 + company_3):

            # In the regular flow, selected companies are changed from the selector, in the UI.
            # The tax unit option of the report changes the value of the selector, so it'll
            # always stay consistent with allowed_company_ids.
            options = self._generate_options(
                tax_unit_report.with_context(allowed_company_ids=active_companies.ids),
                invoice_date,
                invoice_date,
                {'fiscal_position': 'domestic'}
            )

            target_unit = tax_unit if company_3 != active_companies[0] else None
            self.assertTrue(
                (not target_unit and not options['available_tax_units']) \
                or (options['available_tax_units'] and any(available_unit['id'] == target_unit.id for available_unit in options['available_tax_units'])),
                "The tax unit should always be available when self.env.company is part of it."
            )

            self.assertEqual(
                options['tax_unit'] != 'company_only',
                active_companies == unit_companies,
                "The tax unit option should only be enabled when all the companies of the unit are selected, and nothing else."
            )

            self.assertLinesValues(
                tax_unit_report.with_context(allowed_company_ids=active_companies.ids)._get_lines(options),
                #   Name                                                          Balance
                [   0,                                                            1],
                [
                    # Company 1
                    (f'{created_taxes[company_1].id}-invoice-base',           100 if company_1 in active_companies else 0.0),
                    (f'{created_taxes[company_1].id}-invoice-100',             42 if company_1 in active_companies else 0.0),
                    (f'{created_taxes[company_1].id}-refund-base',             0.0),
                    (f'{created_taxes[company_1].id}-refund-100',              0.0),

                    # Company 2
                    (f'{created_taxes[company_2].id}-invoice-base',           200 if active_companies == unit_companies or active_companies[0] == company_2 else 0.0),
                    (f'{created_taxes[company_2].id}-invoice-100',             84 if active_companies == unit_companies or active_companies[0] == company_2 else 0.0),
                    (f'{created_taxes[company_2].id}-refund-base',             0.0),
                    (f'{created_taxes[company_2].id}-refund-100',              0.0),

                    # Company 3 (not part of the unit, so always 0 in our cases)
                    (f'{created_taxes[company_3].id}-invoice-base',           300 if company_3 == active_companies[0] else 0.0),
                    (f'{created_taxes[company_3].id}-invoice-100',            126 if company_3 == active_companies[0] else 0.0),
                    (f'{created_taxes[company_3].id}-refund-base',             0.0),
                    (f'{created_taxes[company_3].id}-refund-100',              0.0),
                ],
                options,
            )

        # Check closing for the vat unit
        options = self._generate_options(
            tax_unit_report.with_context(allowed_company_ids=unit_companies.ids),
            invoice_date,
            invoice_date,
            {'tax_report': tax_unit_report.id, 'fiscal_position': 'all'}
        )

        self._assert_vat_closing(tax_unit_report, options, {
            (company_1, self.env['account.fiscal.position']): [
                {'debit': 42,       'credit':  0,       'account_id': tax_accounts[company_1].id},
                {'debit':  0,       'credit': 42,       'account_id': self.tax_group_1.tax_payable_account_id.id},
            ],

            (company_1, self.foreign_vat_fpos): [
                # Don't check accounts here; they are gotten by searching on taxes, basically we don't care about them as it's 0-balanced.
                {'debit':  0,       'credit':  0,},
                {'debit':  0,       'credit':  0,},
            ],

            (company_2, self.env['account.fiscal.position']): [
                {'debit': 84,       'credit':  0,       'account_id': tax_accounts[company_2].id},
                {'debit':  0,       'credit': 84,       'account_id': tax_group_2.tax_payable_account_id.id},
            ],
        })

    def test_tax_unit_create_horizontal_group(self):
        """ This test will try to create two tax units to see if the creation of horizontal group works as expected """
        company_1 = self.company_data['company']
        company_2 = self.company_data_2['company']
        company_2.currency_id = company_1.currency_id
        unit_companies_1 = company_1 + company_2

        company_3 = self.setup_other_company(name="Company 3")['company']
        company_4 = self.setup_other_company(name="Company 4")['company']
        unit_companies_2 = company_3 + company_4

        self.env['account.tax.unit'].create([
            {
                'name': "First Tax Unit",
                'country_id': self.fiscal_country.id,
                'vat': "DW1234567890",
                'company_ids': [Command.set(unit_companies_1.ids)],
                'main_company_id': company_1.id,
            },
            {
                'name': "Second Tax Unit",
                'country_id': self.fiscal_country.id,
                'vat': "DW1234567890",
                'company_ids': [Command.set(unit_companies_2.ids)],
                'main_company_id': company_3.id,
            },
        ])

        # Check if the two last horizontal_group are the one created from the tax unit
        horizontal_groups = self.env['account.report.horizontal.group'].search([])[-2:]
        self.assertEqual(['First Tax Unit', 'Second Tax Unit'], horizontal_groups.mapped('name'))

        # Check if the generic_tax_report has the two groups
        generic_tax_report = self.env.ref('account.generic_tax_report')
        self.assertTrue(all(horizontal_group_id in generic_tax_report.horizontal_group_ids.ids for horizontal_group_id in horizontal_groups.ids))

    def test_tax_unit_auto_fiscal_position(self):
        # setup companies
        company_1 = self.company_data['company']
        company_2 = self.company_data_2['company']
        company_2.currency_id = company_1.currency_id
        company_3 = self.setup_other_company(name="Company 3")['company']
        company_4 = self.setup_other_company(name="Company 4")['company']
        unit_companies = company_1 + company_2 + company_3
        all_companies = unit_companies + company_4

        # create a tax unit containing 3 companies
        tax_unit = self.env['account.tax.unit'].create({
            'name': "One unit to rule them all",
            'country_id': self.fiscal_country.id,
            'vat': "DW1234567890",
            'company_ids': [Command.set(unit_companies.ids)],
            'main_company_id': company_1.id,
        })
        self.assertFalse(tax_unit.fpos_synced)
        tax_unit.action_sync_unit_fiscal_positions()
        for current_company in unit_companies:
            # verify that partners for other companies in the unit have a fiscal position that removes taxes
            created_fp = tax_unit._get_tax_unit_fiscal_positions(companies=current_company)
            self.assertTrue(created_fp)
            self.assertEqual(
                (unit_companies - current_company).partner_id.with_company(current_company).property_account_position_id,
                created_fp
            )
            self.assertTrue(created_fp.tax_ids.tax_src_id)
            self.assertFalse(created_fp.tax_ids.tax_dest_id)
            self.assertFalse(current_company.partner_id.with_company(current_company).property_account_position_id)
        tax_unit._compute_fiscal_position_completion()
        self.assertTrue(tax_unit.fpos_synced)

        # remove company 3 from the unit and verify that the fiscal positions are removed from the relevant companies
        tax_unit.write({
            'company_ids': [Command.unlink(company_3.id)]
        })
        self.assertFalse(tax_unit.fpos_synced)
        tax_unit.action_sync_unit_fiscal_positions()
        self.assertFalse(company_3.partner_id.with_company(company_1).property_account_position_id)
        self.assertFalse(company_1.partner_id.with_company(company_3).property_account_position_id)
        company_1_fp = tax_unit._get_tax_unit_fiscal_positions(companies=company_1)
        self.assertEqual(company_2.partner_id.with_company(company_1).property_account_position_id, company_1_fp)
        self.assertTrue(tax_unit.fpos_synced)

        # add company 3, remove company 2
        tax_unit.write({
            'company_ids': [Command.link(company_3.id), Command.unlink(company_2.id)]
        })
        self.assertFalse(tax_unit.fpos_synced)
        tax_unit.action_sync_unit_fiscal_positions()
        company_1_fp = tax_unit._get_tax_unit_fiscal_positions(companies=company_1)
        self.assertEqual(company_3.partner_id.with_company(company_1).property_account_position_id, company_1_fp)
        self.assertFalse(company_2.partner_id.with_company(company_1).property_account_position_id)
        self.assertTrue(company_1.partner_id.with_company(company_3).property_account_position_id)

        # remove the fiscal position from the partner of company 1
        company_1.partner_id.with_company(company_3).property_account_position_id = False
        self.assertFalse(tax_unit.fpos_synced)
        tax_unit.action_sync_unit_fiscal_positions()
        self.assertTrue(tax_unit.fpos_synced)

        #replace all companies
        tax_unit.write({
            'company_ids': [Command.set([company_2.id, company_4.id])],
            'main_company_id': company_2.id,
        })
        self.assertFalse(tax_unit.fpos_synced)
        tax_unit.action_sync_unit_fiscal_positions()
        self.assertTrue(tax_unit.fpos_synced)

        # no fiscal positions should exist after deleting the unit
        tax_unit.unlink()
        for company in all_companies:
            self.assertFalse(all_companies.partner_id.with_company(company).property_account_position_id)

    def test_vat_unit_with_foreign_vat_fpos(self):
        # Company 1 has the test country as domestic country, and a foreign VAT fpos in a different province
        company_1 = self.company_data['company']

        # Company 2 belongs to a different country, and has a foreign VAT fpos to the test country, with just one
        # move adding 1000 in the first line of the report.
        company_2 = self.company_data_2['company']
        company_2.currency_id = company_1.currency_id

        foreign_vat_fpos = self.env['account.fiscal.position'].create({
            'name': 'fpos',
            'foreign_vat': 'tagada tsoin tsoin',
            'country_id': self.fiscal_country.id,
            'company_id': company_2.id,
        })

        report_line = self.env['account.report.line'].search([
            ('report_id', '=', self.basic_tax_report.id),
            ('name', '=', f'{self.test_fpos_tax_sale.id}-invoice-base'),
        ])

        plus_tag = report_line.expression_ids._get_matching_tags("+")

        comp2_move = self.env['account.move'].create({
            'journal_id': self.company_data_2['default_journal_misc'].id,
            'date': '2021-02-02',
            'fiscal_position_id': foreign_vat_fpos.id,
            'line_ids': [
                Command.create({
                    'account_id': self.company_data_2['default_account_assets'].id,
                    'credit': 1000,
                }),

                Command.create({
                    'account_id': self.company_data_2['default_account_expense'].id,
                    'debit': 1000,
                    'tax_tag_ids': [Command.set(plus_tag.ids)],
                }),
            ]
        })

        comp2_move.action_post()

        # Both companies belong to a tax unit in test country
        tax_unit = self.env['account.tax.unit'].create({
            'name': "Taxvengers, assemble!",
            'country_id': self.fiscal_country.id,
            'vat': "dudu",
            'company_ids': [Command.set((company_1 + company_2).ids)],
            'main_company_id': company_1.id,
        })

        # Opening the tax report for test country, we should see the same as in test_tax_report_fpos_everything + the 1000 of company 2, whatever the main company

        # Varying the order of the two companies (and hence changing the "main" active one) should make no difference.
        for unit_companies in ((company_1 + company_2), (company_2 + company_1)):
            options = self._generate_options(
                self.basic_tax_report.with_context(allowed_company_ids=unit_companies.ids),
                fields.Date.from_string('2021-01-01'),
                fields.Date.from_string('2021-03-31'),
                {'fiscal_position': 'all'}
            )

            self.assertEqual(options['tax_unit'], tax_unit.id, "The tax unit should have been auto-detected.")

            self.assertLinesValues(
                self.basic_tax_report._get_lines(options),
                #   Name                                                          Balance
                [   0,                                                            1],
                [
                    # out_invoice + 1000 from company_2 on the first line
                    (f'{self.test_fpos_tax_sale.id}-invoice-base',          2000),
                    (f'{self.test_fpos_tax_sale.id}-invoice-30',             150),
                    (f'{self.test_fpos_tax_sale.id}-invoice-70',             350),
                    (f'{self.test_fpos_tax_sale.id}-invoice--100',          -500),

                    #out_refund
                    (f'{self.test_fpos_tax_sale.id}-refund-base',           -220),
                    (f'{self.test_fpos_tax_sale.id}-refund-30',              -33),
                    (f'{self.test_fpos_tax_sale.id}-refund-70',              -77),
                    (f'{self.test_fpos_tax_sale.id}-refund--100',            110),

                    #in_invoice
                    (f'{self.test_fpos_tax_purchase.id}-invoice-base',      1400),
                    (f'{self.test_fpos_tax_purchase.id}-invoice-40',         280),
                    (f'{self.test_fpos_tax_purchase.id}-invoice-60',         420),
                    (f'{self.test_fpos_tax_purchase.id}-invoice--100',      -700),

                    #in_refund
                    (f'{self.test_fpos_tax_purchase.id}-refund-base',       -660),
                    (f'{self.test_fpos_tax_purchase.id}-refund-40',         -132),
                    (f'{self.test_fpos_tax_purchase.id}-refund-60',         -198),
                    (f'{self.test_fpos_tax_purchase.id}-refund--100',        330),
                ],
                options,
            )

    @freeze_time('2023-10-05 02:00:00')
    def test_tax_report_with_entries_with_sale_and_purchase_taxes(self):
        """ Ensure signs are managed properly for entry moves.
        This test runs the case where invoice/bill like entries are created and reverted.
        """
        today = fields.Date.today()
        company = self.env.user.company_id
        tax_report = self.env['account.report'].create({
            'name': 'Test',
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        # We create some report lines
        report_lines_dict = {
            'sale': [
                self._create_tax_report_line('Sale base', tax_report, sequence=1, tag_name='sale_b'),
                self._create_tax_report_line('Sale tax', tax_report, sequence=1, tag_name='sale_t'),
            ],
            'purchase': [
                self._create_tax_report_line('Purchase base', tax_report, sequence=2, tag_name='purchase_b'),
                self._create_tax_report_line('Purchase tax', tax_report, sequence=2, tag_name='purchase_t'),
            ],
        }

        # We create a sale and a purchase tax, linked to our report line tags
        taxes = self._create_taxes_for_report_lines(report_lines_dict, company)

        account_types = {
            'sale': 'income',
            'purchase': 'expense',
        }
        for tax in taxes:
            account = self.env['account.account'].search([('company_ids', '=', company.id), ('account_type', '=', account_types[tax.type_tax_use])], limit=1)
            # create one entry and it's reverse
            move_form = Form(self.env['account.move'].with_context(default_move_type='entry'))
            with move_form.line_ids.new() as line:
                line.account_id = account
                if tax.type_tax_use == 'sale':
                    line.credit = 1000
                else:
                    line.debit = 1000
                line.tax_ids.clear()
                line.tax_ids.add(tax)

            # Create a third account.move.line for balance.
            with move_form.line_ids.new() as line:
                line.account_id = account
                if tax.type_tax_use == 'sale':
                    line.debit = 1200
                else:
                    line.credit = 1200
            move = move_form.save()
            move.action_post()
            refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=move.ids).create({
                'reason': 'reasons',
                'journal_id': self.company_data['default_journal_misc'].id,
            })
            refund_wizard.modify_moves()

            self.assertEqual(
                move.line_ids.tax_repartition_line_id,
                move.reversal_move_ids.line_ids.tax_repartition_line_id,
                "The same repartition line should be used when reverting a misc operation, to ensure they sum up to 0 in all cases."
            )

        options = self._generate_options(tax_report, today, today)

        # We check the taxes on entries have impacted the report properly
        inv_report_lines = tax_report._get_lines(options)

        self.assertLinesValues(
            inv_report_lines,
            #   Name                         Balance
            [   0,                           1],
            [
                ('Sale base',              0.0),
                ('Sale tax',               0.0),
                ('Purchase base',          0.0),
                ('Purchase tax',           0.0),
            ],
            options,
        )

    @freeze_time('2023-10-05 02:00:00')
    def test_invoice_like_entry_reverse_caba_report(self):
        """ Cancelling the reconciliation of an invoice using cash basis taxes should reverse the cash basis move
        in such a way that the original cash basis move lines' impact falls down to 0.
        """
        self.env.company.tax_exigibility = True

        tax_report = self.env['account.report'].create({
            'name': 'CABA test',
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })
        report_line_invoice_base = self._create_tax_report_line('Invoice base', tax_report, sequence=1, tag_name='caba_invoice_base')
        report_line_invoice_tax = self._create_tax_report_line('Invoice tax', tax_report, sequence=2, tag_name='caba_invoice_tax')
        report_line_refund_base = self._create_tax_report_line('Refund base', tax_report, sequence=3, tag_name='caba_refund_base')
        report_line_refund_tax = self._create_tax_report_line('Refund tax', tax_report, sequence=4, tag_name='caba_refund_tax')

        tax = self.env['account.tax'].create({
            'name': 'The Tax Who Says Ni',
            'type_tax_use': 'sale',
            'amount': 42,
            'tax_exigibility': 'on_payment',
            'invoice_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': [Command.set(report_line_invoice_base.expression_ids._get_matching_tags("+").ids)],
                }),
                Command.create({
                    'repartition_type': 'tax',
                    'tag_ids': [Command.set(report_line_invoice_tax.expression_ids._get_matching_tags("+").ids)],
                }),
            ],
            'refund_repartition_line_ids': [
                Command.create({
                    'repartition_type': 'base',
                    'tag_ids': [Command.set(report_line_refund_base.expression_ids._get_matching_tags("+").ids)],
                }),
                Command.create({
                    'repartition_type': 'tax',
                    'tag_ids': [Command.set(report_line_refund_tax.expression_ids._get_matching_tags("+").ids)],
                }),
            ],
        })

        move_form = Form(self.env['account.move'] \
                    .with_company(self.company_data['company']) \
                    .with_context(default_move_type='entry'))
        move_form.date = fields.Date.today()
        with move_form.line_ids.new() as base_line_form:
            base_line_form.name = "Base line"
            base_line_form.account_id = self.company_data['default_account_revenue']
            base_line_form.credit = 100
            base_line_form.tax_ids.clear()
            base_line_form.tax_ids.add(tax)

        with move_form.line_ids.new() as receivable_line_form:
            receivable_line_form.name = "Receivable line"
            receivable_line_form.account_id = self.company_data['default_account_receivable']
            receivable_line_form.debit = 142
        move = move_form.save()
        move.action_post()
        # make payment
        payment = self.env['account.payment'].create({
            'payment_type': 'inbound',
            'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
            'partner_type': 'customer',
            'partner_id': self.partner_a.id,
            'amount': 142,
            'date': move.date,
            'journal_id': self.company_data['default_journal_bank'].id,
        })
        payment.action_post()

        report_options = self._generate_options(tax_report, move.date, move.date)
        self.assertLinesValues(
            tax_report._get_lines(report_options),
            #   Name                                       Balance
            [   0,                                               1],
            [
                ('Invoice base',                               0.0),
                ('Invoice tax',                                0.0),
                ('Refund base',                                0.0),
                ('Refund tax',                                 0.0),
            ],
            report_options,
        )

        # Reconcile the move with a payment
        (payment.move_id + move).line_ids.filtered(lambda x: x.account_id == self.company_data['default_account_receivable']).reconcile()
        self.assertLinesValues(
            tax_report._get_lines(report_options),
            #   Name                                       Balance
            [   0,                                               1],
            [
                ('Invoice base',                               100),
                ('Invoice tax',                                 42),
                ('Refund base',                                0.0),
                ('Refund tax',                                 0.0),
            ],
            report_options,
        )

        # Unreconcile the moves
        move.line_ids.remove_move_reconcile()
        self.assertLinesValues(
            tax_report._get_lines(report_options),
            #   Name                                       Balance
            [   0,                                               1],
            [
                ('Invoice base',                               0.0),
                ('Invoice tax',                                0.0),
                ('Refund base',                                0.0),
                ('Refund tax',                                 0.0),
            ],
            report_options,
        )

    def test_tax_report_get_past_closing_entry(self):
        options = self._generate_options(self.basic_tax_report, '2021-01-01', '2021-12-31')

        with patch.object(type(self.env['account.move']), '_get_vat_report_attachments', autospec=True, side_effect=lambda *args, **kwargs: []):
            # Generate the tax closing entry and close the period without posting it, so that we can assert on the exception
            vat_closing_move = self.env['account.generic.tax.report.handler']._generate_tax_closing_entries(self.basic_tax_report, options)
            vat_closing_move.action_post()

        # Calling the action_periodic_vat_entries method should return the existing tax closing entry.
        vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        self.assertEqual(vat_closing_move.id, vat_closing_action['res_id'])

    def setup_multi_vat_context(self):
        """Setup 2 tax reports, taxes and partner to represent a multiVat context in which both taxes affect both tax report"""

        def get_positive_tag(report_line):
            return report_line.expression_ids._get_matching_tags().filtered(lambda x: not x.tax_negate)

        self.env['account.fiscal.position'].create({
            'name': "FP With foreign VAT number",
            'country_id': self.foreign_country.id,
            'foreign_vat': '422211',
            'auto_apply': True,
        })

        local_tax_report, foreign_tax_report = self.env['account.report'].create([
            {
                'name': "The Local Tax Report",
                'country_id': self.company_data['company'].account_fiscal_country_id.id,
                'root_report_id': self.env.ref('account.generic_tax_report').id,
                'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
            },
            {
                'name': "The Foreign Tax Report",
                'country_id': self.foreign_country.id,
                'root_report_id': self.env.ref('account.generic_tax_report').id,
                'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance', })],
            },
        ])
        local_tax_report_base_line = self._create_tax_report_line("base_local", local_tax_report, sequence=1, code="base_local", tag_name="base_local")
        local_tax_report_tax_line = self._create_tax_report_line("tax_local", local_tax_report, sequence=2, code="tax_local", tag_name="tax_local")
        foreign_tax_report_base_line = self._create_tax_report_line("base_foreign", foreign_tax_report, sequence=1, code="base_foreign", tag_name="base_foreign")
        foreign_tax_report_tax_line = self._create_tax_report_line("tax_foreign", foreign_tax_report, sequence=2, code="tax_foreign", tag_name="tax_foreign")

        local_tax_affecting_foreign_tax_report = self.env['account.tax'].create({'name': "The local tax affecting the foreign report", 'amount': 20})
        foreign_tax_affecting_local_tax_report = self.env['account.tax'].create({
            'name': "The foreign tax affecting the local tax report",
            'amount': 20,
            'country_id': self.foreign_country.id,
        })
        for tax in (local_tax_affecting_foreign_tax_report, foreign_tax_affecting_local_tax_report):
            base_line, tax_line = tax.invoice_repartition_line_ids
            base_line.tag_ids = get_positive_tag(local_tax_report_base_line) + get_positive_tag(foreign_tax_report_base_line)
            tax_line.tag_ids = get_positive_tag(local_tax_report_tax_line) + get_positive_tag(foreign_tax_report_tax_line)

        local_partner = self.partner_a
        foreign_partner = self.partner_a.copy()
        foreign_partner.country_id = self.foreign_country

        return {
            'tax_report': (local_tax_report, foreign_tax_report,),
            'taxes': (local_tax_affecting_foreign_tax_report, foreign_tax_affecting_local_tax_report,),
            'partners': (local_partner, foreign_partner),
        }

    def test_local_tax_can_affect_foreign_tax_report(self):
        setup_data = self.setup_multi_vat_context()
        local_tax_report, foreign_tax_report = setup_data['tax_report']
        local_tax_affecting_foreign_tax_report, _ = setup_data['taxes']
        local_partner, _ = setup_data['partners']

        invoice = self.init_invoice('out_invoice', partner=local_partner, invoice_date='2022-12-01', post=True, amounts=[100], taxes=local_tax_affecting_foreign_tax_report)
        options = self._generate_options(local_tax_report, invoice.date, invoice.date)
        self.assertLinesValues(
            local_tax_report._get_lines(options),
            #   Name                                        Balance
            [   0,                                                1],
            [
                ("base_local",                                100.0),
                ("tax_local",                                  20.0),
            ],
            options,
        )

        options = self._generate_options(foreign_tax_report, invoice.date, invoice.date)
        self.assertLinesValues(
            foreign_tax_report._get_lines(options),
            #   Name                                          Balance
            [   0,                                                1],
            [
                ("base_foreign",                              100.0),
                ("tax_foreign",                                20.0),
            ],
            options,
        )

    def test_foreign_tax_can_affect_local_tax_report(self):
        setup_data = self.setup_multi_vat_context()
        local_tax_report, foreign_tax_report = setup_data['tax_report']
        _, foreign_tax_affecting_local_tax_report = setup_data['taxes']
        _, foreign_partner = setup_data['partners']

        invoice = self.init_invoice('out_invoice', partner=foreign_partner, invoice_date='2022-12-01', post=True, amounts=[100], taxes=foreign_tax_affecting_local_tax_report)
        options = self._generate_options(local_tax_report, invoice.date, invoice.date)
        self.assertLinesValues(
            local_tax_report._get_lines(options),
            #   Name                                        Balance
            [   0,                                                1],
            [
                ("base_local",                                100.0),
                ("tax_local",                                  20.0),
            ],
            options,
        )

        options = self._generate_options(foreign_tax_report, invoice.date, invoice.date)
        self.assertLinesValues(
            foreign_tax_report._get_lines(options),
            #   Name                                          Balance
            [   0,                                                1],
            [
                ("base_foreign",                              100.0),
                ("tax_foreign",                                20.0),
            ],
            options,
        )
    def test_engine_external_many_fiscal_positions(self):
        # Create a tax report that contains default manual expressions
        self.basic_tax_report_2 = self.env['account.report'].create({
            'name': "The Other Tax Report",
            'country_id': self.fiscal_country.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
            'line_ids': [
                Command.create({
                    'name': "test_line_1",
                    'code': "test_line_1",
                    'sequence': 1,
                    'expression_ids': [
                        Command.create({
                            'date_scope': 'strict_range',
                            'engine': 'external',
                            'formula': 'sum',
                            'label': 'balance',
                        }),
                        Command.create({
                            'date_scope': 'strict_range',
                            'engine': 'account_codes',
                            'formula': '101',
                            'label': '_default_balance',
                        })
                    ]
                }),
                Command.create({
                    'name': "test_line_2",
                    'code': "test_line_2",
                    'sequence': 2,
                    'expression_ids': [
                        Command.create({
                            'date_scope': 'strict_range',
                            'engine': 'account_codes',
                            'formula': '101',
                            'label': 'balance',
                        })
                    ],
                })
            ]
        })

        company_2 = self.company_data_2['company']
        company_2.country_id = self.fiscal_country
        company_2.currency_id = self.company_data['company'].currency_id

        # create two foreign fiscal positions (FPs), so we could create moves for each of them
        foreign_vat_fpos = self.env['account.fiscal.position'].create([
            {
                'name': 'fpos 1',
                'foreign_vat': 'A Swallow from Africa',
                'country_id': self.fiscal_country.id,
                'company_id': company_2.id,
                'state_ids': self.country_state_1,
            },
            {
                'name': 'fpos 2',
                'foreign_vat': 'A Swallow from Europe',
                'country_id': self.fiscal_country.id,
                'company_id': company_2.id,
                'state_ids': self.country_state_2,
            },
        ])

        test_account_1 = self.env['account.account'].create({
            'code': "101007",
            'name': "test account",
            'account_type': "asset_current",
            'company_ids': [Command.link(company_2.id)],
        })

        test_account_2 = self.env['account.account'].create({
            'code': "test",
            'name': "test",
            'account_type': "asset_current",
            'company_ids': [Command.link(company_2.id)],
        })

        move_vals = [{
            'date': fields.Date.from_string('2020-01-01'),
            'fiscal_position_id': fp.id,
            'company_id': company_2.id,
            'line_ids': [
                Command.create({
                    'name': 'line 1',
                    'account_id': test_account_1.id,
                    'debit': 1000 * (i + 1),
                    'credit': 0.0,
                }),
                Command.create({
                    'name': 'line 2',
                    'account_id': test_account_2.id,
                    'debit': 0.0,
                    'credit': 1000 * (i + 1),
                }),
            ]
        } for i, fp in enumerate(foreign_vat_fpos)]

        # create a move that includes an account starting with '101'
        # to make sure its amount does not appear in the tax report for company_2
        other_company_move_vals = {
            'date': fields.Date.from_string('2020-01-01'),
            'company_id': self.company_data['company'].id,
            'line_ids': [
                Command.create({
                    'name': 'line 1',
                    'account_id': self.company_data['default_account_revenue'].id,
                    'debit': 1200,
                    'credit': 0.0,
                }),
                Command.create({
                    'name': 'line 2',
                    'account_id': self.company_data['default_account_assets'].id,
                    'debit': 0.0,
                    'credit': 1200,
                }),
            ]
        }

        moves = self.env['account.move'].create(move_vals)
        moves.action_post()
        other_company_move = self.env['account.move'].create(other_company_move_vals)
        other_company_move.action_post()

        # we need to create different options per FP
        fiscal_positions = ['all'] + foreign_vat_fpos.ids
        report_options = {}
        for fp in fiscal_positions:
            fp_options = self._generate_options(
                self.basic_tax_report_2.with_context(allowed_company_ids=[company_2.id]),
                '2020-01-01', '2020-01-04',
                default_options={
                    'fiscal_position': fp,
                }
            )
            report_options[fp] = fp_options

        # when we filter by all FPs, the result on the second line
        # should be the sum of the moves
        # the first line contains a default expression and remains empty
        # until we set a lock date
        total_amount = sum([1000 * (i + 1) for i in range(len(foreign_vat_fpos.ids))])
        report_lines = self.basic_tax_report_2\
            .with_company(company_2)._get_lines(report_options['all'])
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [0,                             1],
            [
                ('test_line_1',           0.0),
                ('test_line_2',  total_amount),
            ],
            report_options['all'],
        )

        # subsequently, line 2 should only contain the amount for the selected FP
        for i, fp in enumerate(foreign_vat_fpos.ids):
            report_lines = self.basic_tax_report_2\
                .with_company(company_2)._get_lines(report_options[fp])
            self.assertLinesValues(
                # pylint: disable=bad-whitespace
                report_lines,
                [0,                               1],
                [
                    ('test_line_1',            0.0),
                    ('test_line_2', 1000 * (i + 1)),
                ],
                report_options[fp],
            )

        # the default values shouldn't be created if the general lock date is set
        lock_date_wizard = self.env['account.change.lock.date']\
            .with_company(company_2).create({
            'fiscalyear_lock_date': fields.Date.from_string('2020-01-04'),
        })
        lock_date_wizard.change_lock_date()

        report_lines = self.basic_tax_report_2\
            .with_company(company_2)._get_lines(report_options['all'])
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [0,                             1],
            [
                ('test_line_1',           0.0),
                ('test_line_2',  total_amount),
            ],
            report_options['all'],
        )

        # We need to reset the fiscalyear_lock_date or it will raise an exception
        company_2.fiscalyear_lock_date = False

        # if we change the tax_lock_date, the default values should be created
        lock_date_wizard = self.env['account.change.lock.date']\
            .with_company(company_2).create({
            'tax_lock_date': fields.Date.from_string('2020-01-04'),
        })
        lock_date_wizard.change_lock_date()

        report_lines = self.basic_tax_report_2\
            .with_company(company_2)._get_lines(report_options['all'])
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [0,                             1],
            [
                ('test_line_1',  total_amount),
                ('test_line_2',  total_amount),
            ],
            report_options['all'],
        )

        for i, fp in enumerate(foreign_vat_fpos.ids):
            report_lines = self.basic_tax_report_2\
                .with_company(company_2)._get_lines(report_options[fp])
            self.assertLinesValues(
                # pylint: disable=bad-whitespace
                report_lines,
                [0,                               1],
                [
                    ('test_line_1', 1000 * (i + 1)),
                    ('test_line_2', 1000 * (i + 1)),
                ],
                report_options[fp],
            )

    def test_tax_report_w_rounding_line(self):
        """Check that the tax report is correct when a rounding line is added to an invoice."""
        self.env['res.config.settings'].create({
            'company_id': self.company_data['company'].id,
            'group_cash_rounding': True
        })

        rounding = self.env['account.cash.rounding'].create({
            'name': 'Test rounding',
            'rounding': 0.05,
            'strategy': 'biggest_tax',
            'rounding_method': 'HALF-UP',
        })

        tax = self.sale_tax_percentage_incl_1.copy({
            'name': 'The Tax Who Says Ni',
            'amount': 21,
        })

        invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_a.id,
            'invoice_line_ids': [
                Command.create({
                    'name': 'The Holy Grail',
                    'quantity': 1,
                    'price_unit': 1.26,
                    'tax_ids': [Command.set(self.sale_tax_percentage_incl_1.ids)],
                }),
                Command.create({
                    'name': 'What is your favourite colour?',
                    'quantity': 1,
                    'price_unit': 2.32,
                    'tax_ids': [Command.set(tax.ids)],
                })
            ],
            'invoice_cash_rounding_id': rounding.id,
        })

        invoice.action_post()

        self.assertRecordValues(invoice.line_ids, [
            {
                'name': 'The Holy Grail',
                'debit': 0.00,
                'credit': 1.05,
            },
            {
                'name': 'What is your favourite colour?',
                'debit': 0.00,
                'credit': 1.92,
            },
            {
                'name': self.sale_tax_percentage_incl_1.name,
                'debit': 0.00,
                'credit': 0.21,
            },
            {
                'name': tax.name,
                'debit': 0.00,
                'credit': 0.40,
            },
            {
                'name': f'{tax.name} (rounding)',
                'debit': 0.00,
                'credit': 0.02,
            },
            {
                'name': invoice.name,
                'debit': 3.60,
                'credit': 0.00,
            }
        ])

        report = self.env.ref('account.generic_tax_report')
        options = self._generate_options(report, invoice.date, invoice.date)

        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                                                                         Base      Tax
            [   0,                                                                                           1,        2],
            [
                ('Sales',                                                                                   "",     0.63),
                (f'{self.sale_tax_percentage_incl_1.name} ({self.sale_tax_percentage_incl_1.amount}%)',   1.05,     0.21),
                (f'{tax.name} ({tax.amount}%)',                                                           1.92,     0.42),
                ('Total Sales',                                                                            "",      0.63),
            ],
            options
        )

        report = self.env.ref("account.generic_tax_report_account_tax")
        options['report_id'] = report.id

        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                                                                         Base      Tax
            [   0,                                                                                           1,        2],
            [
                ('Sales',                                                                                   "",     0.63),
                (self.company_data['default_account_revenue'].display_name,                                 "",     0.63),
                (f'{self.sale_tax_percentage_incl_1.name} ({self.sale_tax_percentage_incl_1.amount}%)',   1.05,     0.21),
                (f'{tax.name} ({tax.amount}%)',                                                           1.92,     0.42),
                (f'Total {self.company_data["default_account_revenue"].display_name}',                      "",     0.63),
                ('Total Sales',                                                                             "",     0.63),
            ],
            options
        )

        report = self.env.ref("account.generic_tax_report_tax_account")
        options['report_id'] = report.id

        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                                                                               Base      Tax
            [   0,                                                                                                 1,        2],
            [
                ('Sales',                                                                                         "",     0.63),
                (f'{self.sale_tax_percentage_incl_1.name} ({self.sale_tax_percentage_incl_1.amount}%)',           "",     0.21),
                (self.company_data['default_account_revenue'].display_name,                                     1.05,     0.21),
                (f'Total {self.sale_tax_percentage_incl_1.name} ({self.sale_tax_percentage_incl_1.amount}%)',     "",     0.21),
                (f'{tax.name} ({tax.amount}%)',                                                                   "",     0.42),
                (self.company_data['default_account_revenue'].display_name,                                     1.92,     0.42),
                (f'Total {tax.name} ({tax.amount}%)',                                                             "",     0.42),
                ('Total Sales',                                                                                   "",     0.63),
            ],
            options
        )

    def test_tax_report_closing_entry_reset_to_draft(self):
        """
        Test the reset to draft functionality to ensure no duplicate closing entry is created.

        This test checks that when a tax report closing entry is posted and then reset to draft,
        creating a subsequent closing entry will not result in a duplicate. Instead, the same
        initial closing entry will be reused.
        """
        options = self._generate_options(self.basic_tax_report, '2021-03-01', '2021-03-31')
        vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        initial_closing_entry = self.env['account.move'].browse(vat_closing_action['res_id'])
        with self.enter_test_mode():
            initial_closing_entry.action_post()
        initial_closing_entry.button_draft()
        vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        subsequent_closing_entry  = self.env['account.move'].browse(vat_closing_action['res_id'])
        self.assertEqual(initial_closing_entry, subsequent_closing_entry)

    def test_tax_report_closing_entry_draft_with_new_entries(self):
        """
        Test whether the tax closing entry gets untouched when reset to draft and the VAT closing button is clicked again.
        """
        options = self._generate_options(self.basic_tax_report, '2023-01-01', '2023-03-31')
        self.init_invoice('out_invoice', partner=self.partner_a, invoice_date='2023-03-22', post=True, amounts=[200], taxes=self.tax_sale_a)
        initial_vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        initial_closing_entry = self.env['account.move'].browse(initial_vat_closing_action['res_id'])
        initial_values = []
        for aml in initial_closing_entry.line_ids:
            self.assertEqual(aml.balance, 30 if aml.balance > 0 else -30)
            initial_values.append({'account_id': aml.account_id.id, 'balance': aml.balance})
        self.init_invoice('out_invoice', partner=self.partner_a, invoice_date='2023-03-22', post=True, amounts=[1000], taxes=self.tax_sale_a)
        subsequent_vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        subsequent_closing_entry  = self.env['account.move'].browse(subsequent_vat_closing_action['res_id'])
        self.assertRecordValues(subsequent_closing_entry.line_ids, [
            {'account_id': initial_values[0]['account_id'], 'balance': initial_values[0]['balance']},
            {'account_id': initial_values[1]['account_id'], 'balance': initial_values[1]['balance']},
        ])

    def test_tax_report_multi_company_post_closing(self):
        # Branches
        root_company = self.setup_other_company(name="Root Company")['company']
        branch_1 = self.env['res.company'].create({'name': "Branch 1", 'parent_id': root_company.id})
        branch_1_1 = self.env['res.company'].create({'name': "Branch 1.1", 'parent_id': branch_1.id})
        branch_2 = self.env['res.company'].create({'name': "Branch 2", 'parent_id': root_company.id})
        branch_companies = root_company + branch_1 + branch_1_1 + branch_2
        branch_companies.account_tax_periodicity_journal_id = root_company.account_tax_periodicity_journal_id.id

        # Tax unit
        unit_part_1 = self.setup_other_company(name="Unit part 1")['company']
        unit_part_2 = self.setup_other_company(name="Unit part 2")['company']

        tax_unit = self.env['account.tax.unit'].create({
            'name': "One unit to rule them all",
            'country_id': unit_part_1.account_fiscal_country_id.id,
            'vat': "123",
            'company_ids': (unit_part_1 + unit_part_2).ids,
            'main_company_id': unit_part_1.id,
        })

        tax_report = self.env['account.report'].create({
            'name': "My Onw Particular Tax Report",
            'country_id': unit_part_1.account_fiscal_country_id.id,
            'root_report_id': self.env.ref("account.generic_tax_report").id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance',})],
        })

        for test_type, main_company, active_companies in [('branches', root_company, branch_companies), ('tax units', tax_unit.main_company_id, tax_unit.company_ids)]:
            with self.subTest(f"Post multicompany closing - {test_type}"):
                tax_report_with_companies = tax_report.with_context(allowed_company_ids=active_companies.ids)
                options = self._generate_options(tax_report_with_companies, '2023-01-01', '2023-01-01')
                closing_moves = self.env['account.generic.tax.report.handler'].with_context(allowed_company_ids=active_companies.ids)._generate_tax_closing_entries(tax_report_with_companies, options)

                self.assertEqual(len(closing_moves), len(active_companies), "One closing move should have been created per company")
                self.assertTrue(all(move.state == 'draft' for move in closing_moves), "All generated closing moves should be in draft")
                main_closing_move = closing_moves.filtered(lambda x: x.company_id == main_company)
                self.assertEqual(len(main_closing_move), 1)

                with self.enter_test_mode():
                    action = main_closing_move.action_post()
                    self.assertTrue(action['params']['depending_action'])
                    # When posting the main closing move a component will open to propose you to post the depending moves.
                    # So while the depending moves are not posted the main closing will not be posted.
                    self.assertTrue(main_closing_move.state == 'draft')
                    (closing_moves - main_closing_move).action_post()
                    self.assertTrue(all(move.state == 'posted' for move in (closing_moves - main_closing_move)))
                    main_closing_move.action_post()
                    self.assertTrue(main_closing_move.state == 'posted')
    def test_tax_report_prevent_draft_if_subsequent_posted(self):
        """
        Test the reset to draft functionality to ensure it is not possible to reset to draft a closing entry
        if subsequent closing entries are already posted.
        """
        options = self._generate_options(self.basic_tax_report, '2023-01-01', '2023-03-31')
        vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        Q1_closing_entry = self.env['account.move'].browse(vat_closing_action['res_id'])
        with self.enter_test_mode():
            Q1_closing_entry.action_post()

        options = self._generate_options(self.basic_tax_report, '2023-04-01', '2023-06-30')
        vat_closing_action = self.env['account.generic.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(options)
        Q2_closing_entry = self.env['account.move'].browse(vat_closing_action['res_id'])

        # We need to force recompute the entry as it is already generated from posting the Q1 entry.
        Q2_closing_entry = self.env['account.generic.tax.report.handler']._generate_tax_closing_entries(self.basic_tax_report, options, closing_moves=Q2_closing_entry)
        with self.enter_test_mode():
            Q2_closing_entry.action_post()

        with self.assertRaises(UserError):
            Q1_closing_entry.button_draft()

    def assert_period(self, input_date, expected_start, expected_end):
        period_start, period_end = self.env.company._get_tax_closing_period_boundaries(input_date, self.basic_tax_report)
        self.assertEqual(period_start, expected_start, f"Period start date ({fields.Date.to_string(period_start)}) doesn't match the expected period start date: ({fields.Date.to_string(expected_start)})")
        self.assertEqual(period_end, expected_end, f"Period end date ({fields.Date.to_string(period_end)}) doesn't match the expected period end date: ({fields.Date.to_string(expected_end)})")

    @freeze_time('2024-09-01')
    def test_tax_report_start_date(self):
        # Periodicity only with default start_date
        self.env.company.account_tax_periodicity = 'monthly'
        self.assert_period(date(2024, 1, 1), expected_start=date(2024, 1, 1), expected_end=date(2024, 1, 31))
        self.assert_period(date(2024, 9, 30), expected_start=date(2024, 9, 1), expected_end=date(2024, 9, 30))
        self.assert_period(date(2024, 10, 1), expected_start=date(2024, 10, 1), expected_end=date(2024, 10, 31))

        self.env.company.account_tax_periodicity = 'trimester'
        self.assert_period(date(2024, 1, 1), expected_start=date(2024, 1, 1), expected_end=date(2024, 3, 31))
        self.assert_period(date(2024, 5, 1), expected_start=date(2024, 4, 1), expected_end=date(2024, 6, 30))
        self.assert_period(date(2024, 9, 30), expected_start=date(2024, 7, 1), expected_end=date(2024, 9, 30))
        self.assert_period(date(2024, 10, 1), expected_start=date(2024, 10, 1), expected_end=date(2024, 12, 31))

        self.env.company.account_tax_periodicity = 'year'
        self.assert_period(date(2024, 1, 1), expected_start=date(2024, 1, 1), expected_end=date(2024, 12, 31))
        self.assert_period(date(2023, 12, 31), expected_start=date(2023, 1, 1), expected_end=date(2023, 12, 31))

        # Basic start dates
        self.env.company.account_tax_periodicity = 'trimester'
        self.basic_tax_report.tax_closing_start_date = '2024-01-01'
        self.assert_period(date(2024, 1, 1), expected_start=date(2024, 1, 1), expected_end=date(2024, 3, 31))
        self.assert_period(date(2024, 4, 1), expected_start=date(2024, 4, 1), expected_end=date(2024, 6, 30))
        self.assert_period(date(2024, 5, 1), expected_start=date(2024, 4, 1), expected_end=date(2024, 6, 30))
        self.assert_period(date(2024, 9, 30), expected_start=date(2024, 7, 1), expected_end=date(2024, 9, 30))
        self.assert_period(date(2024, 10, 1), expected_start=date(2024, 10, 1), expected_end=date(2024, 12, 31))

        self.basic_tax_report.tax_closing_start_date = '2024-02-01'
        self.assert_period(date(2024, 1, 1), expected_start=date(2023, 11, 1), expected_end=date(2024, 1, 31))
        self.assert_period(date(2024, 1, 31), expected_start=date(2023, 11, 1), expected_end=date(2024, 1, 31))
        self.assert_period(date(2024, 2, 1), expected_start=date(2024, 2, 1), expected_end=date(2024, 4, 30))
        self.assert_period(date(2024, 6, 1), expected_start=date(2024, 5, 1), expected_end=date(2024, 7, 31))
        self.assert_period(date(2024, 10, 31), expected_start=date(2024, 8, 1), expected_end=date(2024, 10, 31))
        self.assert_period(date(2024, 11, 1), expected_start=date(2024, 11, 1), expected_end=date(2025, 1, 31))

        self.env.company.account_tax_periodicity = 'monthly'
        self.assert_period(date(2024, 2, 1), expected_start=date(2024, 2, 1), expected_end=date(2024, 2, 29))
        self.assert_period(date(2024, 1, 31), expected_start=date(2024, 1, 1), expected_end=date(2024, 1, 31))
        self.assert_period(date(2024, 1, 1), expected_start=date(2024, 1, 1), expected_end=date(2024, 1, 31))
        self.assert_period(date(2024, 4, 1), expected_start=date(2024, 4, 1), expected_end=date(2024, 4, 30))
        self.assert_period(date(2024, 12, 31), expected_start=date(2024, 12, 1), expected_end=date(2024, 12, 31))
        self.assert_period(date(2024, 12, 1), expected_start=date(2024, 12, 1), expected_end=date(2024, 12, 31))

        # Complexe start dates
        self.env.company.account_tax_periodicity = 'trimester'

        self.basic_tax_report.tax_closing_start_date = '2024-02-06'
        self.assert_period(date(2024, 2, 5), expected_start=date(2023, 11, 6), expected_end=date(2024, 2, 5))
        self.assert_period(date(2024, 2, 1), expected_start=date(2023, 11, 6), expected_end=date(2024, 2, 5))
        self.assert_period(date(2023, 11, 7), expected_start=date(2023, 11, 6), expected_end=date(2024, 2, 5))

        self.assert_period(date(2024, 2, 6), expected_start=date(2024, 2, 6), expected_end=date(2024, 5, 5))
        self.assert_period(date(2024, 5, 5), expected_start=date(2024, 2, 6), expected_end=date(2024, 5, 5))
        self.assert_period(date(2024, 4, 5), expected_start=date(2024, 2, 6), expected_end=date(2024, 5, 5))

        self.assert_period(date(2024, 5, 6), expected_start=date(2024, 5, 6), expected_end=date(2024, 8, 5))
        self.assert_period(date(2024, 11, 5), expected_start=date(2024, 8, 6), expected_end=date(2024, 11, 5))
        self.assert_period(date(2024, 11, 6), expected_start=date(2024, 11, 6), expected_end=date(2025, 2, 5))

        self.basic_tax_report.tax_closing_start_date = '2024-06-06'
        self.assert_period(date(2024, 3, 5), expected_start=date(2023, 12, 6), expected_end=date(2024, 3, 5))
        self.assert_period(date(2024, 6, 5), expected_start=date(2024, 3, 6), expected_end=date(2024, 6, 5))
        self.assert_period(date(2024, 9, 5), expected_start=date(2024, 6, 6), expected_end=date(2024, 9, 5))
        self.assert_period(date(2024, 12, 5), expected_start=date(2024, 9, 6), expected_end=date(2024, 12, 5))

        self.env.company.account_tax_periodicity = 'monthly'
        self.assert_period(date(2024, 3, 5), expected_start=date(2024, 2, 6), expected_end=date(2024, 3, 5))
        self.assert_period(date(2024, 3, 6), expected_start=date(2024, 3, 6), expected_end=date(2024, 4, 5))
        self.assert_period(date(2024, 12, 5), expected_start=date(2024, 11, 6), expected_end=date(2024, 12, 5))
        self.assert_period(date(2024, 12, 6), expected_start=date(2024, 12, 6), expected_end=date(2025, 1, 5))
        self.assert_period(date(2025, 1, 5), expected_start=date(2024, 12, 6), expected_end=date(2025, 1, 5))

    def test_multiple_same_tax_lines_with_multiple_analytics(self):
        """ One Invoice line with analytic_distribution and another with another analytic_distribution, both with the same tax"""
        analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan with Tax details'})
        analytic_account_1 = self.env['account.analytic.account'].create({
            'name': 'Analytic account with Tax details',
            'plan_id': analytic_plan.id,
            'company_id': False,
        })
        analytic_account_2 = self.env['account.analytic.account'].create({
            'name': ' testAnalytic account',
            'plan_id': analytic_plan.id,
            'company_id': False,
        })
        tax_10 = self.env['account.tax'].create({
            'name': "tax_10",
            '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': 200.0,
                    'tax_ids': [Command.set(tax_10.ids)],
                    'analytic_distribution': {
                        analytic_account_1.id: 100,
                    },
                }),
                Command.create({
                    'name': 'line2',
                    'account_id': self.company_data['default_account_revenue'].id,
                    'price_unit': 100.0,
                    'tax_ids': [Command.set(tax_10.ids)],
                    'analytic_distribution': {
                        analytic_account_2.id: 100,
                    },
                }),
            ]
        })
        invoice.action_post()
        invoice2 = 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': 10.0,
                    'tax_ids': [Command.set(tax_10.ids)],
                    'analytic_distribution': {
                        analytic_account_2.id: 100,
                    },
                }),
            ]
        })
        invoice2.action_post()

        self.assertRecordValues(invoice.line_ids, [
            {'name': 'line1', 'debit': 0.00, 'credit': 200.0},
            {'name': 'line2', 'debit': 0.00, 'credit': 100.0},
            {'name': tax_10.name, 'debit': 0.00, 'credit': 20.0},
            {'name': tax_10.name, 'debit': 0.00, 'credit': 10.0},
            {'name': invoice.name, 'debit': 330.0, 'credit': 0.00}
        ])

        self.assertRecordValues(invoice2.line_ids, [
            {'name': 'line1', 'debit': 0.00, 'credit': 10.0},
            {'name': tax_10.name, 'debit': 0.00, 'credit': 1.0},
            {'name': invoice2.name, 'debit': 11.0, 'credit': 0.00}
        ])

        report = self.env.ref('account.generic_tax_report_account_tax')
        options = self._generate_options(report, invoice.date, invoice.date)

        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                                                          Base      Tax
            [   0,                                                                             1,        2],
            [
                ('Sales',                                                                     "",     31.0),
                (self.company_data['default_account_revenue'].display_name,                   "",     31.0),
                (f'{tax_10.name} ({tax_10.amount}%)',                                      310.0,     31.0),
                (f'Total {self.company_data["default_account_revenue"].display_name}',        "",     31.0),
                ('Total Sales',                                                               "",     31.0),
            ],
            options
        )

        report = self.env.ref("account.generic_tax_report_tax_account")
        options['report_id'] = report.id

        self.assertLinesValues(
            report._get_lines(options),
            #   Name                                                                           Base      Tax
            [   0,                                                                              1,        2],
            [
                ('Sales',                                                                      "",     31.0),
                (f'{tax_10.name} ({tax_10.amount}%)',                                          "",     31.0),
                (self.company_data['default_account_revenue'].display_name,                 310.0,     31.0),
                (f'Total {tax_10.name} ({tax_10.amount}%)',                                    "",     31.0),
                ('Total Sales',                                                                "",     31.0),
            ],
            options
        )

    def test_only_one_closing_entry_with_generic_reports(self):
        """ Test that when multiple generic tax reports are used, only one closing entry is created and used by all reports.
            This is to prevent multiple identical closing entries to be created when multiple tax reports are used.
        """
        generic_tax_report = self.env.ref("account.generic_tax_report")
        options = self._generate_options(generic_tax_report, '2021-03-01', '2021-03-31')
        vat_closing_action = generic_tax_report.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        generic_tax_report_closing_moves = self.env['account.move'].browse(vat_closing_action['domain'][0][2])

        generic_tax_report_account_tax = self.env.ref('account.generic_tax_report_account_tax')
        options = self._generate_options(generic_tax_report_account_tax, '2021-03-01', '2021-03-31')
        vat_closing_action = generic_tax_report_account_tax.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        generic_tax_report_account_tax_closing_moves = self.env['account.move'].browse(vat_closing_action['domain'][0][2])

        generic_tax_report_tax_account = self.env.ref('account.generic_tax_report_tax_account')
        options = self._generate_options(generic_tax_report_tax_account, '2021-03-01', '2021-03-31')
        vat_closing_action = generic_tax_report_tax_account.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        generic_tax_report_tax_account_closing_moves = self.env['account.move'].browse(vat_closing_action['domain'][0][2])

        # We have 2 fiscal positions here, the domestic (no fiscal position) and foreign_vat_fpos.
        self.assertEqual(len(generic_tax_report_closing_moves), 2, "There should be one closing entry per fiscal position.")
        self.assertTrue(generic_tax_report_closing_moves.filtered(lambda m: not m.fiscal_position_id), "There should be a closing entry for the domestic fiscal position (no fiscal position).")
        self.assertTrue(generic_tax_report_closing_moves.filtered(lambda m: m.fiscal_position_id == self.foreign_vat_fpos), "There should be a closing entry for the foreign VAT fiscal position.")
        self.assertEqual(generic_tax_report_closing_moves, generic_tax_report_account_tax_closing_moves, "The closing entries used for both reports should be the same.")
        self.assertEqual(generic_tax_report_closing_moves, generic_tax_report_tax_account_closing_moves, "The closing entries used for both reports should be the same.")
        self.assertEqual(generic_tax_report_closing_moves.mapped('tax_closing_report_id'), self.basic_tax_report, "The closing entries should be linked to the basic tax report.")

    def test_only_one_closing_entry_per_fpos(self):
        """ Test that when different fiscal positions with foreign VAT numbers are used, only one closing entry per fiscal position is created and used by all reports.
            This is to prevent multiple identical closing entries to be created when multiple fiscal positions are used.
            We also test that the closing entry used for a report is the one corresponding to the fiscal position of the report's country.
        """
        generic_tax_report = self.env.ref("account.generic_tax_report")
        other_country = self.env['res.country'].create({'name': 'Other Country', 'code': 'OT'})
        other_country_vat_fpos = self.env['account.fiscal.position'].create({
            'name': "Foreign VAT",
            'country_id': other_country.id,
            'foreign_vat': 'ZZ0000000',
        })

        basic_other_country_tax_report = self.env['account.report'].create({
            'name': "The Unseen Tax Report",
            'country_id': other_country.id,
            'root_report_id': generic_tax_report.id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        # Another report to test that when multiple variants are available for the same country, we use the generic tax report.
        self.env['account.report'].create({
            'name': "The Other Unseen Tax Report",
            'country_id': other_country.id,
            'root_report_id': generic_tax_report.id,
            'column_ids': [Command.create({'name': 'balance', 'sequence': 1, 'expression_label': 'balance'})],
        })

        other_country_partner = self.env['res.partner'].create({
            'name': "Another Country Partner",
            'country_id': other_country.id,
        })

        self.init_invoice('out_invoice', partner=other_country_partner, invoice_date='2021-03-15', post=True, amounts=[23000], taxes=self.test_fpos_tax_sale)
        self.init_invoice('in_invoice', partner=other_country_partner, invoice_date='2021-03-20', post=True, amounts=[300], taxes=self.test_fpos_tax_purchase)
        self.init_invoice('out_invoice', partner=other_country_partner, invoice_date='2021-03-25', post=True, amounts=[700], taxes=self.test_fpos_tax_sale)
        self.init_invoice('in_invoice', partner=other_country_partner, invoice_date='2021-03-28', post=True, amounts=[1300], taxes=self.test_fpos_tax_purchase)

        options = self._generate_options(generic_tax_report, '2021-03-01', '2021-03-31')
        vat_closing_action = generic_tax_report.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        generic_closing_moves = self.env['account.move'].browse(vat_closing_action['domain'][0][2])

        generic_closing_move_domestic = generic_closing_moves.filtered(lambda m: not m.fiscal_position_id)
        generic_closing_move_foreign = generic_closing_moves.filtered(lambda m: m.fiscal_position_id == other_country_vat_fpos)

        options = self._generate_options(self.basic_tax_report, '2021-03-01', '2021-03-31')
        vat_closing_action = self.basic_tax_report.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        domestic_closing_move = self.env['account.move'].browse(vat_closing_action['res_id'])

        options = self._generate_options(basic_other_country_tax_report, '2021-03-01', '2021-03-31')
        vat_closing_action = basic_other_country_tax_report.with_context({'override_tax_closing_warning': True}).dispatch_report_action(options, 'action_periodic_vat_entries')
        other_country_closing_move = self.env['account.move'].browse(vat_closing_action['res_id'])

        self.assertNotEqual(generic_closing_move_domestic, generic_closing_move_foreign, "Domestic and foreign closing entries should be different.")
        self.assertEqual(generic_closing_move_domestic, domestic_closing_move, "The domestic closing entry should be the one used for the basic tax report.")
        self.assertEqual(generic_closing_move_foreign, other_country_closing_move, "The foreign closing entry should be the one used for the other country tax report.")
        self.assertEqual(generic_closing_move_domestic.tax_closing_report_id, self.basic_tax_report, "The domestic closing entry should be linked to the domestic tax report.")
        self.assertEqual(generic_closing_move_foreign.tax_closing_report_id, generic_tax_report, "The foreign closing entry should be linked to the generic tax report.")
