from odoo import _, models
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import FloatFmt
from odoo.tools import frozendict


class AccountEdiUBL(models.AbstractModel):
    _name = "account.edi.ubl"
    _inherit = 'account.edi.common'
    _description = "Base helpers for UBL"

    # -------------------------------------------------------------------------
    # BASE LINES HELPERS
    # -------------------------------------------------------------------------

    def _ubl_is_recycling_contribution_tax(self, tax_data):
        """ Indicate if the 'tax_data' passed as parameter is a recycling contribution tax.

        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :return:            True if tax_data['tax'] is a recycling contribution tax, False otherwise.
        """
        if not tax_data:
            return False

        tax = tax_data['tax']
        return tax.amount_type == 'fixed' and tax.include_base_amount

    def _ubl_is_excise_tax(self, tax_data):
        """ Indicate if the 'tax_data' passed as parameter is an excise tax.

        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :return:            True if tax_data['tax'] is an excise tax, False otherwise.
        """
        if not tax_data:
            return False

        tax = tax_data['tax']
        return tax.amount_type == 'code' and tax.include_base_amount

    def _ubl_is_reverse_charge_tax(self, tax_data):
        """ Indicate if the 'tax_data' passed as parameter is an intracommunity reverse charge purchase tax.

        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :return:            True if tax_data['tax'] is an intracommunity reverse charge purchase tax, False otherwise.
        """
        if not tax_data:
            return False

        tax = tax_data['tax']
        return tax.amount_type == 'percent' and tax.has_negative_factor

    def _ubl_is_early_payment_base_line(self, base_line):
        """ Indicate if the 'base_line' passed as parameter has been generated by an 'mixed' early payment.

        :param      base_line: A base line (see '_prepare_base_line_for_taxes_computation').
        :return:    True if the 'base_line' is a 'mixed' early payment line, False otherwise.
        """
        return base_line['special_type'] == 'early_payment'

    def _ubl_is_cash_rounding_base_line(self, base_line):
        """ Indicate if the 'base_line' passed as parameter has been generated by a cash rounding method.

        :param      base_line: A base line (see '_prepare_base_line_for_taxes_computation').
        :return:    True if the 'base_line' is a cash rounding line, False otherwise.
        """
        return base_line['special_type'] == 'cash_rounding'

    def _ubl_default_tax_category_grouping_key(self, base_line, tax_data, vals, currency):
        """ Give the values about the tax category for a given tax.

        :param base_line:   A base line (see '_prepare_base_line_for_taxes_computation').
        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :param vals:        Some custom data.
        :param currency:    The currency for which the grouping key is expressed.
        :return:            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        customer = vals['customer']
        supplier = vals['supplier']
        if tax_data and (
            tax_data['tax'].amount_type != 'percent'
            or self._ubl_is_recycling_contribution_tax(tax_data)
            or self._ubl_is_excise_tax(tax_data)
        ):
            return
        elif self._ubl_is_reverse_charge_tax(tax_data):
            # Reverse-charge taxes with +100/-100% repartition lines are used in vendor bills.
            # In self-billed invoices, we report them from the seller's perspective, as 0% taxes.
            tax = tax_data['tax']
            return {
                'tax_category_code': self._get_tax_category_code(customer.commercial_partner_id, supplier, tax),
                **self._get_tax_exemption_reason(customer.commercial_partner_id, supplier, tax),
                'percent': 0.0,
                'scheme_id': 'VAT',
                'is_withholding': False,
                'currency': currency,
            }
        elif tax_data:
            tax = tax_data['tax']
            return {
                'tax_category_code': self._get_tax_category_code(customer.commercial_partner_id, supplier, tax),
                **self._get_tax_exemption_reason(customer.commercial_partner_id, supplier, tax),
                'percent': tax.amount,
                'scheme_id': 'VAT',
                'is_withholding': tax.amount < 0.0,
                'currency': currency,
            }
        else:
            return {
                'tax_category_code': self._get_tax_category_code(customer.commercial_partner_id, supplier, self.env['account.tax']),
                **self._get_tax_exemption_reason(customer.commercial_partner_id, supplier, self.env['account.tax']),
                'percent': 0.0,
                'scheme_id': 'VAT',
                'is_withholding': False,
                'currency': currency,
            }

    def _ubl_default_tax_subtotal_tax_category_grouping_key(self, tax_grouping_key, vals):
        """ Give the values about how taxes are grouped together in TaxTotal -> TaxSubtotal -> TaxCategory
        (or WithholdingTaxTotal depending on 'is_withholding').

        :param tax_grouping_key:            The grouping key returned by '_ubl_default_tax_category_grouping_key'.
        :param vals:                        Some custom data.
        :return:                            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        return dict(tax_grouping_key)

    def _ubl_default_tax_subtotal_grouping_key(self, tax_category_grouping_key, vals):
        """ Give the values about how taxes are grouped together in TaxTotal -> TaxSubtotal
        (or WithholdingTaxTotal depending on 'is_withholding').

        :param tax_category_grouping_key:   The grouping key returned by '_ubl_default_tax_subtotal_tax_category_grouping_key'.
        :param vals:                        Some custom data.
        :return:                            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        return dict(tax_category_grouping_key)

    def _ubl_default_tax_total_grouping_key(self, tax_subtotal_grouping_key, vals):
        """ Give the values about how taxes are grouped together in TaxTotal
        (or WithholdingTaxTotal depending on 'is_withholding').

        :param tax_subtotal_grouping_key:   The grouping key returned by '_ubl_default_tax_subtotal_grouping_key'.
        :param vals:                        Some custom data.
        :return:                            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        return {
            'is_withholding': tax_subtotal_grouping_key['is_withholding'],
            'currency': tax_subtotal_grouping_key['currency'],
        }

    def _ubl_default_allowance_charge_early_payment_grouping_key(self, base_line, tax_data, vals, currency):
        """ Give the grouping key when generating the allowance/charge from an early payment discount.

        :param base_line:   A base line (see '_prepare_base_line_for_taxes_computation').
        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :param vals:        Some custom data.
        :param currency:    The currency for which the grouping key is expressed.
        :return:            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        if not self._ubl_is_early_payment_base_line(base_line):
            return

        tax_grouping_key = self._ubl_default_tax_category_grouping_key(base_line, tax_data, vals, currency)
        if not tax_grouping_key or tax_grouping_key['is_withholding']:
            return
        return tax_grouping_key

    def _ubl_default_payable_amount_tax_withholding_grouping_key(self, base_line, tax_data, vals, currency):
        """ Give the grouping key when moving the tax withholding amounts to PrepaidAmount.

        :param base_line:   A base line (see '_prepare_base_line_for_taxes_computation').
        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :param vals:        Some custom data.
        :param currency:    The currency for which the grouping key is expressed.
        :return:            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        if not tax_data:
            return
        tax_grouping_key = self._ubl_default_tax_category_grouping_key(base_line, tax_data, vals, currency)
        if not tax_grouping_key:
            return
        return tax_grouping_key['is_withholding']

    def _ubl_default_base_line_item_classified_tax_category_grouping_key(self, base_line, tax_data, vals, currency):
        """ Give the grouping key when computing taxes for Item -> ClassifiedTaxCategory.

        :param base_line:   A base line (see '_prepare_base_line_for_taxes_computation').
        :param tax_data:    One of the tax data in base_line['tax_details']['taxes_data'].
        :param vals:        Some custom data.
        :param currency:    The currency for which the grouping key is expressed.
        :return:            A dictionary that could be used as a grouping key for the taxes helpers.
        """
        tax_grouping_key = self._ubl_default_tax_category_grouping_key(base_line, tax_data, vals, currency)
        if not tax_grouping_key or tax_grouping_key['is_withholding']:
            return
        return tax_grouping_key

    def _ubl_turn_base_lines_price_unit_as_always_positive(self, vals):
        """ Helper to make sure the base_lines don't contain any negative price_unit.

        :param vals: Some custom data.
        """
        for base_line in vals['base_lines']:
            if base_line['price_unit'] < 0.0:
                base_line['quantity'] *= -1
                base_line['price_unit'] *= -1

    def _ubl_turn_emptying_taxes_as_new_base_lines(self, base_lines, company, vals):
        """ Extract emptying taxes such as "Vidanges" on bottles from the current base lines and turn them into
        additional base lines.

        :param base_lines:  The original 'base_lines' of the document.
        :param company:     The company owning the 'base_lines'.
        :param vals:        Some custom data.
        """
        AccountTax = self.env['account.tax']

        def exclude_function(base_line, tax_data):
            if not tax_data:
                return

            tax = tax_data['tax']
            return tax.amount_type in ('fixed', 'code') and not tax.include_base_amount

        new_base_lines = AccountTax._dispatch_taxes_into_new_base_lines(base_lines, company, exclude_function)

        def aggregate_function(target_base_line, base_line):
            target_base_line.setdefault('_aggregated_quantity', 0.0)
            target_base_line['_aggregated_quantity'] += base_line['quantity']

        def grouping_function(base_line):
            return {'tax': base_line['_removed_tax_data']['tax']}

        extra_base_lines = AccountTax._turn_removed_taxes_into_new_base_lines(
            base_lines=new_base_lines,
            company=company,
            grouping_function=grouping_function,
            aggregate_function=aggregate_function,
        )

        # Restore back the values per quantity.
        for base_line in extra_base_lines:
            base_line['quantity'] = base_line['_aggregated_quantity']
            if base_line['_aggregated_quantity']:
                base_line['price_unit'] /= base_line['_aggregated_quantity']
            base_line['product_id'] = self.env['product.product']

        return new_base_lines + extra_base_lines

    # -------------------------------------------------------------------------
    # EXPORT: Collecting data
    # -------------------------------------------------------------------------

    def _ubl_add_base_line_ubl_values_allowance_charges_recycling_contribution(self, vals):
        """ Extract recycling contribution taxes such as RECUPEL, AUVIBEL, etc from the current base lines.
        Instead, add them under 'base_line' -> '_ubl_values' -> 'allowance_charges_recycling_contribution'
        to be reported as allowances/charges.

        From a 'base_line' having
            price_unit = 99
            tax_ids = RECUPEL of 1 + 21% tax
            total_excluded_currency = 99
            total_included_currency = 121
            taxes_data = [1, 21]
            recycling_contribution_data = []
        ... turn it to:
            price_unit = 99
            tax_ids = 21% tax
            total_excluded_currency = 99
            total_included_currency = 121
            taxes_data = [21]
            recycling_contribution_data = [1]

        TO BE REMOVED IN MASTER

        :param vals:        Some custom data.
        """
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        for base_line in base_lines:
            ubl_values = base_line['_ubl_values']
            tax_details = base_line['tax_details']
            taxes_data = tax_details['taxes_data']

            allowance_charges_recycling_contribution = ubl_values['allowance_charges_recycling_contribution'] = []
            allowance_charges_recycling_contribution_currency = ubl_values['allowance_charges_recycling_contribution_currency'] = []
            for tax_data in taxes_data:
                if self._ubl_is_recycling_contribution_tax(tax_data):
                    allowance_charges_recycling_contribution.append({
                        'tax': tax_data['tax'],
                        'is_charge': tax_data['tax_amount'] > 0.0,
                        'amount': tax_data['tax_amount'],
                        'currency': company_currency,
                    })
                    allowance_charges_recycling_contribution_currency.append({
                        'tax': tax_data['tax'],
                        'is_charge': tax_data['tax_amount_currency'] > 0.0,
                        'amount': tax_data['tax_amount_currency'],
                        'currency': currency,
                    })

    def _ubl_add_base_line_ubl_values_allowance_charges_excise(self, vals):
        """ Extract excise taxes from the current base lines.
        Instead, add them under 'base_line' -> '_ubl_values' -> 'allowance_charges_excise'
        to be reported as allowances/charges.

        From a 'base_line' having
            price_unit = 99
            tax_ids = EXCISE of 1 + 21% tax
            total_excluded_currency = 99
            total_included_currency = 121
            taxes_data = [1, 21]
            recycling_contribution_data = []
        ... turn it to:
            price_unit = 99
            tax_ids = 21% tax
            total_excluded_currency = 99
            total_included_currency = 121
            taxes_data = [21]
            recycling_contribution_data = [1]

        TO BE REMOVED IN MASTER

        :param vals:        Some custom data.
        """
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        for base_line in base_lines:
            ubl_values = base_line['_ubl_values']
            tax_details = base_line['tax_details']
            taxes_data = tax_details['taxes_data']

            allowance_charges_excise = ubl_values['allowance_charges_excise'] = []
            allowance_charges_excise_currency = ubl_values['allowance_charges_excise_currency'] = []
            for tax_data in taxes_data:
                if self._ubl_is_excise_tax(tax_data):
                    allowance_charges_excise.append({
                        'tax': tax_data['tax'],
                        'is_charge': tax_data['tax_amount'] > 0.0,
                        'amount': tax_data['tax_amount'],
                        'currency': company_currency,
                    })
                    allowance_charges_excise_currency.append({
                        'tax': tax_data['tax'],
                        'is_charge': tax_data['tax_amount_currency'] > 0.0,
                        'amount': tax_data['tax_amount_currency'],
                        'currency': currency,
                    })

    def _ubl_add_base_line_ubl_values_allowance_charges_discount(self, vals):
        """ Extract the amount implies by a discount. This amount will be turned into an allowances/charge
        into 'base_line' -> '_ubl_values' -> 'allowance_charge_discount'.

        From a 'base_line' having
            price_unit = 100
            quantity = 5
            discount = 20
            total_excluded_currency = (5 * 100) * 0.8 = 400
        ... compute an 'allowance_charge_discount' or (5 * 100) - 400 = 100:

        TO BE REMOVED IN MASTER

        :param vals:        Some custom data.
        """
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        for base_line in base_lines:
            ubl_values = base_line['_ubl_values']
            tax_details = base_line['tax_details']
            raw_discount_amount_currency = tax_details['raw_discount_amount_currency']
            raw_discount_amount = tax_details['raw_discount_amount']

            if (
                base_line['currency_id'].is_zero(raw_discount_amount_currency)
                and company.currency_id.is_zero(raw_discount_amount)
            ):
                ubl_values['allowance_charge_discount'] = None
                ubl_values['allowance_charge_discount_currency'] = None
            else:
                ubl_values['allowance_charge_discount'] = {
                    'currency': company_currency,
                    'percent': base_line['discount'],
                    'is_charge': raw_discount_amount < 0.0,
                    'amount': raw_discount_amount,
                    'base_amount': tax_details['raw_gross_total_excluded'],
                }
                ubl_values['allowance_charge_discount_currency'] = {
                    'currency': currency,
                    'percent': base_line['discount'],
                    'amount': raw_discount_amount_currency,
                    'is_charge': raw_discount_amount_currency < 0.0,
                    'base_amount': tax_details['raw_gross_total_excluded_currency'],
                }

    def _ubl_add_base_line_ubl_values_line_extension_amount(self, vals, use_company_currency=False):
        """ Add 'base_line' -> '_ubl_values' -> 'line_extension_amount[_currency]'.

        'line_extension_amount' is the subtotal of the line but without tax plus charges.

        TO BE REMOVED IN MASTER

        :param vals:                    Some custom data.
        :param use_company_currency:    Express the amount in company currency.
        """
        base_lines = vals['base_lines']
        suffix = '' if use_company_currency else '_currency'

        for base_line in base_lines:
            tax_details = base_line['tax_details']
            ubl_values = base_line['_ubl_values']
            amount = (
                tax_details[f'total_excluded{suffix}']
                + tax_details[f'delta_total_excluded{suffix}']
                + sum(
                    (1 if allowance_charge_values['is_charge'] else -1) * allowance_charge_values['amount']
                    for allowance_charge_values in ubl_values[f'allowance_charges_recycling_contribution{suffix}']
                )
                + sum(
                    (1 if allowance_charge_values['is_charge'] else -1) * allowance_charge_values['amount']
                    for allowance_charge_values in ubl_values[f'allowance_charges_excise{suffix}']
                )
            )
            ubl_values[f'line_extension_amount{suffix}'] = amount

    def _ubl_add_base_line_ubl_values_item(self, vals):
        """ Add 'base_line' -> '_ubl_values' -> 'item'.

        :param vals:        Some custom data.
        """
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        for sub_currency, suffix in ((currency, '_currency'), (company_currency, '')):
            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: self._ubl_default_base_line_item_classified_tax_category_grouping_key(
                    base_line=base_line,
                    tax_data=tax_data,
                    vals=vals,
                    currency=sub_currency,
                ),
            )
            for base_line, aggregated_values in base_lines_aggregated_values:
                item = base_line['_ubl_values'][f'item{suffix}'] = {
                    'currency': sub_currency,
                    'base_line': base_line,
                    'classified_tax_categories': {},
                }
                for grouping_key, values in aggregated_values.items():
                    if grouping_key:
                        item['classified_tax_categories'][grouping_key] = {
                            **grouping_key,
                            'base_amount': values[f'base_amount{suffix}'],
                            'tax_amount': values[f'tax_amount{suffix}'],
                        }

    def _ubl_add_base_line_ubl_values_price(self, vals):
        """ Add 'price_amount' under 'base_line' -> '_ubl_values' -> 'price_amount[_currency]'.

        'price_amount' is price unit of a single unit of the product.

        :param vals:        Some custom data.
        """
        base_lines = vals['base_lines']

        for base_line in base_lines:
            tax_details = base_line['tax_details']
            ubl_values = base_line['_ubl_values']
            for currency_suffix in ('_currency', ''):
                ubl_values[f'price_amount{currency_suffix}'] = tax_details[f'raw_gross_price_unit{currency_suffix}']

    def _ubl_add_values_tax_currency_code_company_currency_if_foreign_currency(self, vals):
        """ Add 'vals' -> '_ubl_values' -> 'tax_currency_code'

        The value is set only at the company currency when there is a foreign currency.

        :param vals:    Some custom data.
        """
        company = vals['company']
        currency = vals['currency_id']
        vals['tax_currency_code'] = None if currency == company.currency_id else company.currency_id.name

    def _ubl_add_values_tax_currency_code_company_currency(self, vals):
        """ Add 'vals' -> '_ubl_values' -> 'tax_currency_code'

        The company currency will always be set on it.

        :param vals:    Some custom data.
        """
        vals['tax_currency_code'] = vals['company'].currency_id.name

    def _ubl_add_values_tax_currency_code_empty(self, vals):
        """ Add 'vals' -> '_ubl_values' -> 'tax_currency_code'

        The value is empty.

        :param vals:    Some custom data.
        """
        vals['tax_currency_code'] = None

    def _ubl_add_values_tax_currency_code(self, vals):
        """ Add 'vals' -> '_ubl_values' -> 'tax_currency_code'

        :param vals:    Some custom data.
        """
        self._ubl_add_values_tax_currency_code_company_currency_if_foreign_currency(vals)

    def _ubl_add_values_tax_totals(self, vals):
        """ Add
            'vals' -> '_ubl_values' -> 'tax_totals'
            'vals' -> '_ubl_values' -> 'withholding_tax_totals'

        'tax_totals' will contain the total and subtotals for not-withholding taxes.
        'withholding_tax_totals' will contain the total and subtotals for withholding taxes.

        TO BE REMOVED IN MASTER

        :param vals:                        Some custom data.
        """
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        ubl_values = vals['_ubl_values']
        ubl_values['tax_totals'] = {}
        ubl_values['tax_totals_currency'] = {}
        ubl_values['withholding_tax_totals'] = {}
        ubl_values['withholding_tax_totals_currency'] = {}

        def tax_category_grouping_function(base_line, tax_data, sub_currency):
            tax_grouping_key = self._ubl_default_tax_category_grouping_key(base_line, tax_data, vals, sub_currency)
            if not tax_grouping_key:
                return
            return self._ubl_default_tax_subtotal_tax_category_grouping_key(tax_grouping_key, vals)

        def tax_subtotal_grouping_function(base_line, tax_data, sub_currency):
            tax_category_grouping_key = tax_category_grouping_function(base_line, tax_data, sub_currency)
            if not tax_category_grouping_key:
                return
            return self._ubl_default_tax_subtotal_grouping_key(tax_category_grouping_key, vals)

        def tax_totals_grouping_function(base_line, tax_data, sub_currency):
            tax_subtotal_grouping_key = tax_subtotal_grouping_function(base_line, tax_data, sub_currency)
            if not tax_subtotal_grouping_key:
                return
            return self._ubl_default_tax_total_grouping_key(tax_subtotal_grouping_key, vals)

        for sub_currency, suffix in ((currency, '_currency'), (company_currency, '')):

            # tax_totals / withholding_tax_totals

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: tax_totals_grouping_function(base_line, tax_data, sub_currency),
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                if grouping_key['is_withholding']:
                    target_key = f'withholding_tax_totals{suffix}'
                    sign = -1
                else:
                    target_key = f'tax_totals{suffix}'
                    sign = 1

                ubl_values[target_key][frozendict(grouping_key)] = {
                    **grouping_key,
                    'amount': sign * values[f'tax_amount{suffix}'],
                    'subtotals': {},
                }

            # tax_subtotals

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: tax_subtotal_grouping_function(base_line, tax_data, sub_currency),
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                if grouping_key['is_withholding']:
                    target_key = f'withholding_tax_totals{suffix}'
                    sign = -1
                else:
                    target_key = f'tax_totals{suffix}'
                    sign = 1

                tax_total_grouping_key = self._ubl_default_tax_total_grouping_key(grouping_key, vals)
                if not tax_total_grouping_key:
                    continue

                tax_total_values = ubl_values[target_key][frozendict(tax_total_grouping_key)]
                tax_total_values['subtotals'][frozendict(grouping_key)] = {
                    **grouping_key,
                    'base_amount': values[f'base_amount{suffix}'],
                    'tax_amount': sign * values[f'tax_amount{suffix}'],
                    'tax_categories': {},
                }

            # tax_categories

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: tax_category_grouping_function(base_line, tax_data, sub_currency),
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                if grouping_key['is_withholding']:
                    target_key = f'withholding_tax_totals{suffix}'
                    sign = -1
                else:
                    target_key = f'tax_totals{suffix}'
                    sign = 1

                tax_subtotal_grouping_key = self._ubl_default_tax_subtotal_grouping_key(grouping_key, vals)
                if not tax_subtotal_grouping_key:
                    continue

                tax_total_grouping_key = self._ubl_default_tax_total_grouping_key(tax_subtotal_grouping_key, vals)
                if not tax_total_grouping_key:
                    continue

                tax_total_values = ubl_values[target_key][frozendict(tax_total_grouping_key)]
                tax_total_values['subtotals'][frozendict(tax_subtotal_grouping_key)]['tax_categories'][frozendict(grouping_key)] = {
                    **grouping_key,
                    'base_amount': values[f'base_amount{suffix}'],
                    'tax_amount': sign * values[f'tax_amount{suffix}'],
                }

            for key in (f'withholding_tax_totals{suffix}', f'tax_totals{suffix}'):
                if not ubl_values[key]:
                    ubl_values[key][None] = {
                        'currency': sub_currency,
                        'amount': 0.0,
                        'subtotals': {},
                    }

    def _ubl_add_values_payable_amount_tax_withholding(self, vals):
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        ubl_values = vals['_ubl_values']
        ubl_values['payable_amount_tax_withholding'] = 0.0
        ubl_values['payable_amount_tax_withholding_currency'] = 0.0
        for sub_currency, suffix in ((currency, '_currency'), (company_currency, '')):
            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: self._ubl_default_payable_amount_tax_withholding_grouping_key(
                    base_line=base_line,
                    tax_data=tax_data,
                    vals=vals,
                    currency=sub_currency,
                ),
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)

            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                ubl_values[f'payable_amount_tax_withholding{suffix}'] -= values[f'tax_amount{suffix}']

    def _ubl_add_values_payable_rounding_amount(self, vals):
        """ Add
            'vals' -> '_ubl_values' -> 'payable_rounding_amount[_currency]'.
            'vals' -> '_ubl_values' -> 'payable_rounding_base_lines'.

        'payable_rounding_amount' is rounding amount to be added to the total in case of a cash rounding.
        'payable_rounding_base_lines' are the rounding base lines.

        :param vals:        Some custom data.
        """
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']

        def grouping_function(base_line, tax_data):
            return base_line['special_type'] == 'cash_rounding'

        base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, grouping_function)
        values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
        ubl_values = vals['_ubl_values']
        ubl_values['payable_rounding_amount'] = 0.0
        ubl_values['payable_rounding_amount_currency'] = 0.0
        ubl_values['payable_rounding_base_lines'] = []
        for grouping_key, values in values_per_grouping_key.items():
            if not grouping_key:
                continue

            ubl_values['payable_rounding_amount_currency'] += values['total_excluded_currency']
            ubl_values['payable_rounding_amount'] += values['total_excluded']
            for base_line, _taxes_data in values['base_line_x_taxes_data']:
                ubl_values['payable_rounding_base_lines'].append(base_line)

    def _ubl_add_values_allowance_charge_early_payment(self, vals):
        """ Add 'vals' -> '_ubl_values' -> 'allowance_charges_early_payment' representing the allowance/charges
        corresponding to a 'mixed' early payment.

        Suppose an invoice with a base amount of 100 and a 21% tax.
        The total of your invoice is 121.
        With a 'mixed' early payment of 5%, 2 additional lines are added to the invoice:
        One line having a negative amount of -5 with 21% tax.
        Another line having a positive amount of 5 with no tax.
        It means the 21% tax line will now be based on 95 instead of 100 leading to
        - an untaxed amount of 95.0
        - a tax amount of 95 * 0.21 = 19.95
        - a total amount of 114.95

        In the UBL, an allowance is added with an amount of 5 and 21% tax applied on it plus a charge with an amount of 5.
        Basically, it's like you had a discount on the full amount but we put back the discount you get on the base as a charge
        to only get the discount regarding the tax amount.

        :param vals:        Some custom data.
        """
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        ubl_values = vals['_ubl_values']

        for sub_currency, suffix in ((currency, '_currency'), (company_currency, '')):
            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=base_lines,
                grouping_function=lambda base_line, tax_data: self._ubl_default_allowance_charge_early_payment_grouping_key(
                    base_line=base_line,
                    tax_data=tax_data,
                    vals=vals,
                    currency=sub_currency,
                ),
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)

            allowance_charges_early_payment = ubl_values[f'allowance_charges_early_payment{suffix}'] = []
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                allowance_charges_early_payment.append({
                    'currency': sub_currency,
                    'amount': values[f'total_excluded{suffix}'],
                    'is_charge': values[f'total_excluded{suffix}'] > 0.0,
                    'tax_categories': {
                        grouping_key: {
                            **grouping_key,
                            'base_amount': values[f'base_amount{suffix}'],
                            'tax_amount': values[f'tax_amount{suffix}'],
                        },
                    },
                })

    # -------------------------------------------------------------------------
    # EXPORT: Building nodes
    # -------------------------------------------------------------------------

    def _ubl_add_line_quantity_node(self, vals):
        base_line = vals['line_vals']['base_line']
        vals['line_node']['cbc:Quantity'] = {
            '_text': base_line['quantity'],
            'unitCode': self._get_uom_unece_code(base_line['product_uom_id']),
        }

    def _ubl_add_line_invoiced_quantity_node(self, vals):
        base_line = vals['line_vals']['base_line']
        vals['line_node']['cbc:InvoicedQuantity'] = {
            '_text': base_line['quantity'],
            'unitCode': self._get_uom_unece_code(base_line['product_uom_id']),
        }

    def _ubl_add_line_credited_quantity_node(self, vals):
        base_line = vals['line_vals']['base_line']
        vals['line_node']['cbc:CreditedQuantity'] = {
            '_text': base_line['quantity'],
            'unitCode': self._get_uom_unece_code(base_line['product_uom_id']),
        }

    def _ubl_add_line_debited_quantity_node(self, vals):
        base_line = vals['line_vals']['base_line']
        vals['line_node']['cbc:DebitedQuantity'] = {
            '_text': base_line['quantity'],
            'unitCode': self._get_uom_unece_code(base_line['product_uom_id']),
        }

    def _ubl_add_line_item_name_description_nodes(self, vals):
        item_node = vals['item_node']
        base_line = vals['line_vals']['base_line']
        product = base_line['product_id']

        if base_line.get('_removed_tax_data'):
            # Emptying tax extra line.
            name = description = base_line['_removed_tax_data']['tax'].name
        else:
            name = product.name or ''
            if line_name := base_line.get('name'):
                # Regular business line.
                description = line_name
                if not name:
                    name = line_name
            else:
                # Undefined line.
                description = product.description_sale or ''

        if description:
            item_node['cbc:Description'] = {'_text': description}
        else:
            item_node['cbc:Description'] = None

        if name:
            item_node['cbc:Name'] = {'_text': name}
        else:
            item_node['cbc:Name'] = None

    def _ubl_add_line_item_identification_nodes(self, vals):
        item_node = vals['item_node']
        base_line = vals['line_vals']['base_line']
        product = base_line['product_id']

        if product.default_code:
            item_node['cac:SellersItemIdentification'] = {
                'cbc:ID': {'_text': product.default_code},
            }
        else:
            item_node['cac:SellersItemIdentification'] = None
        if product.barcode:
            item_node['cac:StandardItemIdentification'] = {
                'cbc:ID': {
                    '_text': product.barcode,
                    'schemeID': '0160',  # GTIN
                },
            }
        else:
            item_node['cac:StandardItemIdentification'] = None

    def _ubl_add_line_item_additional_item_property_nodes(self, vals):
        item_node = vals['item_node']
        base_line = vals['line_vals']['base_line']
        product = base_line['product_id']

        item_node['cac:AdditionalItemProperty'] = [
            {
                'cbc:Name': {'_text': value.attribute_id.name},
                'cbc:Value': {'_text': value.name},
            }
            for value in product.product_template_attribute_value_ids
        ]

    def _ubl_get_line_item_commodity_classification_node_from_intrastat_code(self, vals, intrastat_code):
        return {
            'cbc:ItemClassificationCode': {
                '_text': intrastat_code.code,
                'listID': 'HS',
                'listVersionID': None,
            }
        }

    def _ubl_get_line_item_commodity_classification_node_from_unspsc_code(self, vals, unspsc_code):
        return {
            'cbc:ItemClassificationCode': {
                '_text': unspsc_code.code,
                'listID': 'UNSPSC',
                'listVersionID': None,
            }
        }

    def _ubl_get_line_item_commodity_classification_node_from_cpv_code(self, vals, cpv_code):
        return {
            'cbc:ItemClassificationCode': {
                '_text': cpv_code.code,
                'listID': 'CPV',
                'listVersionID': None,
            }
        }

    def _ubl_add_line_item_commodity_classification_nodes(self, vals):
        item_node = vals['item_node']
        base_line = vals['line_vals']['base_line']
        product = base_line['product_id']
        nodes = item_node['cac:CommodityClassification'] = []

        if self.module_installed('account_intrastat'):
            intrastat_code = product.intrastat_code_id
            if intrastat_code.code:
                nodes.append(self._ubl_get_line_item_commodity_classification_node_from_intrastat_code(vals, intrastat_code))

        if self.module_installed('product_unspsc'):
            unspsc_code = product.unspsc_code_id
            if unspsc_code.code:
                nodes.append(self._ubl_get_line_item_commodity_classification_node_from_unspsc_code(vals, unspsc_code))

        if self.module_installed('l10n_ro_cpv_code'):
            cpv_code = product.cpv_code_id
            if cpv_code.code:
                nodes.append(self._ubl_get_line_item_commodity_classification_node_from_cpv_code(vals, cpv_code))

        return nodes

    def _ubl_get_line_item_node_classified_tax_category_node(self, vals, tax_category):
        """ Generate the node 'cac:ClassifiedTaxCategory' in 'cac:Item'.

        :param vals:            Some custom data.
        :param tax_category:    An entry of vals['_ubl_values']['item_classified_tax_categories']
                                containing all the necessary data to build the node.
        :return:                A new node in 'cac:Item' -> 'cac:ClassifiedTaxCategory'.
        """
        return {
            '_currency': tax_category['currency'],
            'cbc:ID': {'_text': tax_category['tax_category_code']},
            'cbc:Name': {'_text': None},
            'cbc:Percent': {'_text': tax_category['percent']},
            'cbc:TaxExemptionReasonCode': {'_text': None},
            'cbc:TaxExemptionReason': {'_text': None},
            'cac:TaxScheme': {
                'cbc:ID': {'_text': tax_category['scheme_id']},
            }
        }

    def _ubl_add_line_item_classified_tax_category_nodes(self, vals, in_foreign_currency=True):
        AccountTax = self.env['account.tax']
        item_node = vals['item_node']
        base_line = vals['line_vals']['base_line']
        currency = base_line['currency_id'] if in_foreign_currency else vals['company_currency']
        suffix = '_currency' if in_foreign_currency else ''

        classified_tax_category_nodes = item_node['cac:ClassifiedTaxCategory'] = []
        aggregated_values = AccountTax._aggregate_base_line_tax_details(
            base_line=base_line,
            grouping_function=lambda base_line, tax_data: self._ubl_default_base_line_item_classified_tax_category_grouping_key(
                base_line=base_line,
                tax_data=tax_data,
                vals=vals,
                currency=currency,
            ),
        )
        for grouping_key, values in aggregated_values.items():
            if not grouping_key:
                continue

            classified_tax_category_nodes.append(self._ubl_get_line_item_node_classified_tax_category_node(vals, {
                **grouping_key,
                'base_amount': values[f'base_amount{suffix}'],
                'tax_amount': values[f'tax_amount{suffix}'],
            }))

    def _ubl_add_line_item_node(self, vals):
        node = vals['line_node']['cac:Item'] = {}
        sub_vals = {**vals, 'item_node': node}
        self._ubl_add_line_item_name_description_nodes(sub_vals)
        self._ubl_add_line_item_identification_nodes(sub_vals)
        self._ubl_add_line_item_additional_item_property_nodes(sub_vals)
        self._ubl_add_line_item_commodity_classification_nodes(sub_vals)
        self._ubl_add_line_item_classified_tax_category_nodes(sub_vals)

    def _ubl_get_line_item_node(self, vals, item_values):
        # TO BE REMOVED IN MASTER
        item_node = {}
        base_line = item_values['base_line']
        product = base_line['product_id']

        if product.default_code:
            item_node['cac:SellersItemIdentification'] = {
                'cbc:ID': {'_text': product.default_code},
            }
        else:
            item_node['cac:SellersItemIdentification'] = None
        if product.barcode:
            item_node['cac:StandardItemIdentification'] = {
                'cbc:ID': {
                    '_text': product.barcode,
                    'schemeID': '0160',  # GTIN
                },
            }
        else:
            item_node['cac:StandardItemIdentification'] = None
        item_node['cac:AdditionalItemProperty'] = [
            {
                'cbc:Name': {'_text': value.attribute_id.name},
                'cbc:Value': {'_text': value.name},
            }
            for value in product.product_template_attribute_value_ids
        ]

        if base_line.get('_removed_tax_data'):
            # Emptying tax extra line.
            name = description = base_line['_removed_tax_data']['tax'].name
        else:
            name = product.name or ''
            if line_name := base_line.get('name'):
                # Regular business line.
                description = line_name
                if not name:
                    name = line_name
            else:
                # Undefined line.
                description = product.description_sale or ''

        if description:
            item_node['cbc:Description'] = {'_text': description}
        else:
            item_node['cbc:Description'] = None

        if name:
            item_node['cbc:Name'] = {'_text': name}
        else:
            item_node['cbc:Name'] = None

        item_node['cac:ClassifiedTaxCategory'] = [
            self._ubl_get_line_item_node_classified_tax_category_node(vals, tax_category)
            for tax_category in item_values['classified_tax_categories'].values()
        ]
        return item_node

    def _ubl_get_line_allowance_charge_recycling_contribution_node(self, vals, recycling_contribution_values):
        currency = recycling_contribution_values['currency']
        amount = recycling_contribution_values['amount']
        tax = recycling_contribution_values['tax']
        if 'bebat' in tax.name.lower():
            charge_reason_code = 'CAV'
        else:
            charge_reason_code = 'AEO'
        is_charge = recycling_contribution_values['is_charge']
        return {
            '_currency': currency,
            'cbc:ChargeIndicator': {'_text': 'true' if is_charge else 'false'},
            'cbc:AllowanceChargeReasonCode': {'_text': charge_reason_code},
            'cbc:AllowanceChargeReason': {'_text': tax.name},
            'cbc:Amount': {
                '_text': FloatFmt(abs(amount), max_dp=currency.decimal_places),
                'currencyID': currency.name,
            },
        }

    def _ubl_get_line_allowance_charge_excise_node(self, vals, excise_values):
        currency = excise_values['currency']
        amount = excise_values['amount']
        tax = excise_values['tax']
        is_charge = excise_values['is_charge']
        return {
            '_currency': currency,
            'cbc:ChargeIndicator': {'_text': 'true' if is_charge else 'false'},
            'cbc:AllowanceChargeReason': {'_text': tax.name},
            'cbc:Amount': {
                '_text': FloatFmt(abs(amount), max_dp=currency.decimal_places),
                'currencyID': currency.name,
            },
        }

    def _ubl_get_line_allowance_charge_discount_node(self, vals, discount_values):
        currency = discount_values['currency']
        amount = discount_values['amount']
        base_amount = discount_values['base_amount']
        percent = discount_values['percent']
        is_charge = discount_values['is_charge']
        return {
            '_currency': currency,
            'cbc:ChargeIndicator': {'_text': 'true' if is_charge else 'false'},
            'cbc:MultiplierFactorNumeric': {'_text': abs(percent)},
            'cbc:AllowanceChargeReasonCode': {'_text': '95' if amount > 0.0 else 'ADK'},
            'cbc:AllowanceChargeReason': {'_text': _("Discount")},
            'cbc:Amount': {
                '_text': FloatFmt(abs(amount), max_dp=currency.decimal_places),
                'currencyID': currency.name,
            },
            'cbc:BaseAmount': {
                '_text': FloatFmt(abs(base_amount), max_dp=currency.decimal_places),
                'currencyID': currency.name,
            },
        }

    def _ubl_add_line_allowance_charge_nodes_for_discount(self, vals, in_foreign_currency=True):
        line_node = vals['line_node']
        base_line = vals['line_vals']['base_line']
        currency = base_line['currency_id'] if in_foreign_currency else vals['company_currency']
        suffix = '_currency' if in_foreign_currency else ''
        tax_details = base_line['tax_details']

        raw_discount_amount = tax_details[f'discount_amount{suffix}']
        if currency.is_zero(raw_discount_amount):
            return

        allowance_charges_nodes = line_node['cac:AllowanceCharge']
        allowance_charges_nodes.append(self._ubl_get_line_allowance_charge_discount_node(vals, {
            'currency': currency,
            'percent': base_line['discount'],
            'is_charge': raw_discount_amount < 0.0,
            'amount': raw_discount_amount,
            'base_amount': tax_details[f'gross_total_excluded{suffix}'],
        }))

    def _ubl_add_line_allowance_charge_nodes_for_recycling_contribution_taxes(self, vals, in_foreign_currency=True):
        line_node = vals['line_node']
        base_line = vals['line_vals']['base_line']
        currency = base_line['currency_id'] if in_foreign_currency else vals['company_currency']
        suffix = '_currency' if in_foreign_currency else ''

        allowance_charges_nodes = line_node['cac:AllowanceCharge']
        for tax_data in base_line['tax_details']['taxes_data']:
            if not self._ubl_is_recycling_contribution_tax(tax_data):
                continue

            allowance_charges_nodes.append(self._ubl_get_line_allowance_charge_recycling_contribution_node(vals, {
                'tax': tax_data['tax'],
                'is_charge': tax_data['tax_amount'] > 0.0,
                'amount': tax_data[f'tax_amount{suffix}'],
                'currency': currency,
            }))

    def _ubl_add_line_allowance_charge_nodes_for_excise_taxes(self, vals, in_foreign_currency=True):
        line_node = vals['line_node']
        base_line = vals['line_vals']['base_line']
        currency = base_line['currency_id'] if in_foreign_currency else vals['company_currency']
        suffix = '_currency' if in_foreign_currency else ''

        allowance_charges_nodes = line_node['cac:AllowanceCharge']
        for tax_data in base_line['tax_details']['taxes_data']:
            if not self._ubl_is_excise_tax(tax_data):
                continue

            allowance_charges_nodes.append(self._ubl_get_line_allowance_charge_excise_node(vals, {
                'tax': tax_data['tax'],
                'is_charge': tax_data['tax_amount'] > 0.0,
                'amount': tax_data[f'tax_amount{suffix}'],
                'currency': currency,
            }))

    def _ubl_add_line_allowance_charge_nodes(self, vals):
        vals['line_node']['cac:AllowanceCharge'] = []

    def _ubl_add_line_extension_amount_node(self, vals, in_foreign_currency=True):
        line_node = vals['line_node']
        base_line = vals['line_vals']['base_line']
        currency = base_line['currency_id'] if in_foreign_currency else vals['company_currency']
        suffix = '_currency' if in_foreign_currency else ''
        tax_details = base_line['tax_details']

        gross_total_excluded = tax_details[f'gross_total_excluded{suffix}']
        for allowance_charge_node in line_node['cac:AllowanceCharge']:
            sign = 1 if allowance_charge_node['cbc:ChargeIndicator']['_text'] == 'true' else -1
            gross_total_excluded += sign * allowance_charge_node['cbc:Amount']['_text']

        line_node['cbc:LineExtensionAmount'] = {
            '_text': FloatFmt(gross_total_excluded, min_dp=currency.decimal_places),
            'currencyID': currency.name,
        }

    def _ubl_get_allowance_charge_early_payment(self, vals, early_payment_values):
        currency = early_payment_values['currency']
        amount = early_payment_values['amount']
        is_charge = early_payment_values['is_charge']
        return {
            '_currency': currency,
            'cbc:ChargeIndicator': {'_text': 'true' if is_charge else 'false'},
            'cbc:AllowanceChargeReasonCode': {'_text': 'ZZZ' if is_charge else '64'},
            'cbc:AllowanceChargeReason': {'_text': _("Conditional cash/payment discount")},
            'cbc:Amount': {
                '_text': currency.round(abs(amount)),
                'currencyID': currency.name,
            },
            'cac:TaxCategory': [
                self._ubl_get_allowance_charge_early_payment_tax_category_node(vals, tax_category)
                for tax_category in early_payment_values['tax_categories'].values()
            ],
        }

    def _ubl_get_tax_category_node(self, vals, tax_category):
        """ Generate the node 'cac:TaxCategory' in 'cac:SubTotal'.

        :param vals:            Some custom data.
        :param tax_category:    An entry of vals['_ubl_values'](['tax_totals']|['withholding_tax_totals'])['tax_subtotals']
                                containing all the necessary data to build the node.
        :return:                A new node in 'cac:TaxTotal'.
        """
        return {
            '_currency': tax_category['currency'],
            'cbc:ID': {'_text': tax_category['tax_category_code']},
            'cbc:Name': {'_text': None},
            'cbc:Percent': {'_text': tax_category['percent']},
            'cbc:TaxExemptionReasonCode': {'_text': tax_category.get('tax_exemption_reason_code')},
            'cbc:TaxExemptionReason': {'_text': tax_category.get('tax_exemption_reason')},
            'cac:TaxScheme': {
                'cbc:ID': {'_text': tax_category['scheme_id']},
            }
        }

    def _ubl_get_allowance_charge_early_payment_tax_category_node(self, vals, tax_category):
        return {
            '_currency': tax_category['currency'],
            'cbc:ID': {'_text': tax_category['tax_category_code']},
            'cbc:Percent': {'_text': tax_category['percent']},
            'cac:TaxScheme': {
                'cbc:ID': {'_text': tax_category['scheme_id']},
            }
        }

    def _ubl_get_tax_subtotal_node(self, vals, tax_subtotal):
        """ Generate the node 'cac:SubTotal' in 'cac:TaxTotal'/'cac:WithholdingTaxTotal'.

        Note: 'cac:TaxCategory' is managed by '_ubl_get_tax_category_node'.

        :param vals:            Some custom data.
        :param tax_subtotal:    An entry of vals['_ubl_values'](['tax_totals']|['withholding_tax_totals'])['tax_subtotals']
                                containing all the necessary data to build the node.
        :return:                A new node in 'cac:TaxTotal'.
        """
        currency = tax_subtotal['currency']
        return {
            '_currency': currency,
            'cbc:TaxableAmount': {
                '_text': FloatFmt(tax_subtotal['base_amount'], min_dp=currency.decimal_places),
                'currencyID': currency.name
            },
            'cbc:TaxAmount': {
                '_text': FloatFmt(tax_subtotal['tax_amount'], min_dp=currency.decimal_places),
                'currencyID': currency.name
            },
            'cac:TaxCategory': [
                self._ubl_get_tax_category_node(vals, tax_category)
                for tax_category in tax_subtotal['tax_categories'].values()
            ],
        }

    def _ubl_get_tax_total_node(self, vals, tax_total):
        """ Generate the node 'cac:TaxTotal'.

        Note: 'cac:Subtotal' is managed by '_ubl_get_tax_subtotal_node'.

        :param vals:            Some custom data.
        :param tax_total:       An entry of vals['_ubl_values']['tax_totals'] containing all the necessary data to build the node.
        :return:                A new node in 'cac:TaxTotal'.
        """
        currency = tax_total['currency']
        return {
            '_currency': currency,
            'cbc:TaxAmount': {
                '_text': FloatFmt(tax_total['amount'], min_dp=currency.decimal_places),
                'currencyID': currency.name
            },
            'cac:TaxSubtotal': [
                self._ubl_get_tax_subtotal_node(vals, subtotal)
                for subtotal in tax_total['subtotals'].values()
            ],
        }

    def _ubl_get_withholding_tax_total_node(self, vals, tax_total):
        """ Generate the node 'cac:WithholdingTaxTotal'.

        Note: 'cac:Subtotal' is managed by '_ubl_get_tax_subtotal_node'.

        :param vals:            Some custom data.
        :param tax_total:       An entry of vals['_ubl_values']['withholding_tax_totals'] containing all the necessary data to build the node.
        :return:                A new node in 'cac:WithholdingTaxTotal'.
        """
        return self._ubl_get_tax_total_node(vals, tax_total)

    def _ubl_tax_totals_node_grouping_key(self, base_line, tax_data, vals, currency):
        tax_category_key = self._ubl_default_tax_category_grouping_key(base_line, tax_data, vals, currency)
        tax_subtotal_key = tax_category_key
        if tax_category_key:
            tax_total_key = {
                'is_withholding': tax_category_key['is_withholding'],
                'currency': currency,
            }
        else:
            tax_total_key = None
        return {
            'tax_category_key': tax_category_key,
            'tax_subtotal_key': tax_subtotal_key,
            'tax_total_key': tax_total_key
        }

    def _ubl_add_tax_totals_nodes(self, vals):
        AccountTax = self.env['account.tax']
        base_lines = vals['base_lines']
        company = vals['company']
        company_currency = company.currency_id
        currency = vals['currency_id']

        iter_currency = [(currency, '_currency')]
        if currency != company_currency:
            iter_currency.append((company_currency, ''))

        # Since we have to combine 3 keys given by the same grouping key but used at 3 different places, we compute them in advance.
        # /!\ Even without tax, a base line could get a grouping key for a 0% tax without any real record set.
        new_base_lines = [
            {
                **base_line,
                '_tax_totals_keys': {
                    (tax_data['tax'] if tax_data else None, currency): self._ubl_tax_totals_node_grouping_key(base_line, tax_data, vals, currency)
                    for currency, _suffix in iter_currency
                    for tax_data in base_line['tax_details']['taxes_data'] or [None]
                }
            }
            for base_line in base_lines
        ]

        collected_tax_totals_values = {
            'cac:TaxTotal': {},
            'cac:WithholdingTaxTotal': {},
        }
        for sub_currency, suffix in iter_currency:

            # tax_totals / withholding_tax_totals

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=new_base_lines,
                grouping_function=lambda base_line, tax_data: base_line['_tax_totals_keys'][(tax_data or {}).get('tax'), sub_currency]['tax_total_key']
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue

                if grouping_key['is_withholding']:
                    target_key = 'cac:WithholdingTaxTotal'
                    sign = -1
                else:
                    target_key = 'cac:TaxTotal'
                    sign = 1

                collected_tax_totals_values[target_key][frozendict(grouping_key)] = {
                    **grouping_key,
                    'amount': sign * values[f'tax_amount{suffix}'],
                    'subtotals': {},
                }

            # tax_subtotals

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=new_base_lines,
                grouping_function=lambda base_line, tax_data: {
                    k: v
                    for k, v in base_line['_tax_totals_keys'][(tax_data or {}).get('tax'), sub_currency].items()
                    if k in ('tax_total_key', 'tax_subtotal_key')
                },
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue
                tax_total_key = grouping_key['tax_total_key']
                tax_subtotal_key = grouping_key['tax_subtotal_key']
                if not tax_total_key or not tax_subtotal_key:
                    continue

                if tax_total_key['is_withholding']:
                    target_key = 'cac:WithholdingTaxTotal'
                    sign = -1
                else:
                    target_key = 'cac:TaxTotal'
                    sign = 1

                tax_total_values = collected_tax_totals_values[target_key][frozendict(tax_total_key)]
                tax_total_values['subtotals'][frozendict(tax_subtotal_key)] = {
                    **tax_subtotal_key,
                    'base_amount': values[f'base_amount{suffix}'],
                    'tax_amount': sign * values[f'tax_amount{suffix}'],
                    'tax_categories': {},
                }

            # tax_categories

            base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(
                base_lines=new_base_lines,
                grouping_function=lambda base_line, tax_data: base_line['_tax_totals_keys'][(tax_data or {}).get('tax'), sub_currency],
            )
            values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
            for grouping_key, values in values_per_grouping_key.items():
                if not grouping_key:
                    continue
                tax_total_key = grouping_key['tax_total_key']
                tax_subtotal_key = grouping_key['tax_subtotal_key']
                tax_category_key = grouping_key['tax_category_key']
                if not tax_total_key or not tax_subtotal_key or not tax_category_key:
                    continue

                if tax_total_key['is_withholding']:
                    target_key = 'cac:WithholdingTaxTotal'
                    sign = -1
                else:
                    target_key = 'cac:TaxTotal'
                    sign = 1

                tax_total_values = collected_tax_totals_values[target_key][frozendict(tax_total_key)]
                tax_subtotal_values = tax_total_values['subtotals'][frozendict(tax_subtotal_key)]
                tax_subtotal_values['tax_categories'][frozendict(tax_category_key)] = {
                    **tax_category_key,
                    'base_amount': values[f'base_amount{suffix}'],
                    'tax_amount': sign * values[f'tax_amount{suffix}'],
                }

        for key, tax_totals_values in collected_tax_totals_values.items():
            nodes = vals['document_node'][key] = []
            for tax_total_values in tax_totals_values.values():
                tax_total_node = self._ubl_get_tax_total_node(vals, tax_total_values)
                nodes.append(tax_total_node)
