2
0
account-financial-tools/account_chart_update/wizard/wizard_chart_update.py
Lois Rilo ed1730598d [FIX] account_chart_update: do not repartion in groups + do append False IDs
1) In tax groups repartition lines are not used, however when creating
tax groups from templates the default repartition lines are created.
If you run the chart update again, it will detect those "useless"
default repartition lines and mark them to removal, raising an
error when trying to do so as a minial of 2 repartition lines are
needed (on base and one tax).

2) When matching taxes, if not match, do not add `False` to the list.
2023-07-12 11:06:04 +02:00

1355 lines
53 KiB
Python

# 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 Antonio Espinosa <antonioea@tecnativa.com>
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# Copyright 2016 Jacques-Etienne Baudoux <je@bcim.be>
# Copyright 2018 Tecnativa - Pedro M. Baeza
# Copyright 2020 Noviat - Luc De Meyer
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from contextlib import closing
from io import StringIO
from markupsafe import Markup
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(
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(compute="_compute_new_taxes_count")
new_accounts = fields.Integer(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(compute="_compute_updated_taxes_count")
rejected_updated_account_number = fields.Integer()
updated_accounts = fields.Integer(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_id = self.find_tax_by_templates(tax)
if tax_id:
tax_ids.append(tax_id)
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"]
plus_expressions = tpl.plus_report_expression_ids.filtered(
lambda x: x.engine == "tax_tags"
)
for expression in plus_expressions:
country = expression.report_line_id.report_id.country_id
existing_tags = self.env["account.account.tag"]._get_tax_tags(
expression.formula, country.id
)
tags |= existing_tags.filtered(lambda x: not x.tax_negate)
minus_expressions = tpl.minus_report_expression_ids.filtered(
lambda x: x.engine == "tax_tags"
)
for expression in minus_expressions:
country = expression.report_line_id.report_id.country_id
existing_tags = self.env["account.account.tag"]._get_tax_tags(
expression.formula, country.id
)
tags |= existing_tags.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 = {}
if upd_acc and existing.account_id.id != account_id:
upd_vals["account_id"] = account_id
if upd_tags:
if existing.tag_ids != tags:
upd_vals["tag_ids"] = [(6, 0, tags.ids)]
if upd_vals:
# update record
result.append((1, existing.id, upd_vals))
if not existing:
# create a new mapping
result.append(
(
0,
0,
{
inverse_name: tax.id,
"factor_percent": factor_percent,
"repartition_type": repartition_type,
"account_id": account_id,
"tag_ids": [(6, 0, tags.ids)],
},
)
)
if tax.amount_type != "group":
# Mark to be removed the lines not found
remove_ids = [x for x in current_repartition.ids if x not in existing_ids]
result += [(2, x) for x in remove_ids]
return result
@api.model
@tools.ormcache("code")
def padded_code(self, code):
"""Return a right-zero-padded code with the chosen digits."""
return code.ljust(self.code_digits, "0")
@tools.ormcache("templates")
def find_accounts_by_templates(self, templates):
account_ids = []
for account in templates:
account_ids.append(self.find_account_by_templates(account))
return self.env["account.account"].browse(account_ids)
@tools.ormcache("templates")
def find_account_by_templates(self, templates):
"""Find an account that matches the template."""
account_model = self.env["account.account"]
for matching in self.account_matching_ids.sorted("sequence"):
if matching.matching_value == "xml_id":
real = self.env["account.account"]
for template in templates:
try:
real |= self.env.ref(self._get_real_xml_name(template))
except BaseException:
_logger.info("Is not real xml Name")
if not real:
continue
criteria = ("id", "in", real.ids)
elif matching.matching_value == "code":
codes = templates.mapped("code")
if not codes:
continue
criteria = ("code", "in", list(map(self.padded_code, codes)))
else:
field_name = matching.matching_value
field_values = templates.mapped(field_name)
if not field_values:
continue
criteria = (field_name, "in", field_values)
result = account_model.search(
[criteria, ("company_id", "=", self.company_id.id)]
)
if result:
return result.id
return False
@tools.ormcache("templates")
def find_fp_by_templates(self, templates):
"""Find a real fiscal position from a template."""
fp_model = self.env["account.fiscal.position"]
for matching in self.fp_matching_ids.sorted("sequence"):
if matching.matching_value == "xml_id":
real = self.env["account.fiscal.position"]
for template in templates:
try:
real |= self.env.ref(self._get_real_xml_name(template))
except BaseException:
_logger.info("Is not real xml Name")
if not real:
continue
criteria = ("id", "in", real.ids)
else:
field_name = matching.matching_value
field_values = templates.mapped(field_name)
if not field_values:
continue
criteria = (field_name, "in", field_values)
result = fp_model.search(
[criteria, ("company_id", "=", self.company_id.id)], limit=1
)
if result:
return result.id
return False
@tools.ormcache("templates", "current_fp_accounts")
def find_fp_account_by_templates(self, templates, current_fp_accounts):
result = []
for tpl in templates:
pos_id = self.find_fp_by_templates(tpl.position_id)
src_id = self.find_account_by_templates(tpl.account_src_id)
dest_id = self.find_account_by_templates(tpl.account_dest_id)
existing = self.env["account.fiscal.position.account"].search(
[
("position_id", "=", pos_id),
("account_src_id", "=", src_id),
("account_dest_id", "=", dest_id),
]
)
if not existing:
# create a new mapping
result.append(
(
0,
0,
{
"position_id": pos_id,
"account_src_id": src_id,
"account_dest_id": dest_id,
},
)
)
else:
current_fp_accounts -= existing
# Mark to be removed the lines not found
if current_fp_accounts:
result += [(2, x.id) for x in current_fp_accounts]
return result
@tools.ormcache("templates", "current_fp_taxes")
def find_fp_tax_by_templates(self, templates, current_fp_taxes):
result = []
for tpl in templates:
pos_id = self.find_fp_by_templates(tpl.position_id)
src_id = self.find_tax_by_templates(tpl.tax_src_id)
dest_id = self.find_tax_by_templates(tpl.tax_dest_id)
existing = self.env["account.fiscal.position.tax"].search(
[
("position_id", "=", pos_id),
("tax_src_id", "=", src_id),
("tax_dest_id", "=", dest_id),
]
)
if not existing:
# create a new mapping
result.append(
(
0,
0,
{
"position_id": pos_id,
"tax_src_id": src_id,
"tax_dest_id": dest_id,
},
)
)
else:
current_fp_taxes -= existing
# Mark to be removed the lines not found
if current_fp_taxes:
result += [(2, x.id) for x in current_fp_taxes]
return result
@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.
"""
specials_mapping = {
"account.tax.template": {"chart_template_id", "children_tax_ids"},
"account.account.template": set(self.env["mail.thread"]._fields)
| {"chart_template_id", "root_id", "nocreate"},
"account.fiscal.position.template": {"chart_template_id"},
}
specials = {
"display_name",
"__last_update",
"company_id",
} | specials_mapping.get(name, set())
return set(models.MAGIC_COLUMNS) | specials
@api.model
def diff_fields(self, template, real): # noqa: C901
"""Get fields that are different in template and real records.
:param odoo.models.Model template:
Template record.
: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(template._name)
template_field_mapping = {
"account.tax.template": self.tax_field_ids,
"account.account.template": self.account_field_ids,
"account.fiscal.position.template": self.fp_field_ids,
}
to_include = template_field_mapping[template._name].mapped("name")
for key, field in template._fields.items():
if key in ignore or key not in to_include or not hasattr(real, key):
continue
expected = None
# Translate template records to reals for comparison
relation = field.get_description(self.env).get("relation", "")
if relation:
if relation == "account.tax.template":
expected = self.find_taxes_by_templates(template[key])
elif relation == "account.account.template":
expected = self.find_accounts_by_templates(template[key])
elif relation == "account.fiscal.position.tax.template":
expected = self.find_fp_tax_by_templates(template[key], real[key])
elif relation == "account.fiscal.position.account.template":
expected = self.find_fp_account_by_templates(
template[key], real[key]
)
elif relation == "account.tax.repartition.line.template":
expected = self.find_repartition_by_templates(
template[key], real[key], real, field.inverse_name
)
# Register detected differences
if expected is not None:
if expected != [] and (
key
in ["invoice_repartition_line_ids", "refund_repartition_line_ids"]
or (isinstance(expected, models.Model) and expected != real[key])
or (not isinstance(expected, models.Model))
):
result[key] = expected
else:
template_value, real_value = template[key], real[key]
if template._name == "account.account.template" and key == "code":
template_value = self.padded_code(template["code"])
# Normalize values when one field is Char and the other is Html
if (
isinstance(template_value, str) and isinstance(real_value, Markup)
) or (
isinstance(template_value, Markup) and isinstance(real_value, str)
):
template_value = Markup(template_value).striptags()
real_value = Markup(real_value).striptags()
if template_value != real_value:
result[key] = template_value
# Avoid to cache recordset references
if key in result:
if isinstance(real._fields[key], fields.Many2many):
result[key] = [(6, 0, result[key].ids)]
elif isinstance(real._fields[key], fields.Many2one):
result[key] = result[key].id
return result
@api.model
def diff_notes(self, template, real):
"""Get notes for humans on why is this record going to be updated.
:param openerp.models.Model template:
Template record.
:param openerp.models.Model real:
Real record.
:return str:
Notes result.
"""
result = list()
different_fields = sorted(
template._fields[f].get_description(self.env)["string"]
for f in self.diff_fields(template, real).keys()
)
if different_fields:
result.append(
_("Differences in these fields: %s.") % ", ".join(different_fields)
)
# Special for taxes
if template._name == "account.tax.template":
if not real.active:
result.append(_("Tax is disabled."))
return "\n".join(result)
@tools.ormcache("self", "template", "real_obj")
def missing_xml_id(self, template, real_obj):
ir_model_data = self.env["ir.model.data"]
template_xmlid = ir_model_data.search(
[("model", "=", template._name), ("res_id", "=", template.id)]
)
new_xml_id = "%d_%s" % (self.company_id.id, template_xmlid.name)
return not ir_model_data.search(
[
("res_id", "=", real_obj.id),
("model", "=", real_obj._name),
("module", "=", template_xmlid.module),
("name", "=", new_xml_id),
]
)
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_taxes(self):
"""Search for, and load, tax templates to create/update/delete."""
found_taxes_ids = []
self.tax_ids.unlink()
# Search for changes between template and real tax
for template in self.chart_template_ids.with_context(active_test=False).mapped(
"tax_template_ids"
):
# Check if the template matches a real tax
tax_id = self.find_tax_by_templates(template)
if not tax_id:
# Tax to be created
self.tax_ids.create(
{
"tax_id": template.id,
"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
tax = self.env["account.tax"].browse(tax_id)
notes = self.diff_notes(template, tax)
if self.recreate_xml_ids and self.missing_xml_id(template, tax):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Tax to be updated
self.tax_ids.create(
{
"tax_id": template.id,
"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:
self.tax_ids.create(
{
"update_chart_wizard_id": self.id,
"type": "deleted",
"update_tax_id": tax.id,
"notes": _("To deactivate: not in the template"),
}
)
def _find_accounts(self):
"""Load account templates to create/update."""
self.account_ids.unlink()
for template in self.chart_template_ids.mapped("account_ids"):
# Search for a real account that matches the template
account_id = self.find_account_by_templates(template)
if not account_id:
# Account to be created
self.account_ids.create(
{
"account_id": template.id,
"update_chart_wizard_id": self.id,
"type": "new",
"notes": _("No account found with this code."),
}
)
else:
# Check the account for changes
account = self.env["account.account"].browse(account_id)
notes = self.diff_notes(template, account)
if self.recreate_xml_ids and self.missing_xml_id(template, account):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Account to be updated
self.account_ids.create(
{
"account_id": template.id,
"update_chart_wizard_id": self.id,
"type": "updated",
"update_account_id": account_id,
"notes": notes,
}
)
def _find_fiscal_positions(self):
"""Load fiscal position templates to create/update."""
wiz_fp = self.env["wizard.update.charts.accounts.fiscal.position"]
self.fiscal_position_ids.unlink()
# Search for new / updated fiscal positions
templates = self.env["account.fiscal.position.template"].search(
[("chart_template_id", "in", self.chart_template_ids.ids)]
)
for template in templates:
# Search for a real fiscal position that matches the template
fp_id = self.find_fp_by_templates(template)
if not fp_id:
# Fiscal position to be created
wiz_fp.create(
{
"fiscal_position_id": template.id,
"update_chart_wizard_id": self.id,
"type": "new",
"notes": _("No fiscal position found with this name."),
}
)
else:
# Check the fiscal position for changes
fp = self.env["account.fiscal.position"].browse(fp_id)
notes = self.diff_notes(template, fp)
if self.recreate_xml_ids and self.missing_xml_id(template, fp):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Fiscal position template to be updated
wiz_fp.create(
{
"fiscal_position_id": template.id,
"update_chart_wizard_id": self.id,
"type": "updated",
"update_fiscal_position_id": fp_id,
"notes": notes,
}
)
def recreate_xml_id(self, template, real_obj):
ir_model_data = self.env["ir.model.data"]
template_xmlid = ir_model_data.search(
[("model", "=", template._name), ("res_id", "=", template.id)]
)
new_xml_id = "%d_%s" % (self.company_id.id, template_xmlid.name)
ir_model_data.search(
[("model", "=", real_obj._name), ("res_id", "=", real_obj.id)]
).unlink()
template_xmlid.copy(
{
"model": real_obj._name,
"res_id": real_obj.id,
"name": new_xml_id,
"noupdate": True,
}
)
def _update_taxes(self):
"""Process taxes to create/update/deactivate."""
# First create taxes in batch
taxes_to_create = self.tax_ids.filtered(lambda x: x.type == "new")
todo_dict = taxes_to_create.mapped("tax_id")._generate_tax(self.company_id)
template_to_tax_dict = {}
for key in todo_dict["tax_template_to_tax"].keys():
template_to_tax_dict[key.id] = todo_dict["tax_template_to_tax"][key].id
for wiz_tax in taxes_to_create:
new_tax = self.env["account.tax"].browse(
template_to_tax_dict[wiz_tax.tax_id.id]
)
_logger.info(
_("Created tax %s."), "'{}' (ID:{})".format(new_tax.name, new_tax.id)
)
for wiz_tax in self.tax_ids.filtered(lambda x: x.type != "new"):
template, tax = wiz_tax.tax_id, wiz_tax.update_tax_id
# Deactivate tax
if wiz_tax.type == "deleted":
tax.active = False
_logger.info(_("Deactivated tax %s."), "'%s'" % tax.name)
continue
else:
updated = False
for key, value in self.diff_fields(template, tax).items():
# We defer update because account might not be created yet
if key in [
"cash_basis_transition_account_id",
"invoice_repartition_line_ids",
"refund_repartition_line_ids",
]:
continue
tax[key] = value
updated = True
if updated:
_logger.info(_("Updated tax %s."), "'%s'" % template.name)
if self.recreate_xml_ids and self.missing_xml_id(template, tax):
self.recreate_xml_id(template, tax)
_logger.info(
_("Updated tax %s. (Recreated XML-IDs)"), "'%s'" % template.name
)
return todo_dict
def _update_accounts(self):
"""Process accounts to create/update."""
for wiz_account in self.account_ids:
account, template = (wiz_account.update_account_id, wiz_account.account_id)
if wiz_account.type == "new":
# Create the account
tax_template_ref = {
tax.id: self.find_tax_by_templates(tax) for tax in template.tax_ids
}
vals = self.chart_template_id._get_account_vals(
self.company_id,
template,
self.padded_code(template.code),
tax_template_ref,
)
try:
with self.env.cr.savepoint():
self.chart_template_id.create_record_with_xmlid(
self.company_id, template, "account.account", vals
)
_logger.info(
_("Created account %s."),
"'{} - {}'".format(vals["code"], vals["name"]),
)
except Exception:
self.rejected_new_account_number += 1
if config["test_enable"]:
_logger.info(EXCEPTION_TEXT)
else: # pragma: no cover
_logger.exception(
"ERROR: " + _("Exception creating account %s."),
"'{} - {}'".format(template.code, template.name),
)
if not self.continue_on_errors:
break
else:
# Update the account
try:
with self.env.cr.savepoint():
for key, value in iter(
self.diff_fields(template, account).items()
):
account[key] = value
_logger.info(
_("Updated account %s."),
"'{} - {}'".format(account.code, account.name),
)
if self.recreate_xml_ids and self.missing_xml_id(
template, account
):
self.recreate_xml_id(template, account)
_logger.info(
_("Updated account %s. (Recreated XML-ID)"),
"'{} - {}'".format(account.code, account.name),
)
except Exception:
self.rejected_updated_account_number += 1
if config["test_enable"]:
_logger.info(EXCEPTION_TEXT)
else: # pragma: no cover
_logger.exception(
"ERROR: " + _("Exception writing account %s."),
"'{} - {}'".format(account.code, account.name),
)
if not self.continue_on_errors:
break
def _update_taxes_pending_for_accounts(self, todo_dict):
"""Updates the taxes (created or updated on previous steps) to set
the references to the accounts (the taxes where created/updated first,
when the referenced accounts are still not available).
"""
done = self.env["account.tax"]
for k, v in todo_dict["account_dict"]["account.tax"].items():
vals = {}
for fld in [
"cash_basis_transition_account_id",
]:
if v[fld]:
acc_id = self.find_account_by_templates(
self.env["account.account.template"].browse(v[fld].id)
)
if acc_id:
vals[fld] = acc_id
else:
raise exceptions.UserError(
_("No real account found for template account with ID %s")
% v[fld].id
)
if vals:
tax = self.env["account.tax"].browse(k)
tax.write(vals)
done |= tax
for rep_line, v in todo_dict["account_dict"][
"account.tax.repartition.line"
].items():
if v["account_id"]:
acc_id = self.find_account_by_templates(
self.env["account.account.template"].browse(v["account_id"].id)
)
if acc_id:
rep_line.write({"account_id": acc_id})
done |= rep_line.invoice_tax_id or rep_line.refund_tax_id
else:
raise exceptions.UserError(
_("No real account found for template account with ID %s")
% v["account_id"].id
)
for wiz_tax in self.tax_ids.filtered(lambda r: r.type == "updated"):
template = wiz_tax.tax_id
tax = wiz_tax.update_tax_id
vals = {}
for key, value in self.diff_fields(template, tax).items():
if key in {
"cash_basis_transition_account_id",
"invoice_repartition_line_ids",
"refund_repartition_line_ids",
}:
vals[key] = value
if vals:
tax.write(vals)
done |= tax
if done:
_logger.info(
_("Post-updated account fields for taxes with IDs %s."), "%s" % done.ids
)
def _prepare_fp_vals(self, fp_template):
# Tax mappings
tax_mapping = []
for fp_tax in fp_template.tax_ids:
# Create the fp tax mapping
tax_mapping.append(
{
"tax_src_id": self.find_tax_by_templates(fp_tax.tax_src_id),
"tax_dest_id": self.find_tax_by_templates(fp_tax.tax_dest_id),
}
)
# Account mappings
account_mapping = []
for fp_account in fp_template.account_ids:
# Create the fp account mapping
account_mapping.append(
{
"account_src_id": (
self.find_account_by_templates(fp_account.account_src_id)
),
"account_dest_id": (
self.find_account_by_templates(fp_account.account_dest_id)
),
}
)
return {
"company_id": self.company_id.id,
"name": fp_template.name,
"tax_ids": [(0, 0, x) for x in tax_mapping],
"account_ids": [(0, 0, x) for x in account_mapping],
}
def _update_fiscal_positions(self):
"""Process fiscal position templates to create/update."""
for wiz_fp in self.fiscal_position_ids:
fp, template = (wiz_fp.update_fiscal_position_id, wiz_fp.fiscal_position_id)
if wiz_fp.type == "new":
# Create a new fiscal position
self.chart_template_id.create_record_with_xmlid(
self.company_id,
template,
"account.fiscal.position",
self._prepare_fp_vals(template),
)
_logger.info(_("Created fiscal position %s."), "'%s'" % template.name)
else:
for key, value in self.diff_fields(template, fp).items():
fp[key] = value
_logger.info(
_("Updated fiscal position %s."), "'%s'" % template.name
)
if self.recreate_xml_ids and self.missing_xml_id(template, fp):
self.recreate_xml_id(template, fp)
_logger.info(
_("Updated fiscal position %s. (Recreated XML-ID)"),
"'%s'" % template.name,
)
class WizardUpdateChartsAccountsTax(models.TransientModel):
_name = "wizard.update.charts.accounts.tax"
_description = "Tax that needs to be updated (new or updated in the " "template)."
tax_id = fields.Many2one(
comodel_name="account.tax.template", string="Tax template", ondelete="set null"
)
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 template"),
("updated", "Updated template"),
("deleted", "Tax to deactivate"),
],
readonly=False,
)
type_tax_use = fields.Selection(related="tax_id.type_tax_use", 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)
recreate_xml_ids = fields.Boolean(related="update_chart_wizard_id.recreate_xml_ids")
class WizardUpdateChartsAccountsAccount(models.TransientModel):
_name = "wizard.update.charts.accounts.account"
_description = (
"Account that needs to be updated (new or updated in the " "template)."
)
account_id = fields.Many2one(
comodel_name="account.account.template",
string="Account template",
required=True,
)
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 template"), ("updated", "Updated template")],
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)
recreate_xml_ids = fields.Boolean(related="update_chart_wizard_id.recreate_xml_ids")
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)."
)
fiscal_position_id = fields.Many2one(
comodel_name="account.fiscal.position.template",
string="Fiscal position template",
required=True,
)
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 template"), ("updated", "Updated template")],
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)
recreate_xml_ids = fields.Boolean(
string="Recreate missing XML-IDs",
related="update_chart_wizard_id.recreate_xml_ids",
)
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, "{} ({})".format(desc, opt)))
return result
class WizardTaxMatching(models.TransientModel):
_name = "wizard.tax.matching"
_description = "Wizard Tax Matching"
_inherit = "wizard.matching"
def _get_matching_selection(self):
vals = super(WizardTaxMatching, self)._get_matching_selection()
vals += self._selection_from_files(
"account.tax.template", ["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(WizardAccountMatching, self)._get_matching_selection()
vals += self._selection_from_files("account.account.template", ["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(WizardFpMatching, self)._get_matching_selection()
vals += self._selection_from_files("account.fiscal.position.template", ["name"])
return vals