Files
Odoo-18.0-20251222/account_chart_update/wizard/wizard_chart_update.py
tocmo0nlord adbe430761
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled
Initial commit: Odoo 18.0-20251222 extra-addons
2026-03-13 20:43:25 +00:00

1299 lines
51 KiB
Python
Executable File

# 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