# Copyright 2010 Jordi Esteve, Zikzakmedia S.L. (http://www.zikzakmedia.com)
# Copyright 2010 Pexego Sistemas Informáticos S.L.(http://www.pexego.es)
#        Borja López Soilán
# Copyright 2013 Joaquin Gutierrez (http://www.gutierrezweb.es)
# Copyright 2015 Tecnativa - Antonio Espinosa
# Copyright 2016 Tecnativa - Jairo Llopis
# Copyright 2016 Jacques-Etienne Baudoux <je@bcim.be>
# Copyright 2018 Tecnativa - Pedro M. Baeza
# Copyright 2020 Noviat - Luc De Meyer
# Copyright 2024-2025 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging

from odoo import _, api, fields, models, tools

_logger = logging.getLogger(__name__)


class WizardUpdateChartsAccounts(models.TransientModel):
    _name = "wizard.update.charts.accounts"
    _description = "Wizard Update Charts Accounts"

    state = fields.Selection(
        selection=[
            ("init", "Configuration"),
            ("ready", "Select records to update"),
            ("done", "Wizard completed"),
        ],
        string="Status",
        readonly=True,
        default="init",
    )
    company_id = fields.Many2one(
        comodel_name="res.company",
        string="Company",
        required=True,
        default=lambda self: self.env.user.company_id.id,
    )
    chart_template = fields.Selection(
        selection="_chart_template_selection",
        required=True,
    )
    code_digits = fields.Integer()
    update_tax_group = fields.Boolean(
        string="Update taxe groups",
        default=True,
        help="Existing tax groups are updated. Tax group are searched by name.",
    )
    update_tax = fields.Boolean(
        string="Update taxes",
        default=True,
        help="Existing taxes are updated. Taxes are searched by name.",
    )
    update_account = fields.Boolean(
        string="Update accounts",
        default=True,
        help="Existing accounts are updated. Accounts are searched by code.",
    )
    update_account_group = fields.Boolean(
        string="Update account groups",
        default=True,
        help="Existing account groups are updated. "
        "Account groups are searched by prefix_code_start.",
    )
    update_fiscal_position = fields.Boolean(
        string="Update fiscal positions",
        default=True,
        help="Existing fiscal positions are updated. Fiscal positions are "
        "searched by name.",
    )
    tax_group_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.tax.group",
        inverse_name="update_chart_wizard_id",
        string="Taxe Groups",
    )
    tax_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.tax",
        inverse_name="update_chart_wizard_id",
        string="Taxes",
    )
    account_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.account",
        inverse_name="update_chart_wizard_id",
        string="Accounts",
    )
    account_group_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.account.group",
        inverse_name="update_chart_wizard_id",
        string="Account Groups",
    )
    fiscal_position_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.fiscal.position",
        inverse_name="update_chart_wizard_id",
        string="Fiscal positions",
    )
    new_tax_groups = fields.Integer(compute="_compute_new_tax_group_count")
    new_taxes = fields.Integer(compute="_compute_new_taxes_count")
    new_accounts = fields.Integer(compute="_compute_new_accounts_count")
    new_account_groups = fields.Integer(compute="_compute_new_account_groups_count")
    rejected_new_account_number = fields.Integer()
    new_fps = fields.Integer(
        string="New fiscal positions", compute="_compute_new_fps_count"
    )
    updated_tax_groups = fields.Integer(compute="_compute_updated_tax_groups_count")
    updated_taxes = fields.Integer(compute="_compute_updated_taxes_count")
    rejected_updated_account_number = fields.Integer()
    updated_accounts = fields.Integer(compute="_compute_updated_accounts_count")
    updated_account_groups = fields.Integer(
        compute="_compute_updated_account_groups_count"
    )
    updated_fps = fields.Integer(
        string="Updated fiscal positions", compute="_compute_updated_fps_count"
    )
    deleted_taxes = fields.Integer(
        string="Deactivated taxes", compute="_compute_deleted_taxes_count"
    )
    log = fields.Text(string="Messages and Errors", readonly=True)
    tax_group_field_ids = fields.Many2many(
        comodel_name="ir.model.fields",
        relation="wizard_update_charts_tax_group_fields_rel",
        string="Tax group fields",
        domain=lambda self: self._domain_tax_group_field_ids(),
        default=lambda self: self._default_tax_group_field_ids(),
    )
    tax_field_ids = fields.Many2many(
        comodel_name="ir.model.fields",
        relation="wizard_update_charts_tax_fields_rel",
        string="Tax fields",
        domain=lambda self: self._domain_tax_field_ids(),
        default=lambda self: self._default_tax_field_ids(),
    )
    account_field_ids = fields.Many2many(
        comodel_name="ir.model.fields",
        relation="wizard_update_charts_account_fields_rel",
        string="Account fields",
        domain=lambda self: self._domain_account_field_ids(),
        default=lambda self: self._default_account_field_ids(),
    )
    account_group_field_ids = fields.Many2many(
        comodel_name="ir.model.fields",
        relation="wizard_update_charts_account_group_fields_rel",
        string="Account groups fields",
        domain=lambda self: self._domain_account_group_field_ids(),
        default=lambda self: self._default_account_group_field_ids(),
    )
    fp_field_ids = fields.Many2many(
        comodel_name="ir.model.fields",
        relation="wizard_update_charts_fp_fields_rel",
        string="Fiscal position fields",
        domain=lambda self: self._domain_fp_field_ids(),
        default=lambda self: self._default_fp_field_ids(),
    )
    tax_group_matching_ids = fields.One2many(
        comodel_name="wizard.tax.group.matching",
        inverse_name="update_chart_wizard_id",
        string="Tax goups matching",
        default=lambda self: self._default_tax_group_matching_ids(),
    )
    tax_matching_ids = fields.One2many(
        comodel_name="wizard.tax.matching",
        inverse_name="update_chart_wizard_id",
        string="Taxes matching",
        default=lambda self: self._default_tax_matching_ids(),
    )
    account_matching_ids = fields.One2many(
        comodel_name="wizard.account.matching",
        inverse_name="update_chart_wizard_id",
        string="Accounts matching",
        default=lambda self: self._default_account_matching_ids(),
    )
    account_group_matching_ids = fields.One2many(
        comodel_name="wizard.account.group.matching",
        inverse_name="update_chart_wizard_id",
        string="Account groups matching",
        default=lambda self: self._default_account_group_matching_ids(),
    )
    fp_matching_ids = fields.One2many(
        comodel_name="wizard.fp.matching",
        inverse_name="update_chart_wizard_id",
        string="Fiscal positions matching",
        default=lambda self: self._default_fp_matching_ids(),
    )

    def _domain_per_name(self, name):
        return [
            ("model", "=", name),
            ("name", "not in", tuple(self.fields_to_ignore(name))),
        ]

    def _domain_tax_group_field_ids(self):
        return self._domain_per_name("account.tax.group")

    def _domain_tax_field_ids(self):
        return self._domain_per_name("account.tax")

    def _domain_account_field_ids(self):
        return self._domain_per_name("account.account")

    def _domain_account_group_field_ids(self):
        return self._domain_per_name("account.group")

    def _domain_fp_field_ids(self):
        return self._domain_per_name("account.fiscal.position")

    def _default_tax_group_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_tax_group_field_ids() + [("ttype", "!=", "one2many")],
            )
        ]

    def _default_tax_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_tax_field_ids() + [("ttype", "!=", "one2many")],
            )
        ]

    def _default_account_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_account_field_ids() + [("ttype", "!=", "one2many")],
            )
        ]

    def _default_account_group_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_account_group_field_ids()
            )
        ]

    def _default_fp_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(self._domain_fp_field_ids())
        ]

    def _get_matching_ids(self, model_name, ordered_opts):
        vals = []
        for seq, opt in enumerate(ordered_opts, 1):
            vals.append((0, False, {"sequence": seq, "matching_value": opt}))
        all_options = self.env[model_name]._get_matching_selection()
        all_options = map(lambda x: x[0], all_options)
        all_options = list(set(all_options) - set(ordered_opts))

        for seq, opt in enumerate(all_options, len(ordered_opts) + 1):
            vals.append((0, False, {"sequence": seq, "matching_value": opt}))
        return vals

    def _default_fp_matching_ids(self):
        ordered_opts = ["xml_id", "name"]
        return self._get_matching_ids("wizard.fp.matching", ordered_opts)

    def _default_tax_group_matching_ids(self):
        ordered_opts = ["xml_id", "name"]
        return self._get_matching_ids("wizard.tax.group.matching", ordered_opts)

    def _default_tax_matching_ids(self):
        ordered_opts = ["xml_id", "description", "name"]
        return self._get_matching_ids("wizard.tax.matching", ordered_opts)

    def _default_account_matching_ids(self):
        ordered_opts = ["xml_id", "code", "name"]
        return self._get_matching_ids("wizard.account.matching", ordered_opts)

    def _default_account_group_matching_ids(self):
        ordered_opts = ["xml_id", "code_prefix_start"]
        return self._get_matching_ids("wizard.account.group.matching", ordered_opts)

    def _chart_template_selection(self):
        return (
            self.env["account.chart.template"]
            .with_context(chart_template_only_installed=True)
            ._select_chart_template(self.company_id.country_id)
        )

    @api.depends("tax_group_ids")
    def _compute_new_tax_group_count(self):
        self.new_tax_groups = len(
            self.tax_group_ids.filtered(lambda x: x.type == "new")
        )

    @api.depends("tax_ids")
    def _compute_new_taxes_count(self):
        self.new_taxes = len(self.tax_ids.filtered(lambda x: x.type == "new"))

    @api.depends("account_ids")
    def _compute_new_accounts_count(self):
        self.new_accounts = (
            len(self.account_ids.filtered(lambda x: x.type == "new"))
            - self.rejected_new_account_number
        )

    @api.depends("account_group_ids")
    def _compute_new_account_groups_count(self):
        self.new_account_groups = len(
            self.account_group_ids.filtered(lambda x: x.type == "new")
        )

    @api.depends("fiscal_position_ids")
    def _compute_new_fps_count(self):
        self.new_fps = len(self.fiscal_position_ids.filtered(lambda x: x.type == "new"))

    @api.depends("tax_group_ids")
    def _compute_updated_tax_groups_count(self):
        self.updated_tax_groups = len(
            self.tax_group_ids.filtered(lambda x: x.type == "updated")
        )

    @api.depends("tax_ids")
    def _compute_updated_taxes_count(self):
        self.updated_taxes = len(self.tax_ids.filtered(lambda x: x.type == "updated"))

    @api.depends("account_ids")
    def _compute_updated_accounts_count(self):
        self.updated_accounts = (
            len(self.account_ids.filtered(lambda x: x.type == "updated"))
            - self.rejected_updated_account_number
        )

    @api.depends("account_group_ids")
    def _compute_updated_account_groups_count(self):
        self.updated_account_groups = len(
            self.account_group_ids.filtered(lambda x: x.type == "updated")
        )

    @api.depends("fiscal_position_ids")
    def _compute_updated_fps_count(self):
        self.updated_fps = len(
            self.fiscal_position_ids.filtered(lambda x: x.type == "updated")
        )

    @api.depends("tax_ids")
    def _compute_deleted_taxes_count(self):
        self.deleted_taxes = len(self.tax_ids.filtered(lambda x: x.type == "deleted"))

    @api.onchange("company_id")
    def _onchage_company_update_chart_template(self):
        self.chart_template = self.company_id.chart_template

    @api.onchange("chart_template")
    def _onchage_chart_template(self):
        if self.chart_template:
            template = self.env["account.chart.template"]
            data = template._get_chart_template_data(self.chart_template)[
                "template_data"
            ]
            self.code_digits = int(data.get("code_digits", 6))

    def _reopen(self):
        return {
            "type": "ir.actions.act_window",
            "view_mode": "form",
            "res_id": self.id,
            "res_model": self._name,
            "target": "new",
            # save original model in context,
            # because selecting the list of available
            # templates requires a model in context
            "context": {"default_model": self._name},
        }

    def action_init(self):
        """Initial action that sets the initial state."""
        self.write(
            {
                "state": "init",
                "tax_group_ids": [(2, r.id, False) for r in self.tax_group_ids],
                "tax_ids": [(2, r.id, False) for r in self.tax_ids],
                "account_ids": [(2, r.id, False) for r in self.account_ids],
                "fiscal_position_ids": [
                    (2, r.id, False) for r in self.fiscal_position_ids
                ],
            }
        )
        return self._reopen()

    def _get_chart_template_data(self):
        chart_template_model = self.env["account.chart.template"]
        t_data = chart_template_model._get_chart_template_data(self.chart_template)
        model_mapping = {
            "account.group": self.update_account_group,
            "account.account": self.update_account,
            "account.tax.group": self.update_tax_group,
            "account.tax": self.update_tax,
            "account.fiscal.position": self.update_fiscal_position,
        }
        langs = self.env["res.lang"].search([])
        for m_name in model_mapping.keys():
            if not model_mapping[m_name]:
                continue
            for _xmlid, r_data in t_data[m_name].items():
                if "__translation_module__" in r_data:
                    for f_name in list(r_data["__translation_module__"].keys()):
                        for lang in langs:
                            field_translation = (
                                chart_template_model._get_field_translation(
                                    r_data, f_name, lang.code
                                )
                            )
                            short_lang = lang.code.split("_")[0]
                            key_lang = f"{f_name}@{short_lang}"
                            if field_translation:
                                r_data[key_lang] = field_translation
                            else:
                                r_data[key_lang] = r_data[f_name]
        return t_data

    def action_find_records(self):
        """Searchs for records to update/create and shows them."""
        self.env.registry.clear_cache()
        t_data = self._get_chart_template_data()
        # Search for, and load, the records to create/update.
        if self.update_account_group:
            self._find_account_groups(t_data["account.group"])
        if self.update_account:
            self._find_accounts(t_data["account.account"])
        if self.update_tax_group:
            self._find_tax_groups(t_data["account.tax.group"])
        if self.update_tax:
            self._find_taxes(t_data["account.tax"])
        if self.update_fiscal_position:
            self._find_fiscal_positions(t_data["account.fiscal.position"])
        # Write the results, and go to the next step.
        self.state = "ready"
        return self._reopen()

    def action_update_records(self):
        """Action that creates/updates/deletes the selected elements."""
        self.rejected_new_account_number = 0
        self.rejected_updated_account_number = 0
        self.log = False
        t_data = self._get_chart_template_data()
        # Create or update the records.
        if self.update_account_group:
            self._update_account_groups(t_data["account.group"])
        if self.update_account:
            self._update_accounts(t_data["account.account"])
        if self.update_tax_group:
            self._update_tax_groups(t_data["account.tax.group"])
        if self.update_tax:
            self._update_taxes(t_data["account.tax"])
        if self.update_fiscal_position:
            self._update_fiscal_positions(t_data["account.fiscal.position"])
        # Store new chart in the company
        self.company_id.chart_template = self.chart_template
        # Store the data and go to the next step.
        self.state = "done"
        return self._reopen()

    @api.model
    @tools.ormcache("code")
    def padded_code(self, code):
        """Return a right-zero-padded code with the chosen digits.
        Similar to what is done in the _pre_load_data() method of chart.template
        """
        if isinstance(code, str):
            return code.ljust(self.code_digits, "0")
        _logger.info(
            "padded_code received a non-string value: %s. Returning it as is.", code
        )
        return code

    @api.model
    @tools.ormcache("name")
    def fields_to_ignore(self, name):
        """Get fields that will not be used when checking differences.

        :param str template: A template record.
        :param str name: The name of the template model.
        :return set: Fields to ignore in diff.
        """
        mail_thread_fields = set(self.env["mail.thread"]._fields)
        specials_mapping = {
            "account.tax.group": mail_thread_fields | {"sequence"},
            "account.tax": mail_thread_fields | {"children_tax_ids", "sequence"},
            "account.account": mail_thread_fields
            | {
                "root_id",
            },
            "account.group": {"parent_id", "code_prefix_end"},
            "account.fiscal.position": {
                "sequence",
            },
        }
        specials = {
            "display_name",
            "__last_update",
            "company_id",
        } | specials_mapping.get(name, set())
        return set(models.MAGIC_COLUMNS) | specials

    @api.model
    def diff_fields(self, record_values, real):  # noqa: C901
        """Get fields that are different in record values and real records.

        :param odoo.models.Model record_values:
            Record values values.
        :param odoo.models.Model real:
            Real record.

        :return dict:
            Fields that are different in both records, and the expected value.
        """
        result = dict()
        ignore = self.fields_to_ignore(real._name)
        field_mapping = {
            "account.tax": self.tax_field_ids,
            "account.account": self.account_field_ids,
            "account.group": self.account_group_field_ids,
            "account.fiscal.position": self.fp_field_ids,
        }
        langs = self.env["res.lang"].search([])
        # If the fields to be queried are not mapped, use all of them
        # (example: account.tax.repartition.line).
        if real._name not in field_mapping:
            field_mapping[real._name] = self.env["ir.model.fields"].search(
                self._domain_per_name(real._name)
            )
        fields_by_key = {x.name: x for x in field_mapping[real._name]}
        to_include = field_mapping[real._name].mapped("name")
        for key in record_values.keys():
            if key in ignore or key not in to_include or not record_values.get(key):
                continue
            field = fields_by_key[key]
            record_value, real_value = record_values[key], real[key]
            if real._name == "account.account" and key == "code":
                record_value = self.padded_code(record_value)
                real_value = self.padded_code(real_value)
            # Field ttype conditions
            if field.ttype == "many2many":
                if isinstance(record_value, str):
                    real_xml_ids = []
                    for child_item in real_value:
                        real_xml_ids.append(child_item.get_external_id()[child_item.id])
                    real_xml_id = ",".join(real_xml_ids)
                    if real_xml_id != record_value:
                        result[key] = record_value
                else:
                    record_value_compare = []
                    for record_value_item in record_value:
                        record_value_compare += record_value_item[2]
                    if record_value_compare.sort() != real_value.ids.sort():
                        result[key] = record_value
                continue
            elif field.ttype == "many2one":
                real_xml_id = self._get_external_id(real_value) if real_value else False
                full_xml_id = (
                    f"account.{self.company_id.id}_{record_value}"
                    if "." not in record_value
                    else record_value
                )
                if real_xml_id != full_xml_id:
                    result[key] = record_value
                continue
            elif field.ttype == "one2many":
                if len(record_value) != len(real_value):
                    result[key] = [(5, 0, 0)] + record_value
                else:
                    for key2, record_value_item in enumerate(record_value):
                        res_item = self.diff_fields(
                            record_value_item[2], real_value[key2]
                        )
                        if len(res_item) > 0:
                            # Something has changed in an element, we change everything
                            # just in case (we do not know for sure that the record we
                            # are consulting by key is the correct one, for example,
                            # if it has been deleted by mistake and created again in
                            # the same way).
                            result[key] = [(5, 0, 0)] + record_value
                            break
                continue
            # Define correct value if field is translatable
            if field.translate:
                for lang in langs:
                    short_lang = lang.code.split("_")[0]
                    key_lang = f"{key}@{short_lang}"
                    if key_lang in record_values:
                        real_value_lang = real.with_context(lang=lang.code)[key]
                        record_value_lang = record_values[key_lang]
                        if field.ttype == "html":
                            # Convert HTML to inner content for comparison
                            # especially to prevent comparing str with Markup
                            real_value_lang = tools.mail.html_to_inner_content(
                                real_value_lang
                            )
                        if record_value_lang != real_value_lang:
                            result[key_lang] = record_value_lang
            elif field.ttype == "html":
                # Convert HTML to inner content for comparison
                # especially to prevent comparing str with Markup
                real_value = tools.mail.html_to_inner_content(real_value)
                if record_value != real_value:
                    result[key] = record_value
            elif record_value != real_value:
                result[key] = record_value
        # __translation_module__
        if len(result.keys()) > 0 and not self.env.context.get("skip_translation_keys"):
            if "__translation_module__" in record_values:
                result["__translation_module__"] = record_values[
                    "__translation_module__"
                ]
        return result

    @api.model
    def diff_notes(self, record_values, real):
        """Get notes for humans on why is this record going to be updated.

        :param openerp.models.Model record_values:
            Record values values.

        :param openerp.models.Model real:
            Real record.

        :return str:
            Notes result.
        """
        result = list()
        different_fields_set = set()
        for f in (
            self.with_context(skip_translation_keys=True)
            .diff_fields(record_values, real)
            .keys()
        ):
            field_name = f.split("@")[0] if "@" in f else f
            different_fields_set.add(
                real._fields[field_name].get_description(self.env)["string"]
            )
        different_fields = sorted(list(different_fields_set))
        if different_fields:
            result.append(
                _("Differences in these fields: %s.") % ", ".join(different_fields)
            )
        return "\n".join(result)

    def _domain_taxes_to_deactivate(self, found_taxes_ids):
        return [
            ("company_id", "=", self.company_id.id),
            ("id", "not in", found_taxes_ids),
            ("active", "=", True),
        ]

    def _find_record_matching(self, model_name, xmlid, data):
        mapped_fields = {
            "account.group": self.account_group_matching_ids,
            "account.account": self.account_matching_ids,
            "account.tax.group": self.tax_group_matching_ids,
            "account.tax": self.tax_matching_ids,
            "account.fiscal.position": self.fp_matching_ids,
        }
        company = self.company_id
        model = self.env[model_name]
        company_domain = []
        if "company_id" in model._fields:
            company_domain = [("company_id", "=", company.id)]
        elif "company_ids" in model._fields:
            company_domain = [("company_ids", "in", company.ids)]
        for matching in mapped_fields[model_name].sorted("sequence"):
            if matching.matching_value == "xml_id":
                full_xmlid = (
                    f"account.{company.id}_{xmlid}" if "." not in xmlid else xmlid
                )
                record = self.env.ref(full_xmlid, raise_if_not_found=False)
                if record:
                    # To read company-dependent fields correctly
                    return record.with_company(company)
            else:
                f_name = matching.matching_value
                if not data.get(f_name):
                    continue
                f_value = data[f_name]
                # Fix code from account.account
                if model_name == "account.account" and f_name == "code":
                    f_value = self.padded_code(f_value)
                # Prepare domain
                domain = [(f_name, "=", f_value)] + company_domain
                if model_name == "account.tax" and f_name != "type_tax_use":
                    # Extra domain to prevent find the wrong record
                    domain += [("type_tax_use", "=", data["type_tax_use"])]
                # Search record from model
                result = model.search(domain)
                if result:
                    return result
        return False

    def _get_external_id(self, record):
        external_ids = record.get_external_id()
        return external_ids.get(record.id, False)

    @tools.ormcache("self", "record", "xml_id")
    def missing_xml_id(self, record, xml_id):
        record_xml_id = self._get_external_id(record)
        full_xml_id = (
            f"account.{self.company_id.id}_{xml_id}" if "." not in xml_id else xml_id
        )
        return record_xml_id != full_xml_id

    def recreate_xml_id(self, record, xml_id):
        """Eecreate the xml_id if it is different than expected, otherwise
        chart.template won't do it correctly.
        """
        if self.missing_xml_id(record, xml_id):
            ir_model_data = self.env["ir.model.data"]
            ir_model_data.search(
                [("model", "=", record._name), ("res_id", "=", record.id)]
            ).write(
                {
                    "module": "account",
                    "name": f"{self.company_id.id}_{xml_id}",
                    "noupdate": True,
                }
            )

    def _find_tax_groups(self, t_data):
        """Search for, and load, template data to create/update/delete."""
        found_tax_groups_ids = []
        tax_group_vals = []
        for xmlid, r_data in t_data.items():
            tax_group = self._find_record_matching("account.tax.group", xmlid, r_data)
            # Check if the template data matches a real tax group
            if not tax_group:
                # Tax group to be created
                tax_group_vals.append(
                    {
                        "xml_id": xmlid,
                        "update_chart_wizard_id": self.id,
                        "type": "new",
                        "notes": _("Name or description not found."),
                    }
                )
            else:
                found_tax_groups_ids.append(tax_group.id)
                # Check the tax group for changes
                notes = self.diff_notes(r_data, tax_group)
                if self.missing_xml_id(tax_group, xmlid):
                    notes += (notes and "\n" or "") + _("Missing XML-ID.")
                if notes:
                    # Tax group to be updated
                    tax_group_vals.append(
                        {
                            "xml_id": xmlid,
                            "update_chart_wizard_id": self.id,
                            "type": "updated",
                            "update_tax_group_id": tax_group.id,
                            "notes": notes,
                        }
                    )
        self.tax_group_ids = [(5, 0, 0)] + [
            (0, 0, tax_group_val) for tax_group_val in tax_group_vals
        ]

    def _find_taxes(self, t_data):
        """Search for, and load, template data to create/update/delete."""
        found_taxes_ids = []
        tax_vals = []
        for xmlid, r_data in t_data.items():
            tax = self._find_record_matching("account.tax", xmlid, r_data)
            # Check if the template data matches a real tax
            if not tax:
                # Tax to be created
                tax_vals.append(
                    {
                        "xml_id": xmlid,
                        "type_tax_use": r_data["type_tax_use"],
                        "update_chart_wizard_id": self.id,
                        "type": "new",
                        "notes": _("Name or description not found."),
                    }
                )
            else:
                found_taxes_ids.append(tax.id)
                # Check the tax for changes
                notes = self.diff_notes(r_data, tax)
                if self.missing_xml_id(tax, xmlid):
                    notes += (notes and "\n" or "") + _("Missing XML-ID.")
                if notes:
                    # Tax to be updated
                    tax_vals.append(
                        {
                            "xml_id": xmlid,
                            "type_tax_use": tax.type_tax_use,
                            "update_chart_wizard_id": self.id,
                            "type": "updated",
                            "update_tax_id": tax.id,
                            "notes": notes,
                        }
                    )
        # search for taxes not in the template and propose them for
        # deactivation
        taxes_to_deactivate = self.env["account.tax"].search(
            self._domain_taxes_to_deactivate(found_taxes_ids)
        )
        for tax in taxes_to_deactivate:
            tax_vals.append(
                {
                    "update_chart_wizard_id": self.id,
                    "type_tax_use": tax.type_tax_use,
                    "type": "deleted",
                    "update_tax_id": tax.id,
                    "notes": _("To deactivate: not in the template"),
                }
            )
        self.tax_ids = [(5, 0, 0)] + [(0, 0, tax_val) for tax_val in tax_vals]

    def _find_accounts(self, t_data):
        """Load account template data to create/update."""
        account_vals = []
        for xmlid, r_data in t_data.items():
            account = self._find_record_matching("account.account", xmlid, r_data)
            # Account to be created
            if not account:
                account_vals.append(
                    {
                        "xml_id": xmlid,
                        "update_chart_wizard_id": self.id,
                        "type": "new",
                        "notes": _("No account found with this code."),
                    }
                )
            else:
                # Check the account for changes
                notes = self.diff_notes(r_data, account)
                if self.missing_xml_id(account, xmlid):
                    notes += (notes and "\n" or "") + _("Missing XML-ID.")
                if notes:
                    # Account to be updated
                    account_vals.append(
                        {
                            "xml_id": xmlid,
                            "update_chart_wizard_id": self.id,
                            "type": "updated",
                            "update_account_id": account.id,
                            "notes": notes,
                        }
                    )
        self.account_ids = [(5, 0, 0)] + [(0, 0, a_val) for a_val in account_vals]

    def _find_account_groups(self, t_data):
        """Load account template data to create/update."""
        ag_vals = []
        for xmlid, r_data in t_data.items():
            account_group = self._find_record_matching("account.group", xmlid, r_data)
            if not account_group:
                # Account to be created
                ag_vals.append(
                    {
                        "xml_id": xmlid,
                        "update_chart_wizard_id": self.id,
                        "type": "new",
                        "notes": _("No account found with this code."),
                    }
                )
            else:
                # Check the account for changes
                notes = self.diff_notes(r_data, account_group)
                code_prefix_end = (
                    r_data["code_prefix_end"]
                    if "code_prefix_end" in r_data
                    and r_data["code_prefix_end"] < r_data["code_prefix_start"]
                    else r_data["code_prefix_start"]
                )
                if code_prefix_end != account_group.code_prefix_end:
                    notes += (notes and "\n" or "") + _(
                        "Differences in these fields: %s."
                    ) % r_data["code_prefix_end"]
                if self.missing_xml_id(account_group, xmlid):
                    notes += (notes and "\n" or "") + _("Missing XML-ID.")
                if notes:
                    # Account to be updated
                    ag_vals.append(
                        {
                            "xml_id": xmlid,
                            "update_chart_wizard_id": self.id,
                            "type": "updated",
                            "update_account_group_id": account_group.id,
                            "notes": notes,
                        }
                    )
        self.account_group_ids = [(5, 0, 0)] + [(0, 0, ag_val) for ag_val in ag_vals]

    def _find_fiscal_positions(self, t_data):
        """Load fiscal position template data to create/update."""
        fp_vals = []
        for xmlid, r_data in t_data.items():
            fp = self._find_record_matching("account.fiscal.position", xmlid, r_data)
            if not fp:
                # Fiscal position to be created
                fp_vals.append(
                    {
                        "xml_id": xmlid,
                        "update_chart_wizard_id": self.id,
                        "type": "new",
                        "notes": _("No fiscal position found with this name."),
                    }
                )
            else:
                # Check the fiscal position for changes
                notes = self.diff_notes(r_data, fp)
                if self.missing_xml_id(fp, xmlid):
                    notes += (notes and "\n" or "") + _("Missing XML-ID.")
                if notes:
                    # Fiscal position template to be updated
                    fp_vals.append(
                        {
                            "xml_id": xmlid,
                            "update_chart_wizard_id": self.id,
                            "type": "updated",
                            "update_fiscal_position_id": fp.id,
                            "notes": notes,
                        }
                    )
        self.fiscal_position_ids = [(5, 0, 0)] + [(0, 0, fp_val) for fp_val in fp_vals]

    def _load_data(self, model, data):
        """Process similar to the one in chart template _load() method."""
        template = self.env["account.chart.template"].with_context(
            default_company_id=self.company_id.id,
            allowed_company_ids=[self.company_id.id],
            tracking_disable=True,
            delay_account_group_sync=True,
            # lang="en_US",
        )
        created_records = template._load_data({model: data})[model]
        langs = self.env["res.lang"].search([])
        # Similar and simpler process than what the _load_translations() method does
        for xml_id, record_vals in data.items():
            if "__translation_module__" not in record_vals:
                continue
            translation_vals_lang = {}
            for f_name in record_vals["__translation_module__"].keys():
                for lang in langs:
                    short_lang = lang.code.split("_")[0]
                    key_lang = f"{f_name}@{short_lang}"
                    if key_lang in record_vals:
                        if lang not in translation_vals_lang:
                            translation_vals_lang[lang.code] = {}
                        translation_vals_lang[lang.code][f_name] = record_vals[key_lang]
            if isinstance(xml_id, int):
                record = self.env[model].browse(xml_id)
            else:
                prefix = f"account.{self.company_id.id}_" if "." not in xml_id else ""
                xml_id = f"{prefix}{xml_id}"
                record = self.env.ref(xml_id)
            # Updatr translation vals
            for lang in langs:
                if lang.code not in translation_vals_lang:
                    continue
                translation_vals = translation_vals_lang[lang.code]
                record.with_context(lang=lang.code).write(translation_vals)
        for record in created_records:
            msg = _(
                (f"Created/updated {record._name} %s."),
                f"'{record.name}' (ID:{record.id})",
            )
            _logger.info(msg)
            if not self.log:
                self.log = msg
            else:
                self.log += f"\n{msg}"

    def _update_tax_groups(self, t_data):
        """Process account groups templates to create/update."""
        data = {}
        for wiz_tg in self.tax_group_ids:
            tg = wiz_tg.update_tax_group_id
            xml_id = wiz_tg.xml_id
            key = tg.id or xml_id
            t_data_item = t_data[xml_id]
            data_item = t_data_item if wiz_tg.type == "new" else {}
            if wiz_tg.type == "updated":
                self.recreate_xml_id(tg, xml_id)
                data_item = self.diff_fields(t_data_item, tg)
            data[key] = data_item
        self._load_data("account.tax.group", data)

    def _update_taxes(self, t_data):
        """Process taxes to create/update/deactivate."""
        # First create taxes in batch
        data = {}
        for wiz_tax in self.tax_ids:
            tax = wiz_tax.update_tax_id
            if wiz_tax.type == "deleted":
                tax.active = False
                _logger.info(_("Deactivated tax %s."), tax.name)
                continue
            xml_id = wiz_tax.xml_id
            key = tax.id or xml_id
            t_data_item = t_data[xml_id]
            data_item = t_data_item if wiz_tax.type == "new" else {}
            if wiz_tax.type == "updated":
                self.recreate_xml_id(tax, xml_id)
                data_item = self.diff_fields(t_data_item, tax)
            # Do not set tax_group_id if it does not exist
            if wiz_tax.type == "new" and "tax_group_id" in data_item:
                tax_group_id_xml_id = data_item["tax_group_id"]
                real_tax_group_xml_id = (
                    f"account.{self.company_id.id}_{tax_group_id_xml_id}"
                )
                if not self.env.ref(real_tax_group_xml_id, raise_if_not_found=False):
                    del data_item["tax_group_id"]
            # Do not set repartition_line_ids lines linked to non-existent accounts
            if wiz_tax.type == "new" and "repartition_line_ids" in data_item:
                new_repartition_line_ids = []
                for line in data_item["repartition_line_ids"]:
                    if "account_id" in line[2]:
                        account_id_xml_id = line[2]["account_id"]
                        real_account_id_xml_id = (
                            f"account.{self.company_id.id}_{account_id_xml_id}"
                        )
                        if self.env.ref(
                            real_account_id_xml_id, raise_if_not_found=False
                        ):
                            new_repartition_line_ids.append(line)
                    else:
                        new_repartition_line_ids.append(line)
                data_item["repartition_line_ids"] = new_repartition_line_ids
            data[key] = data_item
        self._load_data("account.tax", data)

    def _update_accounts(self, t_data):
        """Process accounts to create/update."""
        data = {}
        for wiz_account in self.account_ids:
            account = wiz_account.update_account_id
            xml_id = wiz_account.xml_id
            key = account.id or xml_id
            t_data_item = t_data[xml_id]
            data_item = t_data_item if wiz_account.type == "new" else {}
            if wiz_account.type == "updated":
                self.recreate_xml_id(account, xml_id)
                data_item = self.diff_fields(t_data_item, account)
            else:
                data_item["code"] = self.padded_code(data_item["code"])
            data[key] = data_item
        self._load_data("account.account", data)

    def _update_account_groups(self, t_data):
        """Process account groups templates to create/update."""
        data = {}
        for wiz_ag in self.account_group_ids:
            ag = wiz_ag.update_account_group_id
            xml_id = wiz_ag.xml_id
            key = ag.id or xml_id
            t_data_item = t_data[xml_id]
            data_item = t_data_item if wiz_ag.type == "new" else {}
            if wiz_ag.type == "updated":
                self.recreate_xml_id(ag, xml_id)
                data_item = self.diff_fields(t_data_item, ag)
            data[key] = data_item
        self._load_data("account.group", data)

    def _update_fiscal_positions(self, t_data):
        """Process fiscal position templates to create/update."""
        data = {}
        for wiz_fp in self.fiscal_position_ids:
            fp = wiz_fp.update_fiscal_position_id
            xml_id = wiz_fp.xml_id
            key = fp.id or xml_id
            t_data_item = t_data[xml_id]
            data_item = t_data_item if wiz_fp.type == "new" else {}
            if wiz_fp.type == "updated":
                self.recreate_xml_id(fp, xml_id)
                data_item = self.diff_fields(t_data_item, fp)
            data[key] = data_item
        self._load_data("account.fiscal.position", data)


