Files
enterprise-addons/web_studio/models/studio_export_model.py
T

450 lines
16 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import _, api, fields, models
from odoo.fields import Command, Domain
from odoo.addons.web_studio.wizard.studio_export_wizard import FIELDS_TO_EXPORT
# List of preset models to export when the preset action is triggered.
# This list may include specific defaults for each model.
PRESET_MODELS_DEFAULTS = [
("appointment.resource", {}),
("res.partner", {"domain": "[('user_ids', '=', False)]", "is_demo_data": True}),
("resource.resource", {"is_demo_data": True}),
("hr.employee", {"is_demo_data": True}),
("hr.applicant", {"is_demo_data": True}),
("hr.department", {"is_demo_data": True}),
("hr.job", {"is_demo_data": True}),
("hr.recruitment.stage", {}),
("product.public.category", {}),
("project.task.type", {}),
("documents.document", {"domain": "[('type', '=', 'folder')]"}),
("product.category", {}),
("worksheet.template", {}),
("account.analytic.plan", {"is_demo_data": True}),
("account.analytic.account", {"is_demo_data": True}),
("appointment.type", {"is_demo_data": True}),
("project.project", {}),
("uom.uom", {}),
("planning.role", {}),
("product.template", {}),
("calendar.event", {"is_demo_data": True}),
("crm.tag", {"is_demo_data": True}),
("crm.team", {"is_demo_data": True}),
("crm.stage", {}),
("crm.lead", {"is_demo_data": True}),
("event.event", {}),
("event.event.ticket", {}),
("event.tag", {}),
("event.tag.category", {}),
("helpdesk.ticket", {"is_demo_data": True}),
("product.supplierinfo", {"is_demo_data": True}),
("sale.order", {"domain": "[('state', 'not in', ['draft', 'cancel'])]", "is_demo_data": True}),
("sale.order.line", {"is_demo_data": True}),
("loyalty.program", {"is_demo_data": True}),
("loyalty.reward", {"is_demo_data": True}),
("loyalty.rule", {"is_demo_data": True}),
("mail.activity", {"is_demo_data": True}),
("mail.activity.type", {"is_demo_data": True}),
("mail.template", {}),
("maintenance.equipment", {}),
("mrp.bom", {"is_demo_data": True}),
("mrp.bom.line", {"is_demo_data": True}),
("mrp.production", {"is_demo_data": True}),
("mrp.routing.workcenter", {"is_demo_data": True}),
("mrp.workcenter", {"is_demo_data": True}),
("mrp.workorder", {"is_demo_data": True}),
("project.task", {"is_demo_data": True}),
("project.tags", {}),
("project.project.stage", {}),
("product.attribute", {}),
("product.attribute.value", {}),
("product.pricelist", {}),
("product.pricelist.item", {}),
("product.template.attribute.line", {}),
("product.template.attribute.value", {}),
("product.product", {}),
("product.image", {}),
("pos.category", {}),
("pos.config", {}),
("pos.order", {'domain': "[('state', '!=', 'cancel')]", "is_demo_data": True}),
("pos.order.line", {"is_demo_data": True}),
("pos.payment.method", {}),
("pos.session", {"is_demo_data": True}),
("pos.prep.display", {"is_demo_data": True}),
("sale.order.template", {}),
("sale.order.template.line", {}),
("knowledge.cover", {"include_attachment": True}),
("knowledge.article", {"domain": "[('category', 'in', ['workspace', 'shared'])]"}),
("website", {"is_demo_data": True}),
("website.page", {"is_demo_data": True}),
("website.menu", {"is_demo_data": True}),
("stock.lot", {"is_demo_data": True}),
("purchase.order", {"is_demo_data": True}),
("purchase.order.line", {"is_demo_data": True}),
("quality.point", {}),
("quality.check", {"is_demo_data": True}),
("planning.slot.template", {"is_demo_data": True}),
("planning.recurrency", {"is_demo_data": True}),
("planning.slot", {"is_demo_data": True}),
("restaurant.floor", {}),
("restaurant.table", {}),
("repair.order", {"is_demo_data": True}),
("sign.item", {}),
("sign.request", {}),
("sign.template", {}),
("stock.quant", {"is_demo_data": True}),
("stock.warehouse.orderpoint", {"is_demo_data": True}),
("survey.survey", {"is_demo_data": True}),
("survey.question", {"is_demo_data": True}),
("survey.question.answer", {"is_demo_data": True}),
]
DEFAULTS_BY_PRESET_MODELS = {
m[0]: {**m[1], "sequence": index}
for index, m in enumerate(PRESET_MODELS_DEFAULTS)
}
# _compute_excluded_fields: default fields to exclude
DEFAULT_FIELDS_TO_EXCLUDE = {
"res.partner": {
"ocn_token",
"signup_type",
"commercial_partner_id",
"complete_name",
"calendar_last_notif_ack",
"category_id",
"commercial_company_name",
"customer_rank",
"email_normalized",
"phone_sanitized",
"peppol_eas",
"peppol_endpoint",
"contact_address_complete",
"tz",
"company_type",
},
"hr.employee": {"employee_token", "resource_calendar_id"},
"account.analytic.plan": {"complete_name"},
"product.category": {"complete_name"},
"product.product": {
"combination_indices",
# the other image fields are excluded through the image.mixin fields to exclude
# and we only need the image_variant_1920 for the export
"image_1920",
"image_variant_128",
"image_variant_256",
"image_variant_512",
"image_variant_1024",
"base_unit_count",
},
"knowledge.cover": {"attachment_url"},
"knowledge.article": {
"root_article_id",
"last_edition_date",
"favorite_count",
},
"loyalty.rule": {
"promo_barcode",
"mode",
},
"loyalty.reward": {"description"},
"stock.quant": {
"inventory_date",
"in_date",
},
"stock.warehouse.orderpoint": {
"trigger",
"name",
"warehouse_id",
"location_id",
},
"project.project": {"sale_line_id"},
"project.task": {"personal_stage_type_ids", "date_last_stage_update"},
"project.task.type": {"project_ids"},
"product.attribute.value": {"pav_attribute_line_ids"},
"product.public.category": {"product_tmpl_ids"},
"product.template.attribute.value": {
"ptav_product_variant_ids",
"product_tmpl_id",
"attribute_id",
},
"product.template.attribute.line": {"value_count"},
"planning.slot": {
"access_token",
"allocated_hours",
"sale_order_id",
},
"sale.order": {
"name",
"team_id",
"transaction_ids",
"require_signature",
"require_payment",
"validity_date",
"note",
"partner_shipping_id",
"partner_invoice_id",
"payment_term_id",
"state",
"subscription_state",
"currency_rate",
"amount_tax",
"amount_untaxed",
"amount_total",
"amount_to_invoice",
"invoice_status",
},
"sale.order.line": {
"invoice_lines",
"task_id",
"price_subtotal",
"price_tax",
"price_total",
"price_reduce_taxexcl",
"price_reduce_taxinc",
"qty_delivered_method",
"qty_delivered",
"qty_to_invoice",
"qty_invoiced",
"invoice_status",
"untaxed_amount_invoiced",
"untaxed_amount_to_invoice",
},
"purchase.order": {
"name",
"origin",
"invoice_ids",
"reference_ids",
"invoice_count",
"invoice_status",
"amount_tax",
"amount_total",
"currency_rate",
},
"purchase.order.line": {
"currency_id",
"move_dest_ids",
"price_subtotal",
"price_total",
"price_tax",
"qty_invoiced",
"qty_received_method",
"qty_received",
"qty_to_invoice",
},
"crm.lead": {
"recurring_plan",
"lost_reason_id",
"duplicate_lead_ids",
"lang_id",
"prorated_revenue",
"automated_probability",
"date_last_stage_update",
},
"crm.team": {"member_ids"},
"survey.survey": {"session_question_id"},
"survey.question": {"page_id"},
}
# _compute_excluded_fields: abstract model fields to exclude
ABSTRACT_MODEL_FIELDS_TO_EXCLUDE = {
"html.field.history.mixin": {"html_field_history_metadata", "html_field_history"},
"mail.activity.mixin": {"activity_ids"},
"mail.thread": {"message_follower_ids", "message_ids", "message_partner_ids"},
"mail.thread.blacklist": {"email_normalized", "is_blacklisted", "message_bounce"},
"mail.alias.mixin": {"alias_id"},
"portal.mixin": {"access_url", "access_token", "access_warning"},
"website.published.mixin": {"website_published"},
"avatar.mixin": {
"avatar_1920",
"avatar_1024",
"avatar_512",
"avatar_256",
"avatar_128",
},
# only export image_1920, the other sizes can be generated from it
"image.mixin": {"image_1024", "image_512", "image_256", "image_128"},
}
# _compute_excluded_fields: relations to exclude
RELATED_MODELS_TO_EXCLUDE = [
"account.account.tag",
"account.account",
"account.bank.statement",
"account.edi.document",
"account.fiscal.position",
"account.full.reconcile",
"account.journal",
"account.partial.reconcile",
"account.payment",
"account.root",
"account.tax.repartition.line",
"account.tax",
]
class StudioExportModel(models.Model):
_name = 'studio.export.model'
_description = "Studio Export Models"
_order = "sequence,id"
_unique_model = models.Constraint(
'unique(model_id)',
"This model is already being exported.",
)
sequence = fields.Integer()
model_id = fields.Many2one(
"ir.model",
required=True,
ondelete="cascade",
domain="[('transient', '!=', True), ('abstract', '!=', True)]",
)
model_name = fields.Char(string="Model Name", related="model_id.model", store=True)
excluded_fields = fields.Many2many(
"ir.model.fields",
string="Fields to exclude",
domain="[('model_id', '=', model_id)]",
compute="_compute_excluded_fields",
readonly=False,
store=True,
)
domain = fields.Text(default="[]")
records_count = fields.Char(string="Records", compute="_compute_records_count")
is_demo_data = fields.Boolean(
default=False,
string="Demo",
help="If set, the exported records will be considered as demo data during the import.",
)
updatable = fields.Boolean(
default=False,
help="Defines if the records would be updated during a module update.",
)
include_attachment = fields.Boolean(
string="Attachments",
default=False,
help="""If set, attachments related to the exported records will be included.
This does not include the attachments uploaded in binary fields such as pictures, files, etc.
Include the corresponding fields in the export to get them.""",
)
@api.depends("model_id")
def _compute_display_name(self):
for record in self:
record.display_name = record.model_id.display_name
@api.depends("model_id")
def _compute_excluded_fields(self):
to_reset = self.filtered(lambda r: not r.model_id)
to_reset.excluded_fields = None
for record in self - to_reset:
RecordModel = self.env[record.model_name]
fields_not_to_export = set(DEFAULT_FIELDS_TO_EXCLUDE.get(
record.model_name, ()
))
# also exclude fields of abstract models
to_search = {m for m in RecordModel._base_classes__ if m._abstract}
searched = set()
while to_search:
current = to_search.pop()
if current._name in ABSTRACT_MODEL_FIELDS_TO_EXCLUDE:
fields_not_to_export |= ABSTRACT_MODEL_FIELDS_TO_EXCLUDE[current._name]
searched.add(current)
to_search |= (
{
m
for m in current._base_classes__
if m not in searched and m._abstract
}
if "_base_classes__" in dir(current)
else set()
)
for field_name, field in RecordModel._fields.items():
if field_name in fields_not_to_export:
continue
module = field._modules[0] if field._modules else None
if (
# - exclude computed fields (not related) that can't impact the import because they are not writeable
((field.compute and not field.related) and not (field.store or field.inverse))
# - exclude related fields:
# - if they are related to an excluded model
# - or if they are not stored+readonly (= the field is writeable but on the
# current record only, not the source record), except if required
or (field.related and (field.related_field.model_name in RELATED_MODELS_TO_EXCLUDE or not ((field.store and field.readonly) or field.required)))
# - exclude one2many fields
or (field.type == "one2many")
# - exclude fields created in l10n_* modules
or (module and module.startswith("l10n_"))
# - exclude many2x if comodel is not to export
or (
field.type in ["many2one", "many2many"]
and field.comodel_name in RELATED_MODELS_TO_EXCLUDE
)
):
fields_not_to_export.add(field_name)
if RecordModel._parent_store:
fields_not_to_export.add("parent_path")
fields_not_to_export -= set(FIELDS_TO_EXPORT.get(record.model_name, []))
excluded_fields = self.env["ir.model.fields"].search(
[
("model_id", "=", record.model_id.id),
("name", "in", list(fields_not_to_export)),
]
)
record.excluded_fields = [Command.set(excluded_fields.ids)]
@api.depends("model_name", "domain")
def _compute_records_count(self):
for record in self:
records_count = (
self.env[record.model_name].sudo()
.search_count(literal_eval(record.domain or "[]"))
)
record.records_count = _("%s record(s)", records_count)
def action_preset(self):
curr_models_names = [
r["model_name"] for r in self.search_read([], ["model_name"])
]
preset_models = [
(model, DEFAULTS_BY_PRESET_MODELS.get(model["model"], {}))
# find all existing models from the preset list + custom ones
for model in self.env["ir.model"].search_read(
Domain("transient", "=", False)
& Domain("abstract", "=", False)
& (
Domain("model", "in", list(DEFAULTS_BY_PRESET_MODELS.keys()))
| Domain("model", "=like", r"x\_%")
| Domain("state", "=", "manual")
),
["model"],
)
]
to_create = [
{**defaults, "model_id": model["id"]}
for model, defaults in preset_models
# filter out models that are already configured or do not have some records to export
if model["model"] not in curr_models_names
and (
not self.env[model["model"]]._log_access
or (
self.env[model["model"]].sudo()
.search_count(literal_eval(defaults.get("domain", "[]")))
)
)
]
if to_create:
self.create(to_create)
def _get_exportable_records(self):
self.ensure_one()
domain = literal_eval(self.domain or "[]")
model = self.env[self.model_name].sudo()
return model.search(domain)