809 lines
28 KiB
Python
809 lines
28 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import OrderedDict, defaultdict
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tools import groupby, topological_sort
|
|
from odoo.tools.misc import OrderedSet
|
|
|
|
|
|
# List of models to export (the order ensures that dependencies are satisfied)
|
|
DEFAULT_MODELS_TO_EXPORT = [
|
|
"res.groups",
|
|
"res.groups.privilege",
|
|
"ir.module.category",
|
|
"report.paperformat",
|
|
"ir.model",
|
|
"ir.model.fields",
|
|
"ir.ui.view",
|
|
"ir.actions.act_window",
|
|
"ir.actions.act_window.view",
|
|
"ir.actions.report",
|
|
"mail.template",
|
|
"ir.actions.server",
|
|
"ir.ui.menu",
|
|
"ir.filters",
|
|
"base.automation",
|
|
"ir.model.access",
|
|
"ir.rule",
|
|
"ir.default",
|
|
"studio.approval.rule",
|
|
]
|
|
|
|
# List of fields to export by model
|
|
FIELDS_TO_EXPORT = {
|
|
"base.automation": [
|
|
"action_server_ids",
|
|
"active",
|
|
"description",
|
|
"filter_domain",
|
|
"filter_pre_domain",
|
|
"last_run",
|
|
"log_webhook_calls",
|
|
"model_id",
|
|
"name",
|
|
"on_change_field_ids",
|
|
"record_getter",
|
|
"trg_date_id",
|
|
"trg_date_range_type",
|
|
"trg_date_range",
|
|
"trg_field_ref",
|
|
"trg_selection_field_id",
|
|
"trigger_field_ids",
|
|
"trigger",
|
|
],
|
|
"ir.actions.act_window": [
|
|
"binding_model_id",
|
|
"binding_type",
|
|
"binding_view_types",
|
|
"context",
|
|
"domain",
|
|
"filter",
|
|
"group_ids",
|
|
"help",
|
|
"limit",
|
|
"name",
|
|
"res_model",
|
|
"search_view_id",
|
|
"target",
|
|
"type",
|
|
"usage",
|
|
"view_id",
|
|
"view_mode",
|
|
],
|
|
"ir.actions.act_window.view": [
|
|
"act_window_id",
|
|
"multi",
|
|
"sequence",
|
|
"view_id",
|
|
"view_mode",
|
|
],
|
|
"ir.actions.report": [
|
|
"attachment",
|
|
"attachment_use",
|
|
"binding_model_id",
|
|
"binding_type",
|
|
"binding_view_types",
|
|
"group_ids",
|
|
"model",
|
|
"multi",
|
|
"name",
|
|
"paperformat_id",
|
|
"report_name",
|
|
"report_type",
|
|
],
|
|
"ir.actions.server": [
|
|
"binding_model_id",
|
|
"binding_type",
|
|
"binding_view_types",
|
|
"child_ids",
|
|
"code",
|
|
"crud_model_id",
|
|
"evaluation_type",
|
|
"group_ids",
|
|
"help",
|
|
"link_field_id",
|
|
"model_id",
|
|
"name",
|
|
"resource_ref",
|
|
"selection_value",
|
|
"sequence",
|
|
"state",
|
|
"update_boolean_value",
|
|
"update_field_id",
|
|
"update_m2m_operation",
|
|
"update_path",
|
|
"usage",
|
|
"value",
|
|
"webhook_field_ids",
|
|
"webhook_url",
|
|
],
|
|
"ir.filters": [
|
|
"action_id",
|
|
"active",
|
|
"context",
|
|
"domain",
|
|
"is_default",
|
|
"model_id",
|
|
"name",
|
|
"sort",
|
|
],
|
|
"ir.model": [
|
|
"info",
|
|
"is_mail_thread",
|
|
"is_mail_activity",
|
|
"model",
|
|
"name",
|
|
"state",
|
|
"transient",
|
|
],
|
|
"ir.model.access": [
|
|
"active",
|
|
"group_id",
|
|
"model_id",
|
|
"name",
|
|
"perm_create",
|
|
"perm_read",
|
|
"perm_unlink",
|
|
"perm_write",
|
|
],
|
|
"ir.model.fields": [
|
|
"compute",
|
|
"copied",
|
|
"depends",
|
|
"domain",
|
|
"field_description",
|
|
"groups",
|
|
"group_expand",
|
|
"help",
|
|
"index",
|
|
"model",
|
|
"model_id",
|
|
"name",
|
|
"on_delete",
|
|
"readonly",
|
|
"related",
|
|
"relation",
|
|
"relation_field",
|
|
"relation_table",
|
|
"required",
|
|
"selectable",
|
|
"selection",
|
|
"size",
|
|
"state",
|
|
"store",
|
|
"tracking",
|
|
"translate",
|
|
"ttype",
|
|
"currency_field",
|
|
],
|
|
"ir.rule": [
|
|
"active",
|
|
"domain_force",
|
|
"groups",
|
|
"model_id",
|
|
"name",
|
|
"perm_create",
|
|
"perm_read",
|
|
"perm_unlink",
|
|
"perm_write",
|
|
],
|
|
"ir.ui.menu": [
|
|
"action",
|
|
"active",
|
|
"group_ids",
|
|
"name",
|
|
"parent_id",
|
|
"sequence",
|
|
"web_icon",
|
|
"web_icon_data",
|
|
],
|
|
"ir.ui.view": [
|
|
"active",
|
|
"arch",
|
|
"group_ids",
|
|
"inherit_id",
|
|
"key",
|
|
"mode",
|
|
"model",
|
|
"name",
|
|
"priority",
|
|
"type",
|
|
"website_id",
|
|
],
|
|
"mail.template": [
|
|
"auto_delete",
|
|
"body_html",
|
|
"email_cc",
|
|
"email_from",
|
|
"email_to",
|
|
"lang",
|
|
"model_id",
|
|
"name",
|
|
"partner_to",
|
|
"ref_ir_act_window",
|
|
"reply_to",
|
|
"report_template_ids",
|
|
"scheduled_date",
|
|
"subject",
|
|
"use_default_to",
|
|
],
|
|
"res.groups": ["sequence", "privilege_id", "comment", "implied_ids", "name", "share"],
|
|
"res.groups.privilege": ["sequence", "category_id", "name", "placeholder"],
|
|
"ir.module.category": ["sequence", "name"],
|
|
"ir.default": ["field_id", "condition", "json_value"],
|
|
"studio.approval.rule": [
|
|
"approver_ids",
|
|
"approval_group_id",
|
|
"model_id",
|
|
"method",
|
|
"action_id",
|
|
"name",
|
|
"message",
|
|
"exclusive_user",
|
|
"domain",
|
|
"can_validate",
|
|
],
|
|
# Only use for export attachment config in studio export models
|
|
"ir.attachment": [
|
|
"name",
|
|
"type",
|
|
"datas",
|
|
"url",
|
|
"res_id",
|
|
"res_model",
|
|
"res_field",
|
|
"access_token",
|
|
"key",
|
|
"website_id",
|
|
],
|
|
"mail.message": [
|
|
"subject",
|
|
"res_id",
|
|
"model",
|
|
"body",
|
|
"message_type",
|
|
"subtype_id",
|
|
"author_id",
|
|
"attachment_ids",
|
|
],
|
|
}
|
|
|
|
# List of models to mark as noupdate.
|
|
MODELS_WITH_NOUPDATE = [
|
|
"ir.attachment",
|
|
"ir.rule",
|
|
]
|
|
|
|
# List of relational fields NOT to export, by model.
|
|
RELATIONS_NOT_TO_EXPORT = {
|
|
"base.automation": ["trg_date_calendar_id"],
|
|
"ir.actions.server": ["partner_ids"],
|
|
"ir.filters": ["user_ids"],
|
|
"mail.template": ["attachment_ids", "mail_server_id"],
|
|
"report.paperformat": ["report_ids"],
|
|
"res.groups": ["user_ids"],
|
|
}
|
|
|
|
|
|
def _find_circular_dependencies(elems):
|
|
"""Return a list of circular dependencies encountered in `elems`.
|
|
|
|
:param elems: specifies the elements to sort with their dependencies; it is
|
|
a dictionary like `{element: dependencies}`.
|
|
|
|
:returns: a list of lists of elements forming circular dependencies.
|
|
"""
|
|
circular_dependencies = []
|
|
|
|
def traverse(elem, visited, stack):
|
|
visited.add(elem)
|
|
stack.append(elem)
|
|
|
|
if elem in elems:
|
|
for dependency in elems[elem]:
|
|
if dependency == elem:
|
|
continue
|
|
if dependency not in visited and not traverse(dependency, visited, stack):
|
|
return False
|
|
if dependency == stack[0]:
|
|
stack.append(dependency)
|
|
circular_dependencies.append(stack.copy())
|
|
return False
|
|
|
|
stack.remove(elem)
|
|
return True
|
|
|
|
to_skip = set()
|
|
for element in elems:
|
|
if element not in to_skip and not traverse(element, set(), []):
|
|
to_skip.update(circular_dependencies[-1])
|
|
|
|
return circular_dependencies
|
|
|
|
|
|
def _get_attachment_fields(env, model):
|
|
def is_attachment_field(info):
|
|
return "relation" in info and info["relation"] == "ir.attachment"
|
|
|
|
Model = env[model]
|
|
return [f for f in Model.fields_get().items() if is_attachment_field(f[1])]
|
|
|
|
|
|
class StudioExportWizardData(models.TransientModel):
|
|
"""The wizard data resembles the ir.model.data model.
|
|
It is used to store the export data for the wizard,
|
|
even for data that do not have an xmlid (an ir.model.data record).
|
|
"""
|
|
_name = 'studio.export.wizard.data'
|
|
_description = "Studio Export Data"
|
|
_order = "model_name, res_id"
|
|
|
|
model = fields.Char(required=True)
|
|
model_name = fields.Char(
|
|
string="Model Description", compute="_compute_model_name", store=True
|
|
)
|
|
res_id = fields.Many2oneReference(model_field="model", required=True)
|
|
name = fields.Char("Record Name", readonly=True)
|
|
xmlid = fields.Char("External ID", readonly=True)
|
|
pre = fields.Boolean(default=False, readonly=True)
|
|
post = fields.Boolean(default=False, readonly=True)
|
|
studio = fields.Boolean(default=False, readonly=True)
|
|
is_demo_data = fields.Boolean(
|
|
default=False,
|
|
string="As Demo",
|
|
readonly=True,
|
|
)
|
|
|
|
@api.depends("model")
|
|
def _compute_model_name(self):
|
|
models = self.mapped("model")
|
|
ir_models = self.env["ir.model"].search([("model", "in", models)])
|
|
for rec in self:
|
|
rec.model_name = ir_models.filtered(lambda r: r.model == rec.model).name
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""Compute the needed names and xmlids for the records."""
|
|
models = defaultdict(list)
|
|
for vals in vals_list:
|
|
models[vals["model"]].append(vals["res_id"])
|
|
|
|
if "ir.actions.actions" in models:
|
|
# We should replace "ir.actions.actions" records by their proper
|
|
# models, as we never want to export an abstract ir.actions.actions
|
|
# but instead the concrete actions models.
|
|
actions_by_type = self.env["ir.actions.actions"].sudo().browse(models["ir.actions.actions"])._get_actions_by_type()
|
|
types_by_action_id = defaultdict(str)
|
|
for action_type, actions in actions_by_type.items():
|
|
models[action_type].extend(actions.ids)
|
|
types_by_action_id.update(dict.fromkeys(actions.ids, action_type))
|
|
models.pop("ir.actions.actions")
|
|
|
|
names = defaultdict(dict)
|
|
xmlids = defaultdict(dict)
|
|
deleted = defaultdict(list)
|
|
for model, res_ids in models.items():
|
|
records = self.env[model].sudo().browse(res_ids)
|
|
existing = records.exists()
|
|
deleted[model] = [r.id for r in records - existing]
|
|
names[model] = {r.id: r.display_name for r in existing}
|
|
xmlids[model] = records._get_external_ids()
|
|
|
|
vals_list = [vals for vals in vals_list if not vals["res_id"] in deleted[vals["model"]]]
|
|
for vals in vals_list:
|
|
if vals["model"] == "ir.actions.actions":
|
|
vals["model"] = types_by_action_id[vals["res_id"]]
|
|
model = vals["model"]
|
|
res_id = vals["res_id"]
|
|
vals["name"] = names[model][res_id]
|
|
vals["xmlid"] = (
|
|
xmlids[model][res_id]
|
|
or ["__export__.%s_%s" % (model.replace(".", "_"), res_id)]
|
|
)[0]
|
|
|
|
return super().create(vals_list)
|
|
|
|
def _xmlid_for_export(self):
|
|
self.ensure_one()
|
|
return self.xmlid.replace('__export__.', '').replace('studio_customization.', '')
|
|
|
|
|
|
class StudioExportWizard(models.TransientModel):
|
|
_name = 'studio.export.wizard'
|
|
_description = "Studio Export Wizard"
|
|
|
|
def _default_studio_export_data(self):
|
|
data = self.env["ir.model.data"].search([
|
|
("studio", "=", True),
|
|
("model", "in", DEFAULT_MODELS_TO_EXPORT),
|
|
])
|
|
|
|
# filter active data
|
|
active_ids = {}
|
|
for model_name, model_data_list in groupby(data, lambda d: d.model):
|
|
records = self.env[model_name].browse(d.res_id for d in model_data_list)
|
|
if records._active_name:
|
|
records = records.filtered(records._active_name)
|
|
active_ids[model_name] = set(records.ids)
|
|
|
|
active_data = [d for d in data if d.res_id in active_ids[d.model]]
|
|
|
|
return self.env["studio.export.wizard.data"].create(
|
|
[{"model": d.model, "res_id": d.res_id, "studio": d.studio} for d in active_data]
|
|
)
|
|
|
|
default_export_data = fields.Many2many(
|
|
"studio.export.wizard.data",
|
|
default=_default_studio_export_data,
|
|
relation="rel_studio_export_wizard_data",
|
|
)
|
|
|
|
include_additional_data = fields.Boolean(default=False, string="Include Data")
|
|
include_demo_data = fields.Boolean(default=False, string="Include Demo Data")
|
|
|
|
additional_models = fields.Many2many(
|
|
"studio.export.model",
|
|
compute="_compute_additional_models",
|
|
string="Additional models to export",
|
|
help="Additional models you may choose to export in addition to the Studio customizations",
|
|
)
|
|
additional_export_data = fields.Many2many(
|
|
"studio.export.wizard.data",
|
|
compute="_compute_export_data",
|
|
relation="rel_studio_export_wizard_additional_data",
|
|
)
|
|
|
|
@api.constrains("default_export_data")
|
|
def _check_export_data(self):
|
|
for rec in self:
|
|
data = rec.default_export_data | rec.additional_export_data
|
|
for model, records in data.grouped("model").items():
|
|
if len(records) != len(set(records.mapped("res_id"))):
|
|
raise ValidationError(_("Model '%s' should not contain records with the same ID.", model))
|
|
|
|
@api.depends("include_additional_data", "include_demo_data")
|
|
def _compute_additional_models(self):
|
|
to_update = self.filtered("include_additional_data")
|
|
(self - to_update).additional_models = False
|
|
for record in to_update:
|
|
domain = [("is_demo_data", "=", False)] if not record.include_demo_data else []
|
|
record.additional_models = self.env["studio.export.model"].search(domain)
|
|
|
|
@api.depends("additional_models", "default_export_data")
|
|
def _compute_export_data(self):
|
|
"""
|
|
Compute the list of records that are exported in addition to the ones
|
|
defined in the default export data. (a.k.a. "not from the studio customizations ones")
|
|
"""
|
|
to_update = self.filtered("additional_models")
|
|
(self - to_update).additional_export_data = False
|
|
for rec in to_update:
|
|
export_data_vals = []
|
|
|
|
def add(export_model, model, records, pre=False, post=False):
|
|
nonlocal export_data_vals
|
|
model_default_data_ids = rec.default_export_data.filtered(
|
|
lambda r: r.model == model
|
|
).mapped("res_id")
|
|
|
|
# FIXME: This prevents the same record to be exported multiple times
|
|
# but what do we do if the first record is demo and the second not?
|
|
model_default_data_ids += [
|
|
export_data["res_id"]
|
|
for export_data in export_data_vals
|
|
if export_data["model"] == model
|
|
]
|
|
|
|
export_data_vals += [
|
|
{
|
|
"model": model,
|
|
"res_id": res_id,
|
|
"is_demo_data": export_model.is_demo_data,
|
|
"pre": pre,
|
|
"post": post,
|
|
}
|
|
for res_id in records.ids
|
|
if res_id not in model_default_data_ids
|
|
]
|
|
|
|
for export_model in rec.additional_models.sorted("sequence"):
|
|
to_export = export_model._get_exportable_records()
|
|
if to_export is None:
|
|
continue
|
|
add(export_model, export_model.model_name, to_export)
|
|
|
|
if export_model.model_name == "website":
|
|
attachments = self.env["ir.attachment"].search(
|
|
[
|
|
("type", "=", "binary"),
|
|
("website_id", "in", to_export.ids),
|
|
"!",
|
|
("name", "=like", "%.js.map"),
|
|
"!",
|
|
("name", "=like", "%.css.map"),
|
|
(
|
|
"mimetype",
|
|
"not in",
|
|
["application/javascript", "text/css", "text/plain"],
|
|
),
|
|
]
|
|
)
|
|
add(export_model, "ir.attachment", attachments, post=True)
|
|
|
|
website_views = self.env["ir.ui.view"].search(
|
|
[
|
|
("website_id", "in", to_export.ids),
|
|
("arch_updated", "=", True),
|
|
]
|
|
)
|
|
add(export_model, "ir.ui.view", website_views)
|
|
|
|
if export_model.model_name == "worksheet.template":
|
|
model_data = to_export.model_id
|
|
add(export_model, "ir.model", model_data)
|
|
|
|
model_rule_data = to_export.model_id.mapped("rule_ids")
|
|
add(export_model, "ir.rule", model_rule_data)
|
|
|
|
model_access_data = to_export.model_id.mapped("access_ids")
|
|
add(export_model, "ir.model.access", model_access_data)
|
|
|
|
model_group_data = to_export.model_id.mapped("rule_ids").mapped(
|
|
"groups"
|
|
)
|
|
add(export_model, "res.groups", model_group_data)
|
|
|
|
view_data = self.env["ir.ui.view"].search(
|
|
[("model", "in", to_export.model_id.mapped("model"))]
|
|
)
|
|
add(export_model, "ir.ui.view", view_data)
|
|
|
|
action_data = to_export.action_id
|
|
add(export_model, "ir.actions.act_window", action_data)
|
|
|
|
field_data = to_export.model_id.field_id.filtered_domain(
|
|
[("state", "!=", "base")]
|
|
)
|
|
add(export_model, "ir.model.fields", field_data)
|
|
|
|
if export_model.model_name == "project.task.type":
|
|
mail_templates = to_export.mail_template_id
|
|
add(export_model, "mail.template", mail_templates)
|
|
|
|
# add attachments
|
|
if export_model.include_attachment:
|
|
fields = _get_attachment_fields(self.env, export_model.model_name)
|
|
pre_attachments = self.env["ir.attachment"].search(
|
|
[
|
|
("res_id", "in", to_export.ids),
|
|
("res_model", "=", export_model.model_name),
|
|
"|",
|
|
(
|
|
"res_field",
|
|
"in",
|
|
[f[0] for f in fields if f[1]["type"] != "one2many"],
|
|
),
|
|
("res_field", "=", False),
|
|
]
|
|
)
|
|
add(export_model, "ir.attachment", pre_attachments, pre=True)
|
|
other_attachments = self.env["ir.attachment"].search(
|
|
[
|
|
("res_id", "in", to_export.ids),
|
|
("res_model", "=", export_model.model_name),
|
|
(
|
|
"res_field",
|
|
"in",
|
|
[f[0] for f in fields if f[1]["type"] == "one2many"],
|
|
),
|
|
]
|
|
)
|
|
add(export_model, "ir.attachment", other_attachments, post=True)
|
|
|
|
rec.additional_export_data = self.env["studio.export.wizard.data"].create(
|
|
export_data_vals
|
|
)
|
|
|
|
def get_export_info(self):
|
|
"""
|
|
Gather all the data to export from the wizard and return it in the correct order.
|
|
|
|
:return: A tuple containing the data to export, the list of xml files to generate and
|
|
the list of circular dependencies found.
|
|
:rtype: tuple
|
|
"""
|
|
self.ensure_one()
|
|
all_data = self.default_export_data | self.additional_export_data
|
|
no_update_models = MODELS_WITH_NOUPDATE + self.additional_models.filtered(lambda r: not r.updatable).mapped("model_name")
|
|
pre_export = []
|
|
export = []
|
|
post_export = []
|
|
|
|
models_to_export, circular_dependencies = self._get_models_to_export()
|
|
for model, _is_demo, fields_by_group, data, real_records in models_to_export:
|
|
|
|
def add(info, records_data, suffix="", force_exclude=[]):
|
|
if not records_data:
|
|
return
|
|
|
|
res_ids = records_data.mapped("res_id")
|
|
records = real_records.filtered(lambda r: r.id in res_ids)
|
|
|
|
for group, group_fields in fields_by_group.items():
|
|
for field_to_exclude in force_exclude:
|
|
if field_to_exclude in group_fields:
|
|
group_fields.remove(field_to_exclude)
|
|
|
|
if not group_fields:
|
|
# no fields to export
|
|
continue
|
|
|
|
path_info = (group, suffix)
|
|
no_update = model in no_update_models
|
|
info.append((model, records, group_fields, no_update, path_info))
|
|
|
|
pre_records = data.filtered("pre")
|
|
post_records = data.filtered("post")
|
|
data_records = data - pre_records - post_records
|
|
|
|
force_exclude = (
|
|
["res_id", "res_model", "res_field", "website_id"]
|
|
if model == "ir.attachment"
|
|
else []
|
|
)
|
|
add(pre_export, pre_records, suffix="_pre", force_exclude=force_exclude)
|
|
add(export, data_records)
|
|
add(post_export, post_records, suffix="_post")
|
|
|
|
return all_data, pre_export + export + post_export, circular_dependencies
|
|
|
|
def _get_fields_to_export_by_models(self):
|
|
"""Return a dict of {model_name: [fields_to_export]}"""
|
|
self.ensure_one()
|
|
all_data = self.default_export_data | self.additional_export_data
|
|
model_names = set(all_data.mapped("model"))
|
|
result = {}
|
|
for model_name in model_names:
|
|
Model = self.env[model_name]
|
|
fields_to_export = [
|
|
field
|
|
for field in FIELDS_TO_EXPORT.get(model_name, [])
|
|
if field in Model._fields
|
|
]
|
|
|
|
if not fields_to_export:
|
|
# deduce the fields_to_export from available data
|
|
fields_to_export = list(
|
|
OrderedSet(Model._fields.keys()) - OrderedSet(models.MAGIC_COLUMNS)
|
|
)
|
|
|
|
if RELATIONS_NOT_TO_EXPORT.get(model_name):
|
|
fields_to_export = list(
|
|
OrderedSet(fields_to_export)
|
|
- OrderedSet(RELATIONS_NOT_TO_EXPORT.get(model_name))
|
|
)
|
|
|
|
# Remove the excluded fields from fields_to_export
|
|
export_model = self.additional_models.filtered(
|
|
lambda m: m.model_name == model_name
|
|
)
|
|
export_model_fields_to_exclude = export_model.excluded_fields.mapped("name")
|
|
fields_to_export = [
|
|
f for f in fields_to_export if f not in export_model_fields_to_exclude
|
|
]
|
|
|
|
# Sort
|
|
fields_details = Model.fields_get(fields_to_export)
|
|
field_deps = OrderedDict.fromkeys(fields_to_export)
|
|
for field in fields_to_export:
|
|
field_deps[field] = (
|
|
[f.split(".")[0] for f in fields_details[field]["depends"]]
|
|
if field in fields_details
|
|
else list()
|
|
)
|
|
|
|
result[model_name] = topological_sort(field_deps)
|
|
|
|
return result
|
|
|
|
def _get_models_to_export(self):
|
|
"""
|
|
Returns a sorted list of tuple (model_name, is_demo, groups_to_export)
|
|
The groups_to_export is a list of tuple ("data"|"demo", fields_to_export)
|
|
"""
|
|
self.ensure_one()
|
|
fields_by_models = self._get_fields_to_export_by_models()
|
|
result = []
|
|
circular_dependencies = []
|
|
all_data = self.default_export_data | self.additional_export_data
|
|
|
|
# (*) Important that we do demo data before master data
|
|
for is_demo in [True, False]:
|
|
data = all_data.filtered(lambda r: r.is_demo_data == is_demo)
|
|
current_models = set(data.mapped("model"))
|
|
model_names = [m for m in DEFAULT_MODELS_TO_EXPORT if m in current_models]
|
|
model_names += sorted([m for m in current_models if m not in model_names])
|
|
|
|
# Note that here we can use the result in construction
|
|
# as we have already done the demo data, see (*) above.
|
|
demo_records = {m: records for (m, demo, _, _, records) in result if demo}
|
|
|
|
model_grouped_fields = {}
|
|
model_data = {}
|
|
model_records = {}
|
|
for model in model_names:
|
|
model_grouped_fields[model] = self._get_groups_fields(model, is_demo, fields_by_models[model], demo_records)
|
|
model_data[model] = data.filtered(lambda r: r.model == model)
|
|
sudo_model = self.env[model].sudo()
|
|
model_records[model] = sudo_model.browse(model_data[model].mapped("res_id"))
|
|
|
|
models_deps = OrderedDict().fromkeys(model_names)
|
|
for model in model_names:
|
|
models_deps[model] = set()
|
|
for field_name, info in sorted(self.env[model].fields_get().items(), key=lambda item: item[0]):
|
|
if (
|
|
field_name not in model_grouped_fields[model]["demo" if is_demo else "data"]
|
|
or "relation" not in info
|
|
or info["type"] == "one2many"
|
|
):
|
|
continue
|
|
|
|
related_model = info.get("relation")
|
|
if model_records[model].filtered(lambda r: (
|
|
related_model in model_records
|
|
and set(r[field_name].ids).intersection(set(model_records[related_model].ids))
|
|
)):
|
|
models_deps[model].add(related_model)
|
|
|
|
for circular_dependency in _find_circular_dependencies(models_deps):
|
|
circular_dependencies.append((is_demo, circular_dependency))
|
|
for model in topological_sort(models_deps):
|
|
result.append((model, is_demo, model_grouped_fields[model], model_data[model], model_records[model]))
|
|
|
|
return result, circular_dependencies
|
|
|
|
def _get_groups_fields(self, model, is_demo, fields_to_export, demo_records):
|
|
self.ensure_one()
|
|
|
|
if is_demo:
|
|
return {"demo": fields_to_export}
|
|
|
|
all_data = self.default_export_data | self.additional_export_data
|
|
|
|
# For master data (non demo) we do not want to export fields
|
|
# related to demo records, if any.
|
|
# Instead we want to export the master data records ALSO as demo
|
|
# but only with the fields related to demo records.
|
|
fields_relations = {
|
|
field_name: field_info["relation"]
|
|
for (field_name, field_info) in self.env[model]
|
|
.fields_get(fields_to_export)
|
|
.items()
|
|
if field_info.get("relation") and field_info["relation"] != model
|
|
}
|
|
demo_fields_to_export = []
|
|
new_fields_to_export = []
|
|
for field_name in fields_to_export:
|
|
related_model = fields_relations.get(field_name)
|
|
if related_model in demo_records:
|
|
data = all_data.filtered(lambda r: r.model == model and not r.is_demo_data)
|
|
records = self.env[model].sudo().browse(data.mapped("res_id"))
|
|
if any(r[field_name] in demo_records[related_model] for r in records):
|
|
demo_fields_to_export.append(field_name)
|
|
continue
|
|
new_fields_to_export.append(field_name)
|
|
|
|
return {"data": new_fields_to_export, "demo": demo_fields_to_export}
|
|
|
|
@api.onchange("include_additional_data")
|
|
def _onchange_include_additional_data(self):
|
|
if not self.include_additional_data:
|
|
self.include_demo_data = False
|
|
|
|
@api.onchange("include_demo_data")
|
|
def _onchange_include_demo_data(self):
|
|
if self.include_demo_data:
|
|
self.include_additional_data = True
|