class WizardUpdateChartsAccountsTaxGroup(models.TransientModel):
    _name = "wizard.update.charts.accounts.tax.group"
    _description = (
        "Tax group that needs to be updated (new or updated in the template)."
    )

    xml_id = fields.Char()
    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    type = fields.Selection(
        selection=[
            ("new", "New tax group"),
            ("updated", "Updated tax group"),
        ],
        readonly=False,
    )
    update_tax_group_id = fields.Many2one(
        comodel_name="account.tax.group",
        string="Tax group to update",
        required=False,
        ondelete="set null",
    )
    notes = fields.Text(readonly=True)


class WizardUpdateChartsAccountsTax(models.TransientModel):
    _name = "wizard.update.charts.accounts.tax"
    _description = "Tax that needs to be updated (new or updated in the " "template)."

    xml_id = fields.Char()
    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    type = fields.Selection(
        selection=[
            ("new", "New tax"),
            ("updated", "Updated tax"),
            ("deleted", "Tax to deactivate"),
        ],
        readonly=False,
    )
    type_tax_use = fields.Selection(
        selection="_get_account_tax_type_tax_uses", readonly=True
    )
    update_tax_id = fields.Many2one(
        comodel_name="account.tax",
        string="Tax to update",
        required=False,
        ondelete="set null",
    )
    notes = fields.Text(readonly=True)

    def _get_account_tax_type_tax_uses(self):
        return self.env["account.tax"].fields_get(allfields=["type_tax_use"])[
            "type_tax_use"
        ]["selection"]


