<commit_before># Copyright 2010 <NAME>, Zikzakmedia S.L. (http://www.zikzakmedia.com)
# Copyright 2010 Pexego Sistemas Informáticos S.L.(http://www.pexego.es)
#        <NAME>
# Copyright 2013 <NAME> (http://www.gutierrezweb.es)
# Copyright 2015 <NAME> <<EMAIL>>
# Copyright 2016 <NAME> <<EMAIL>>
# Copyright 2016 <NAME> <<EMAIL>>
# Copyright 2018 Tecnativa - <NAME>
# Copyright 2020 Noviat - <NAME>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging
from contextlib import closing
from io import StringIO

from odoo import _, api, exceptions, fields, models, tools
from odoo.tools import config

_logger = logging.getLogger(__name__)
EXCEPTION_TEXT = "Traceback (most recent call last)"


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_id = fields.Many2one(
        comodel_name="account.chart.template",
        string="Chart Template",
        ondelete="cascade",
        required=True,
    )
    chart_template_ids = fields.Many2many(
        "account.chart.template",
        string="Chart Templates",
        compute="_compute_chart_template_ids",
        help="Includes all chart templates.",
    )
    code_digits = fields.Integer(related="chart_template_id.code_digits")
    lang = fields.Selection(
        lambda self: self._get_lang_selection_options(),
        "Language",
        required=True,
        help="For records searched by name (taxes, fiscal "
        "positions), the template name will be matched against the "
        "record name on this language.",
        default=lambda self: self.env.context.get("lang", self.env.user.lang),
    )
    update_tax = fields.Boolean(
        string="Update taxes",
        default=True,
        help="Existing taxes are updated. Taxes are searched by name.",
    )
    update_tax_repartition_line_account = fields.Boolean(
        string="Update Tax Accounts",
        default=True,
        help="Update account_id field on existing Tax repartition lines",
    )
    update_tax_repartition_line_tags = fields.Boolean(
        string="Update Tax Tags",
        default=True,
        help="Update tag_ids field on existing Tax repartition lines",
    )
    update_account = fields.Boolean(
        string="Update accounts",
        default=True,
        help="Existing accounts are updated. Accounts are searched by code.",
    )
    update_fiscal_position = fields.Boolean(
        string="Update fiscal positions",
        default=True,
        help="Existing fiscal positions are updated. Fiscal positions are "
        "searched by name.",
    )
    continue_on_errors = fields.Boolean(
        string="Continue on errors",
        default=False,
        help="If set, the wizard will continue to the next step even if "
        "there are minor errors.",
    )
    recreate_xml_ids = fields.Boolean(string="Recreate missing XML-IDs")
    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",
    )
    fiscal_position_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.fiscal.position",
        inverse_name="update_chart_wizard_id",
        string="Fiscal positions",
    )
    new_taxes = fields.Integer(string="New taxes", compute="_compute_new_taxes_count")
    new_accounts = fields.Integer(
        string="New accounts", compute="_compute_new_accounts_count"
    )
    rejected_new_account_number = fields.Integer()
    new_fps = fields.Integer(
        string="New fiscal positions", compute="_compute_new_fps_count"
    )
    updated_taxes = fields.Integer(
        string="Updated taxes", compute="_compute_updated_taxes_count"
    )
    rejected_updated_account_number = fields.Integer()
    updated_accounts = fields.Integer(
        string="Updated accounts", compute="_compute_updated_accounts_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_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(),
    )
    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_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(),
    )
    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_field_ids(self):
        return self._domain_per_name("account.tax.template")

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

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

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

    def _default_account_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_account_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_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)

    @api.model
    def _get_lang_selection_options(self):
        """Gets the available languages for the selection."""
        langs = self.env["res.lang"].search([])
        return [(lang.code, lang.name) for lang in langs]

    @api.depends("chart_template_id")
    def _compute_chart_template_ids(self):
        all_parents = self.chart_template_id._get_chart_parent_ids()
        self.chart_template_ids = all_parents

    @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("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_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("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_id = self.company_id.chart_template_id

    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_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 action_find_records(self):
        """Searchs for records to update/create and shows them."""
        self.clear_caches()
        self = self.with_context(lang=self.lang)
        # Search for, and load, the records to create/update.
        if self.update_tax:
            self._find_taxes()
        if self.update_account:
            self._find_accounts()
        if self.update_fiscal_position:
            self._find_fiscal_positions()
        # Write the results, and go to the next step.
        self.state = "ready"
        return self._reopen()

    def _check_consistency(self):
        """Method for assuring consistency in operations before performing
        them. For now, implemented:

        - If a parent tax is tried to be created, children taxes must be
          also included to be created.

        TODO:

        - Check that needed accounts in taxes/FPs are created at the same time.
        - Check that needed taxes in FPs are created at the same time.
        """
        taxes2create = self.tax_ids.filtered(lambda x: x.type == "new")
        parents2create = taxes2create.filtered(lambda x: x.tax_id.children_tax_ids)
        for parent in parents2create:
            if bool(
                parent.tax_id.children_tax_ids - taxes2create.mapped("tax_id")
            ):  # some children taxes are not included to be added
                raise exceptions.UserError(
                    _(
                        "You have at least one parent tax template (%s) whose "
                        "children taxes are not going to be created. Aborting "
                        "as this will provoke an infinite loop. Please check "
                        "if children have been matched, but not the parent one."
                    )
                    % parent.tax_id.name
                )

    def action_update_records(self):
        """Action that creates/updates/deletes the selected elements."""
        self._check_consistency()
        self = self.with_context(lang=self.lang)
        self.rejected_new_account_number = 0
        self.rejected_updated_account_number = 0
        with closing(StringIO()) as log_output:
            handler = logging.StreamHandler(log_output)
            _logger.addHandler(handler)
            # Create or update the records.
            if self.update_tax:
                todo_dict = self._update_taxes()
            perform_rest = True
            if self.update_account:
                self._update_accounts()
                if (
                    EXCEPTION_TEXT in log_output.getvalue()
                    and not self.continue_on_errors
                ):  # Abort early
                    perform_rest = False
            # Clear this cache for avoiding incorrect account hits (as it was
            # queried before account creation)
            self.find_account_by_templates.clear_cache(self)
            if self.update_tax and perform_rest:
                self._update_taxes_pending_for_accounts(todo_dict)
            if self.update_fiscal_position and perform_rest:
                self._update_fiscal_positions()
            # Store new chart in the company
            self.company_id.chart_template_id = self.chart_template_id
            _logger.removeHandler(handler)
            self.log = log_output.getvalue()
        # Check if errors where detected and wether we should stop.
        if EXCEPTION_TEXT in self.log and not self.continue_on_errors:
            raise exceptions.UserError(
                _("One or more errors detected!\n\n%s") % self.log
            )
        # Store the data and go to the next step.
        self.state = "done"
        return self._reopen()

    def _get_real_xml_name(self, template):
        [external_id] = template.get_external_id().values()
        (name, module) = external_id.split(".")
        return "%s.%d_%s" % (name, self.company_id.id, module)

    @tools.ormcache("templates")
    def find_taxes_by_templates(self, templates):
        tax_ids = []
        for tax in templates:
            tax_ids.append(self.find_tax_by_templates(tax))
        return self.env["account.tax"].browse(tax_ids)

    @tools.ormcache("templates")
    def find_tax_by_templates(self, templates):
        """Find a tax that matches the template."""
        # search inactive taxes too, to avoid re-creating
        # taxes that have been deactivated before
        tax_model = self.env["account.tax"].with_context(active_test=False)
        for template in templates:
            for matching in self.tax_matching_ids.sorted("sequence"):
                if matching.matching_value == "xml_id":
                    real = self.env.ref(
                        self._get_real_xml_name(template), raise_if_not_found=False
                    )
                    if not real:
                        continue
                    criteria = ("id", "=", real.id)
                else:
                    field_name = matching.matching_value
                    if not template[field_name]:
                        continue
                    criteria = (field_name, "=", template[field_name])

                result = tax_model.search(
                    [
                        criteria,
                        ("company_id", "=", self.company_id.id),
                        ("type_tax_use", "=", template.type_tax_use),
                    ],
                    limit=1,
                )
                if result:
                    return result.id

        return False

    @tools.ormcache("templates", "current_repartition")
    def find_repartition_by_templates(
        self, templates, current_repartition, tax, inverse_name
    ):
        upd_acc = self.update_tax_repartition_line_account
        upd_tags = self.update_tax_repartition_line_tags
        result = []
        existing_ids = []
        for i, tpl in enumerate(templates):
            factor_percent = tpl.factor_percent
            repartition_type = tpl.repartition_type
            account_id = self.find_account_by_templates(tpl.account_id)
            tags = self.env["account.account.tag"]
            tags += tpl.plus_report_line_ids.mapped("tag_ids").filtered(
                lambda x: not x.tax_negate
            )
            tags += tpl.minus_report_line_ids.mapped("tag_ids").filtered(
                lambda x: x.tax_negate
            )
            tags += tpl.tag_ids
            existing = self.env["account.tax.repartition.line"]
            existing_candidates = current_repartition.filtered(
                lambda r: r.factor_percent == factor_percent
                and r.repartition_type == repartition_type
            )
            if len(existing_candidates) == 1:
                existing = existing_candidates
            elif len(existing_candidates) > 1:
                # We may have this situation in case of e.g. 50%/50% on tax.
                # In this case we assume that the repartition line order
                # is the same between templates and actual tax objects
                existing_candidate = current_repartition[i]
                if existing_candidate in existing_candidates:
                    existing = existing_candidate

            if existing:
                existing_ids.append(existing.id)
                upd_vals = {}<commit_msg>[FIX] account_chart_update: do not match twice the same repartition line<commit_after># Copyright 2010 <NAME>, Zikzakmedia S.L. (http://www.zikzakmedia.com)
# Copyright 2010 Pexego Sistemas Informáticos S.L.(http://www.pexego.es)
#        <NAME>
# Copyright 2013 <NAME> (http://www.gutierrezweb.es)
# Copyright 2015 <NAME> <<EMAIL>>
# Copyright 2016 <NAME> <<EMAIL>>
# Copyright 2016 <NAME> <<EMAIL>>
# Copyright 2018 Tecnativa - <NAME>
# Copyright 2020 Noviat - <NAME>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging
from contextlib import closing
from io import StringIO

from odoo import _, api, exceptions, fields, models, tools
from odoo.tools import config

_logger = logging.getLogger(__name__)
EXCEPTION_TEXT = "Traceback (most recent call last)"


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_id = fields.Many2one(
        comodel_name="account.chart.template",
        string="Chart Template",
        ondelete="cascade",
        required=True,
    )
    chart_template_ids = fields.Many2many(
        "account.chart.template",
        string="Chart Templates",
        compute="_compute_chart_template_ids",
        help="Includes all chart templates.",
    )
    code_digits = fields.Integer(related="chart_template_id.code_digits")
    lang = fields.Selection(
        lambda self: self._get_lang_selection_options(),
        "Language",
        required=True,
        help="For records searched by name (taxes, fiscal "
        "positions), the template name will be matched against the "
        "record name on this language.",
        default=lambda self: self.env.context.get("lang", self.env.user.lang),
    )
    update_tax = fields.Boolean(
        string="Update taxes",
        default=True,
        help="Existing taxes are updated. Taxes are searched by name.",
    )
    update_tax_repartition_line_account = fields.Boolean(
        string="Update Tax Accounts",
        default=True,
        help="Update account_id field on existing Tax repartition lines",
    )
    update_tax_repartition_line_tags = fields.Boolean(
        string="Update Tax Tags",
        default=True,
        help="Update tag_ids field on existing Tax repartition lines",
    )
    update_account = fields.Boolean(
        string="Update accounts",
        default=True,
        help="Existing accounts are updated. Accounts are searched by code.",
    )
    update_fiscal_position = fields.Boolean(
        string="Update fiscal positions",
        default=True,
        help="Existing fiscal positions are updated. Fiscal positions are "
        "searched by name.",
    )
    continue_on_errors = fields.Boolean(
        string="Continue on errors",
        default=False,
        help="If set, the wizard will continue to the next step even if "
        "there are minor errors.",
    )
    recreate_xml_ids = fields.Boolean(string="Recreate missing XML-IDs")
    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",
    )
    fiscal_position_ids = fields.One2many(
        comodel_name="wizard.update.charts.accounts.fiscal.position",
        inverse_name="update_chart_wizard_id",
        string="Fiscal positions",
    )
    new_taxes = fields.Integer(string="New taxes", compute="_compute_new_taxes_count")
    new_accounts = fields.Integer(
        string="New accounts", compute="_compute_new_accounts_count"
    )
    rejected_new_account_number = fields.Integer()
    new_fps = fields.Integer(
        string="New fiscal positions", compute="_compute_new_fps_count"
    )
    updated_taxes = fields.Integer(
        string="Updated taxes", compute="_compute_updated_taxes_count"
    )
    rejected_updated_account_number = fields.Integer()
    updated_accounts = fields.Integer(
        string="Updated accounts", compute="_compute_updated_accounts_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_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(),
    )
    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_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(),
    )
    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_field_ids(self):
        return self._domain_per_name("account.tax.template")

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

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

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

    def _default_account_field_ids(self):
        return [
            (4, x.id)
            for x in self.env["ir.model.fields"].search(
                self._domain_account_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_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)

    @api.model
    def _get_lang_selection_options(self):
        """Gets the available languages for the selection."""
        langs = self.env["res.lang"].search([])
        return [(lang.code, lang.name) for lang in langs]

    @api.depends("chart_template_id")
    def _compute_chart_template_ids(self):
        all_parents = self.chart_template_id._get_chart_parent_ids()
        self.chart_template_ids = all_parents

    @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("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_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("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_id = self.company_id.chart_template_id

    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_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 action_find_records(self):
        """Searchs for records to update/create and shows them."""
        self.clear_caches()
        self = self.with_context(lang=self.lang)
        # Search for, and load, the records to create/update.
        if self.update_tax:
            self._find_taxes()
        if self.update_account:
            self._find_accounts()
        if self.update_fiscal_position:
            self._find_fiscal_positions()
        # Write the results, and go to the next step.
        self.state = "ready"
        return self._reopen()

    def _check_consistency(self):
        """Method for assuring consistency in operations before performing
        them. For now, implemented:

        - If a parent tax is tried to be created, children taxes must be
          also included to be created.

        TODO:

        - Check that needed accounts in taxes/FPs are created at the same time.
        - Check that needed taxes in FPs are created at the same time.
        """
        taxes2create = self.tax_ids.filtered(lambda x: x.type == "new")
        parents2create = taxes2create.filtered(lambda x: x.tax_id.children_tax_ids)
        for parent in parents2create:
            if bool(
                parent.tax_id.children_tax_ids - taxes2create.mapped("tax_id")
            ):  # some children taxes are not included to be added
                raise exceptions.UserError(
                    _(
                        "You have at least one parent tax template (%s) whose "
                        "children taxes are not going to be created. Aborting "
                        "as this will provoke an infinite loop. Please check "
                        "if children have been matched, but not the parent one."
                    )
                    % parent.tax_id.name
                )

    def action_update_records(self):
        """Action that creates/updates/deletes the selected elements."""
        self._check_consistency()
        self = self.with_context(lang=self.lang)
        self.rejected_new_account_number = 0
        self.rejected_updated_account_number = 0
        with closing(StringIO()) as log_output:
            handler = logging.StreamHandler(log_output)
            _logger.addHandler(handler)
            # Create or update the records.
            if self.update_tax:
                todo_dict = self._update_taxes()
            perform_rest = True
            if self.update_account:
                self._update_accounts()
                if (
                    EXCEPTION_TEXT in log_output.getvalue()
                    and not self.continue_on_errors
                ):  # Abort early
                    perform_rest = False
            # Clear this cache for avoiding incorrect account hits (as it was
            # queried before account creation)
            self.find_account_by_templates.clear_cache(self)
            if self.update_tax and perform_rest:
                self._update_taxes_pending_for_accounts(todo_dict)
            if self.update_fiscal_position and perform_rest:
                self._update_fiscal_positions()
            # Store new chart in the company
            self.company_id.chart_template_id = self.chart_template_id
            _logger.removeHandler(handler)
            self.log = log_output.getvalue()
        # Check if errors where detected and wether we should stop.
        if EXCEPTION_TEXT in self.log and not self.continue_on_errors:
            raise exceptions.UserError(
                _("One or more errors detected!\n\n%s") % self.log
            )
        # Store the data and go to the next step.
        self.state = "done"
        return self._reopen()

    def _get_real_xml_name(self, template):
        [external_id] = template.get_external_id().values()
        (name, module) = external_id.split(".")
        return "%s.%d_%s" % (name, self.company_id.id, module)

    @tools.ormcache("templates")
    def find_taxes_by_templates(self, templates):
        tax_ids = []
        for tax in templates:
            tax_ids.append(self.find_tax_by_templates(tax))
        return self.env["account.tax"].browse(tax_ids)

    @tools.ormcache("templates")
    def find_tax_by_templates(self, templates):
        """Find a tax that matches the template."""
        # search inactive taxes too, to avoid re-creating
        # taxes that have been deactivated before
        tax_model = self.env["account.tax"].with_context(active_test=False)
        for template in templates:
            for matching in self.tax_matching_ids.sorted("sequence"):
                if matching.matching_value == "xml_id":
                    real = self.env.ref(
                        self._get_real_xml_name(template), raise_if_not_found=False
                    )
                    if not real:
                        continue
                    criteria = ("id", "=", real.id)
                else:
                    field_name = matching.matching_value
                    if not template[field_name]:
                        continue
                    criteria = (field_name, "=", template[field_name])

                result = tax_model.search(
                    [
                        criteria,
                        ("company_id", "=", self.company_id.id),
                        ("type_tax_use", "=", template.type_tax_use),
                    ],
                    limit=1,
                )
                if result:
                    return result.id

        return False

    @tools.ormcache("templates", "current_repartition")
    def find_repartition_by_templates(
        self, templates, current_repartition, tax, inverse_name
    ):
        upd_acc = self.update_tax_repartition_line_account
        upd_tags = self.update_tax_repartition_line_tags
        result = []
        existing_ids = []
        for i, tpl in enumerate(templates):
            factor_percent = tpl.factor_percent
            repartition_type = tpl.repartition_type
            account_id = self.find_account_by_templates(tpl.account_id)
            tags = self.env["account.account.tag"]
            tags += tpl.plus_report_line_ids.mapped("tag_ids").filtered(
                lambda x: not x.tax_negate
            )
            tags += tpl.minus_report_line_ids.mapped("tag_ids").filtered(
                lambda x: x.tax_negate
            )
            tags += tpl.tag_ids
            existing = self.env["account.tax.repartition.line"]
            existing_candidates = current_repartition.filtered(
                lambda r: r.factor_percent == factor_percent
                and r.repartition_type == repartition_type
                and r.id not in existing_ids
            )
            if len(existing_candidates) == 1:
                existing = existing_candidates
            elif len(existing_candidates) > 1:
                # We may have this situation in case of e.g. 50%/50% on tax.
                # In this case we assume that the repartition line order
                # is the same between templates and actual tax objects
                existing_candidate = current_repartition[i]
                if existing_candidate in existing_candidates:
                    existing = existing_candidate

            if existing:
                existing_ids.append(existing.id)
                upd_vals = {}
{'fraction_non_alphanumeric': 0.09809133925248552, 'fraction_numerical': 0.0037747886650007973, 'mean_word_length': 2.312698133145474, 'pattern_counts': {'":': 32, '<': 31, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 8, 'xml': 14}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1364296', 'n_tokens_mistral': 11045, 'n_tokens_neox': 10602, 'n_words': 2314}