class WizardUpdateChartsAccountsAccount(models.TransientModel):
    _name = "wizard.update.charts.accounts.account"
    _description = (
        "Account that needs to be updated (new or updated in the " "template)."
    )

    xml_id = fields.Char()
    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    type = fields.Selection(
        selection=[("new", "New account"), ("updated", "Updated account")],
        readonly=False,
    )
    update_account_id = fields.Many2one(
        comodel_name="account.account",
        string="Account to update",
        required=False,
        ondelete="set null",
    )
    notes = fields.Text(readonly=True)


class WizardUpdateChartsAccountsAccountGroup(models.TransientModel):
    _name = "wizard.update.charts.accounts.account.group"
    _description = (
        "Account group that needs to be updated (new or updated in the template)."
    )

    xml_id = fields.Char()
    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    type = fields.Selection(
        selection=[("new", "New account group"), ("updated", "Updated accoung group")],
        readonly=False,
    )
    update_account_group_id = fields.Many2one(
        comodel_name="account.group",
        string="Account group to update",
        required=False,
        ondelete="set null",
    )
    notes = fields.Text(readonly=True)


class WizardUpdateChartsAccountsFiscalPosition(models.TransientModel):
    _name = "wizard.update.charts.accounts.fiscal.position"
    _description = (
        "Fiscal position that needs to be updated (new or updated " "in the template)."
    )

    xml_id = fields.Char()
    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    type = fields.Selection(
        selection=[
            ("new", "New fiscal position"),
            ("updated", "Updated fiscal position"),
        ],
        readonly=False,
    )
    update_fiscal_position_id = fields.Many2one(
        comodel_name="account.fiscal.position",
        required=False,
        string="Fiscal position to update",
        ondelete="set null",
    )
    notes = fields.Text(readonly=True)


class WizardMatching(models.TransientModel):
    _name = "wizard.matching"
    _description = "Wizard Matching"
    _order = "sequence"

    update_chart_wizard_id = fields.Many2one(
        comodel_name="wizard.update.charts.accounts",
        string="Update chart wizard",
        required=True,
        ondelete="cascade",
    )
    sequence = fields.Integer(required=True, default=1)
    matching_value = fields.Selection(selection="_get_matching_selection")

    def _get_matching_selection(self):
        return [("xml_id", "XML-ID")]

    def _selection_from_files(self, model_name, field_opts):
        result = []
        for opt in field_opts:
            model = self.env[model_name]
            desc = model._fields[opt].get_description(self.env)["string"]
            result.append((opt, f"{desc} ({opt})"))
        return result


class WizardTaxGroupMatching(models.TransientModel):
    _name = "wizard.tax.group.matching"
    _description = "Wizard Tax Group Matching"
    _inherit = "wizard.matching"

    def _get_matching_selection(self):
        vals = super()._get_matching_selection()
        vals += self._selection_from_files("account.tax.group", ["name"])
        return vals


class WizardTaxMatching(models.TransientModel):
    _name = "wizard.tax.matching"
    _description = "Wizard Tax Matching"
    _inherit = "wizard.matching"

    def _get_matching_selection(self):
        vals = super()._get_matching_selection()
        vals += self._selection_from_files("account.tax", ["description", "name"])
        return vals


class WizardAccountMatching(models.TransientModel):
    _name = "wizard.account.matching"
    _description = "Wizard Account Matching"
    _inherit = "wizard.matching"

    def _get_matching_selection(self):
        vals = super()._get_matching_selection()
        vals += self._selection_from_files("account.account", ["code", "name"])
        return vals


class WizardFpMatching(models.TransientModel):
    _name = "wizard.fp.matching"
    _description = "Wizard Fiscal Position Matching"
    _inherit = "wizard.matching"

    def _get_matching_selection(self):
        vals = super()._get_matching_selection()
        vals += self._selection_from_files("account.fiscal.position", ["name"])
        return vals


class WizardAccountGroupMatching(models.TransientModel):
    _name = "wizard.account.group.matching"
    _description = "Wizard Account Group Matching"
    _inherit = "wizard.matching"

    def _get_matching_selection(self):
        vals = super()._get_matching_selection()
        vals += self._selection_from_files("account.group", ["code_prefix_start"])
        return vals
