合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
5
web_studio/__init__.py
Normal file
5
web_studio/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
156
web_studio/__manifest__.py
Normal file
156
web_studio/__manifest__.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': "Studio",
|
||||
'summary': "Create and customize your Odoo apps",
|
||||
'website': 'https://www.odoo.com/app/studio',
|
||||
'description': """
|
||||
Studio - Customize Odoo
|
||||
=======================
|
||||
|
||||
This addon allows the user to customize most element of the user interface, in a
|
||||
simple and graphical way. It has two main features:
|
||||
|
||||
* create a new application (add module, top level menu item, and default action)
|
||||
* customize an existing application (edit menus, actions, views, translations, ...)
|
||||
|
||||
Note: Only the admin user is allowed to make those customizations.
|
||||
""",
|
||||
'category': 'Customizations/Studio',
|
||||
'sequence': 75,
|
||||
'version': '1.0',
|
||||
'depends': [
|
||||
'base_automation',
|
||||
'base_import_module',
|
||||
'mail',
|
||||
'web',
|
||||
'web_enterprise',
|
||||
'web_editor',
|
||||
'web_map',
|
||||
'web_gantt',
|
||||
'sms',
|
||||
],
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'views/actions.xml',
|
||||
'views/base_import_module_view.xml',
|
||||
'views/ir_actions_report_xml.xml',
|
||||
'views/ir_model_data.xml',
|
||||
'views/studio_approval_views.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'wizard/base_module_uninstall_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/studio_security.xml',
|
||||
],
|
||||
'application': True,
|
||||
'license': 'OEEL-1',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'web_studio/static/src/systray_item/**/*.js',
|
||||
'web_studio/static/src/studio_service.js',
|
||||
'web_studio/static/src/utils.js',
|
||||
'web_studio/static/src/tours/**/*.js',
|
||||
|
||||
'web_studio/static/src/legacy/js/approval_component.js',
|
||||
'web_studio/static/src/legacy/scss/approval_component.scss',
|
||||
'web_studio/static/src/legacy/js/bus.js',
|
||||
'web_studio/static/src/legacy/js/views/renderers/form_renderer.js',
|
||||
'web_studio/static/src/legacy/js/views/renderers/list_renderer_eager.js',
|
||||
'web_studio/static/src/legacy/js/views/controllers/form_controller.js',
|
||||
'web_studio/static/src/legacy/studio_legacy_service.js',
|
||||
'web_studio/static/src/home_menu/**/*.js',
|
||||
'web_studio/static/src/views/**/*.js',
|
||||
'web_studio/static/src/approval/**/*',
|
||||
'web_studio/static/src/**/*.xml',
|
||||
('remove', 'web_studio/static/src/legacy/xml/sidebar_web_editor.xml'),
|
||||
],
|
||||
'web_editor.assets_wysiwyg': {
|
||||
'web_studio/static/src/legacy/xml/sidebar_web_editor.xml',
|
||||
},
|
||||
'web.assets_backend_prod_only': [
|
||||
'web_studio/static/src/client_action/studio_action_loader.js',
|
||||
'web_studio/static/src/client_action/app_creator/app_creator_shortcut.js',
|
||||
],
|
||||
# This bundle is lazy loaded: it is loaded when studio is opened for the first time
|
||||
'web_studio.studio_assets': [
|
||||
'web_studio/static/src/client_action/**/*.js',
|
||||
('remove', 'web_studio/static/src/client_action/studio_action_loader.js'),
|
||||
('remove', 'web_studio/static/src/client_action/app_creator/app_creator_shortcut.js'),
|
||||
'web_studio/static/src/legacy/action_editor_main.js',
|
||||
'web_studio/static/src/legacy/edit_menu_adapter.js',
|
||||
'web_studio/static/src/legacy/new_model_adapter.js',
|
||||
|
||||
'web_studio/static/src/legacy/js/py.js',
|
||||
'web_studio/static/src/legacy/js/edit_menu.js',
|
||||
'web_studio/static/src/legacy/js/new_model.js',
|
||||
'web_studio/static/src/legacy/js/common_menu_dialog.js',
|
||||
'web_studio/static/src/legacy/js/common/**/*.js',
|
||||
'web_studio/static/src/legacy/js/reports/**/*.js',
|
||||
'web_studio/static/src/legacy/js/views/abstract_view.js',
|
||||
'web_studio/static/src/legacy/js/views/action_editor.js',
|
||||
'web_studio/static/src/legacy/js/views/action_editor_sidebar.js',
|
||||
'web_studio/static/src/legacy/js/views/action_editor_view.js',
|
||||
'web_studio/static/src/legacy/js/views/view_components.js',
|
||||
'web_studio/static/src/legacy/js/views/view_editor_manager.js',
|
||||
'web_studio/static/src/legacy/js/views/view_editor_sidebar.js',
|
||||
'web_studio/static/src/legacy/js/views/renderers/search_renderer.js',
|
||||
'web_studio/static/src/legacy/js/views/renderers/list_renderer_lazy.js',
|
||||
'web_studio/static/src/legacy/js/views/view_editors/**/*.js',
|
||||
|
||||
('include', 'web._assets_helpers'),
|
||||
'web_studio/static/src/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web_studio/static/src/client_action/variables.scss',
|
||||
'web_studio/static/src/client_action/mixins.scss',
|
||||
'web_studio/static/src/client_action/**/*.scss',
|
||||
|
||||
'web_studio/static/src/legacy/scss/icons.scss',
|
||||
'web_studio/static/src/legacy/scss/action_editor.scss',
|
||||
'web_studio/static/src/legacy/scss/kanban_view.scss',
|
||||
'web_studio/static/src/legacy/scss/kanban_editor.scss',
|
||||
'web_studio/static/src/legacy/scss/list_editor.scss',
|
||||
'web_studio/static/src/legacy/scss/new_field_dialog.scss',
|
||||
'web_studio/static/src/legacy/scss/report_editor.scss',
|
||||
'web_studio/static/src/legacy/scss/report_editor_manager.scss',
|
||||
'web_studio/static/src/legacy/scss/report_editor_sidebar.scss',
|
||||
'web_studio/static/src/legacy/scss/report_kanban_view.scss',
|
||||
'web_studio/static/src/legacy/scss/search_editor.scss',
|
||||
'web_studio/static/src/legacy/scss/sidebar.scss',
|
||||
'web_studio/static/src/legacy/scss/view_editor_manager.scss',
|
||||
'web_studio/static/src/legacy/scss/xml_editor.scss',
|
||||
|
||||
'web_studio/static/src/legacy/xml/new_model.xml',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'web_studio/static/tests/legacy/tours/**/*',
|
||||
],
|
||||
'web_studio.report_assets': [
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web_studio/static/src/legacy/scss/report_iframe.scss',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
# In tests we don't want to lazy load this
|
||||
# And we don't want to push them into any other test suite either
|
||||
# as web.tests_assets would
|
||||
('include', 'web_studio.studio_assets'),
|
||||
'web_studio/static/tests/mock_server.js',
|
||||
'web_studio/static/tests/helpers.js',
|
||||
'web_studio/static/tests/*.js',
|
||||
'web_studio/static/tests/views/**/*.js',
|
||||
'web_studio/static/tests/legacy/action_editor_action_tests.js',
|
||||
'web_studio/static/tests/legacy/edit_menu_tests.js',
|
||||
'web_studio/static/tests/legacy/new_model_tests.js',
|
||||
'web_studio/static/tests/legacy/mock_server.js',
|
||||
'web_studio/static/tests/legacy/test_utils.js',
|
||||
'web_studio/static/tests/legacy/reports/**/*.js',
|
||||
'web_studio/static/tests/legacy/views/**/*.js',
|
||||
],
|
||||
'web.qunit_mobile_suite_tests': [
|
||||
'web_studio/static/tests/views/disable_patch.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
6
web_studio/controllers/__init__.py
Normal file
6
web_studio/controllers/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import export
|
||||
from . import ir_http
|
||||
from . import main
|
||||
from . import report
|
||||
373
web_studio/controllers/export.py
Normal file
373
web_studio/controllers/export.py
Normal file
@@ -0,0 +1,373 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from collections import OrderedDict
|
||||
from contextlib import closing
|
||||
import io
|
||||
from lxml import etree
|
||||
from lxml.builder import E
|
||||
import os.path
|
||||
import zipfile
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv.expression import OR
|
||||
from odoo.tools import topological_sort
|
||||
|
||||
# list of models to export (the order ensures that dependencies are satisfied)
|
||||
MODELS_TO_EXPORT = [
|
||||
'res.groups',
|
||||
'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',
|
||||
]
|
||||
# list of fields to export by model
|
||||
FIELDS_TO_EXPORT = {
|
||||
'base.automation': [
|
||||
'action_server_id', 'active', 'filter_domain', 'filter_pre_domain',
|
||||
'last_run', 'on_change_field_ids', 'trg_date_id', 'trg_date_range',
|
||||
'trg_date_range_type', 'trigger'
|
||||
],
|
||||
'ir.actions.act_window': [
|
||||
'binding_model_id', 'binding_type', 'binding_view_types',
|
||||
'context', 'domain', 'filter',
|
||||
'groups_id', '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', 'groups_id', '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', 'help',
|
||||
'link_field_id', 'model_id', 'name', 'sequence', 'state'
|
||||
],
|
||||
'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': [
|
||||
'complete_name', 'compute', 'copied', 'depends', 'domain', 'field_description', 'groups',
|
||||
'help', 'index', 'model', 'model_id', 'name', 'on_delete', 'readonly', 'related',
|
||||
'relation', 'relation_field', 'relation_table', 'required', 'selectable', 'selection',
|
||||
'size', 'state', 'store', 'tracking', 'translate',
|
||||
'ttype'
|
||||
],
|
||||
'ir.rule': [
|
||||
'active', 'domain_force', 'groups', 'model_id', 'name', 'perm_create', 'perm_read',
|
||||
'perm_unlink', 'perm_write'
|
||||
],
|
||||
'ir.ui.menu': ['action', 'active', 'groups_id', 'name', 'parent_id', 'sequence', 'web_icon'],
|
||||
'ir.ui.view': [
|
||||
'active', 'arch', 'field_parent', 'groups_id', 'inherit_id', 'key', 'mode', 'model', 'name',
|
||||
'priority', 'type'
|
||||
],
|
||||
'mail.template': [
|
||||
'auto_delete', 'body_html', 'copyvalue', 'email_cc', 'email_from', 'email_to', 'lang',
|
||||
'model_id', 'model_object_field', 'name', 'null_value', 'partner_to', 'ref_ir_act_window',
|
||||
'reply_to', 'report_name', 'report_template', 'scheduled_date', 'sub_model_object_field',
|
||||
'sub_object', 'subject', 'use_default_to'
|
||||
],
|
||||
'res.groups': ['color', 'comment', 'implied_ids', 'name', 'share'],
|
||||
'ir.default': ['field_id', 'condition', 'json_value'],
|
||||
}
|
||||
# list of relational fields to NOT export, by model
|
||||
FIELDS_NOT_TO_EXPORT = {
|
||||
'base.automation': ['trg_date_calendar_id'],
|
||||
'ir.actions.server': ['fields_lines', 'partner_ids'],
|
||||
'ir.filter': ['user_id'],
|
||||
'mail.template': ['attachment_ids', 'mail_server_id'],
|
||||
'report.paperformat': ['report_ids'],
|
||||
'res.groups': ['category_id', 'users'],
|
||||
}
|
||||
# The fields whose value must be wrapped in <![CDATA[]]>
|
||||
CDATA_FIELDS = [
|
||||
('ir.actions.server', 'code'), ('ir.model.fields', 'compute'), ('ir.rule', 'domain_force'),
|
||||
('ir.actions.act_window', 'help'), ('ir.actions.server', 'help'), ('ir.model.fields', 'help')
|
||||
]
|
||||
# The fields whose value is some XML content
|
||||
XML_FIELDS = [('ir.ui.view', 'arch')]
|
||||
|
||||
|
||||
def generate_archive(module, data):
|
||||
""" Returns a zip file containing the given module with the given data. """
|
||||
with closing(io.BytesIO()) as f:
|
||||
with zipfile.ZipFile(f, 'w') as archive:
|
||||
for filename, content in generate_module(module, data):
|
||||
archive.writestr(os.path.join(module.name, filename), content)
|
||||
return f.getvalue()
|
||||
|
||||
|
||||
def generate_module(module, data):
|
||||
""" Return an iterator of pairs (filename, content) to put in the exported
|
||||
module. Returned filenames are local to the module directory.
|
||||
Only exports models in MODELS_TO_EXPORT.
|
||||
Groups exported data by model in separated files.
|
||||
The content of the files is yielded as an encoded bytestring (utf-8)
|
||||
"""
|
||||
get_xmlid = xmlid_getter()
|
||||
|
||||
# Generate xml files and yield them
|
||||
filenames = [] # filenames to include in the module to export
|
||||
# depends contains module dependencies of the module to export, as a result
|
||||
# we add web_studio by default to deter importing studio customizations
|
||||
# in community databases
|
||||
depends = set([u'web_studio'])
|
||||
skipped = [] # non-exported field values
|
||||
|
||||
for model in MODELS_TO_EXPORT:
|
||||
# determine records to export for model
|
||||
model_data = data.filtered(lambda r: r.model == model)
|
||||
records = data.env[model].browse(model_data.mapped('res_id')).exists()
|
||||
if not records:
|
||||
continue
|
||||
|
||||
# retrieve module and inter-record dependencies
|
||||
fields = [records._fields[name] for name in get_fields_to_export(records)]
|
||||
record_deps = OrderedDict.fromkeys(records, records.browse())
|
||||
for record in records:
|
||||
xmlid = get_xmlid(record)
|
||||
module_name = xmlid.split('.', 1)[0]
|
||||
if module_name != module.name:
|
||||
# data depends on a record from another module
|
||||
depends.add(module_name)
|
||||
for field in fields:
|
||||
rel_records = get_relations(record, field)
|
||||
if not rel_records:
|
||||
continue
|
||||
for rel_record in rel_records:
|
||||
rel_xmlid = get_xmlid(rel_record, check=False)
|
||||
if rel_xmlid and rel_xmlid.split('.')[0] != module.name:
|
||||
# data depends on a record from another module
|
||||
depends.add(rel_xmlid.split('.')[0])
|
||||
if rel_records._name == model:
|
||||
# fill in inter-record dependencies
|
||||
record_deps[record] |= rel_records
|
||||
if record._name == 'ir.model.fields' and record.ttype == 'monetary':
|
||||
# add a dependency on the currency field
|
||||
rel_record = record._get(record.model, 'currency_id') or record._get(record.model, 'x_currency_id')
|
||||
rel_xmlid = get_xmlid(rel_record, check=False)
|
||||
if rel_xmlid and rel_xmlid.split('.')[0] != module.name:
|
||||
# data depends on a record from another module
|
||||
depends.add(rel_xmlid.split('.')[0])
|
||||
record_deps[record] |= rel_record
|
||||
|
||||
# sort records to satisfy inter-record dependencies
|
||||
records = topological_sort(record_deps)
|
||||
|
||||
# create the XML containing the generated record nodes
|
||||
nodes = []
|
||||
for record in records:
|
||||
xmlid = get_xmlid(record)
|
||||
if xmlid.split('.', 1)[0] != '__export__':
|
||||
record_node, record_skipped = generate_record(record, get_xmlid)
|
||||
nodes.append(record_node)
|
||||
skipped.extend(record_skipped)
|
||||
root = E.odoo(*nodes)
|
||||
xml = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True)
|
||||
|
||||
# add the XML file to the archive
|
||||
filename = '/'.join(['data', '%s.xml' % model.replace('.', '_')])
|
||||
yield (filename, xml)
|
||||
filenames.append(filename)
|
||||
|
||||
# yield a warning file to notify that some data haven't been exported
|
||||
if skipped:
|
||||
content = [
|
||||
"The following relational data haven't been exported because they either refer",
|
||||
"to a model that Studio doesn't export, or have no XML id:",
|
||||
"",
|
||||
]
|
||||
for xmlid, field, value in skipped:
|
||||
content.append("Record: %s" % xmlid)
|
||||
content.append("Model: %s" % field.model_name)
|
||||
content.append("Field: %s" % field.name)
|
||||
content.append("Type: %s" % field.type)
|
||||
content.append("Value: %s (%s)" % (value, ', '.join("%r" % v.display_name for v in value)))
|
||||
content.append("")
|
||||
yield ('warning.txt', "\n".join(content))
|
||||
|
||||
# yield files '__manifest__.py' and '__init__.py'
|
||||
manifest = """# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': %r,
|
||||
'version': %r,
|
||||
'category': 'Studio',
|
||||
'description': %s,
|
||||
'author': %r,
|
||||
'depends': [%s
|
||||
],
|
||||
'data': [%s
|
||||
],
|
||||
'application': %s,
|
||||
'license': %r,
|
||||
}
|
||||
""" % (
|
||||
module.display_name,
|
||||
module.installed_version,
|
||||
'u"""\n%s\n"""' % module.description,
|
||||
module.author,
|
||||
''.join("\n %r," % d for d in sorted(depends - {'__export__'})),
|
||||
''.join("\n %r," % f for f in filenames),
|
||||
module.application,
|
||||
module.license,
|
||||
)
|
||||
manifest = manifest.encode('utf-8')
|
||||
|
||||
yield ('__manifest__.py', manifest)
|
||||
yield ('__init__.py', b'')
|
||||
|
||||
|
||||
def get_relations(record, field):
|
||||
""" Return either a recordset that ``record`` depends on for ``field``, or a
|
||||
falsy value.
|
||||
"""
|
||||
if not record[field.name]:
|
||||
return
|
||||
|
||||
if field.type in ('many2one', 'one2many', 'many2many', 'reference'):
|
||||
return record[field.name]
|
||||
|
||||
if field.model_name == 'ir.model.fields':
|
||||
# Some fields (depends, related, relation_field) are of type char, but
|
||||
# refer to other fields that must be defined beforehand
|
||||
if field.name in ('depends', 'related'):
|
||||
# determine the fields that record depends on
|
||||
dep_fields = set()
|
||||
for dep_names in record[field.name].split(','):
|
||||
dep_model = record.env[record.model]
|
||||
for dep_name in dep_names.strip().split('.'):
|
||||
dep_field = dep_model._fields[dep_name]
|
||||
if not dep_field.automatic:
|
||||
dep_fields.add(dep_field)
|
||||
if dep_field.relational:
|
||||
dep_model = record.env[dep_field.comodel_name]
|
||||
# determine the 'ir.model.fields' corresponding to 'dep_fields'
|
||||
if dep_fields:
|
||||
return record.search(OR([
|
||||
['&', ('model', '=', dep_field.model_name), ('name', '=', dep_field.name)]
|
||||
for dep_field in dep_fields
|
||||
]))
|
||||
elif field.name == 'relation_field':
|
||||
# The field 'relation_field' on 'ir.model.fields' is of type char,
|
||||
# but it refers to another field that must be defined beforehand
|
||||
return record.search([('model', '=', record.relation), ('name', '=', record.relation_field)])
|
||||
|
||||
# Fields 'res_model' and 'binding_model' on 'ir.actions.act_window' and 'model'
|
||||
# on 'ir.actions.report' are of type char but refer to models that may
|
||||
# be defined in other modules and those modules need to be listed as
|
||||
# dependencies of the exported module
|
||||
if field.model_name == 'ir.actions.act_window' and field.name in ('res_model', 'binding_model'):
|
||||
return record.env['ir.model']._get(record[field.name])
|
||||
if field.model_name == 'ir.actions.report' and field.name == 'model':
|
||||
return record.env['ir.model']._get(record.model)
|
||||
|
||||
|
||||
def generate_record(record, get_xmlid):
|
||||
""" Return an etree Element for the given record, together with a list of
|
||||
skipped field values (fields in FIELDS_NOT_TO_EXPORT).
|
||||
"""
|
||||
xmlid = get_xmlid(record)
|
||||
skipped = []
|
||||
|
||||
# Create the record node
|
||||
record_node = E.record(id=xmlid, model=record._name, context="{'studio': True}")
|
||||
for name in get_fields_to_export(record):
|
||||
field = record._fields[name]
|
||||
try:
|
||||
record_node.append(generate_field(record, field, get_xmlid))
|
||||
except MissingXMLID:
|
||||
# the field value contains a record without an xml_id; skip it
|
||||
skipped.append((xmlid, field, record[name]))
|
||||
|
||||
# The record contains relational data that don't export, so register it in skipped
|
||||
for name in FIELDS_NOT_TO_EXPORT.get(record._name, ()):
|
||||
if record[name]:
|
||||
field = record._fields[name]
|
||||
skipped.append((xmlid, field, record[name]))
|
||||
|
||||
return record_node, skipped
|
||||
|
||||
def get_fields_to_export(record):
|
||||
fields_to_export = FIELDS_TO_EXPORT.get(record._name)
|
||||
if not fields_to_export:
|
||||
# deduce the fields_to_export from available data
|
||||
fields_to_export = set(record._fields.keys())
|
||||
fields_to_export -= set(models.MAGIC_COLUMNS)
|
||||
fields_to_export.discard(record.CONCURRENCY_CHECK_FIELD)
|
||||
if FIELDS_NOT_TO_EXPORT.get(record._name):
|
||||
fields_to_export -= set(FIELDS_NOT_TO_EXPORT.get(record._name))
|
||||
return fields_to_export
|
||||
|
||||
def generate_field(record, field, get_xmlid):
|
||||
""" Serialize the value of ``field`` on ``record`` as an etree Element. """
|
||||
value = record[field.name]
|
||||
if field.type == 'boolean':
|
||||
return E.field(name=field.name, eval=str(value))
|
||||
elif field.type in ('many2one', 'reference'):
|
||||
if value:
|
||||
return E.field(name=field.name, ref=get_xmlid(value))
|
||||
else:
|
||||
return E.field(name=field.name, eval=u"False")
|
||||
elif field.type in ('many2many', 'one2many'):
|
||||
return E.field(
|
||||
name=field.name,
|
||||
eval='[(6, 0, [%s])]' % ', '.join("ref('%s')" % get_xmlid(v) for v in value),
|
||||
)
|
||||
else:
|
||||
if not value:
|
||||
return E.field(name=field.name, eval=u"False")
|
||||
elif (field.model_name, field.name) in CDATA_FIELDS:
|
||||
# Wrap value in <![CDATA[]] to preserve it to be interpreted as XML markup
|
||||
node = E.field(name=field.name)
|
||||
node.text = etree.CDATA(str(value))
|
||||
return node
|
||||
elif (field.model_name, field.name) in XML_FIELDS:
|
||||
# Use an xml parser to remove new lines and indentations in value
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
return E.field(etree.XML(value, parser), name=field.name, type='xml')
|
||||
else:
|
||||
return E.field(str(value), name=field.name)
|
||||
|
||||
|
||||
def xmlid_getter():
|
||||
""" Return a function that returns the xml_id of a given record. """
|
||||
cache = {}
|
||||
|
||||
def get(record, check=True):
|
||||
""" Return the xml_id of ``record``.
|
||||
Raise a ``MissingXMLID`` if ``check`` is true and xml_id is empty.
|
||||
"""
|
||||
try:
|
||||
res = cache[record]
|
||||
except KeyError:
|
||||
# prefetch when possible
|
||||
records = record.browse(record._prefetch_ids)
|
||||
for rid, val in records.get_external_id().items():
|
||||
cache[record.browse(rid)] = val
|
||||
res = cache[record]
|
||||
if check and not res:
|
||||
raise MissingXMLID(record)
|
||||
return res
|
||||
|
||||
return get
|
||||
|
||||
|
||||
class MissingXMLID(Exception):
|
||||
def __init__(self, record):
|
||||
super(MissingXMLID, self).__init__("Missing XMLID: %s (%s)" % (record, record.display_name))
|
||||
19
web_studio/controllers/ir_http.py
Normal file
19
web_studio/controllers/ir_http.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
def session_info(self):
|
||||
result = super(IrHttp, self).session_info()
|
||||
|
||||
if result['is_system']:
|
||||
# necessary keys for Studio
|
||||
result['dbuuid'] = request.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
||||
result['multi_lang'] = len(request.env['res.lang'].get_installed()) > 1
|
||||
|
||||
return result
|
||||
1560
web_studio/controllers/main.py
Normal file
1560
web_studio/controllers/main.py
Normal file
File diff suppressed because it is too large
Load Diff
316
web_studio/controllers/report.py
Normal file
316
web_studio/controllers/report.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import json
|
||||
|
||||
from lxml import etree
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.web_studio.controllers import main
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class WebStudioReportController(main.WebStudioController):
|
||||
|
||||
@http.route('/web_studio/create_new_report', type='json', auth='user')
|
||||
def create_new_report(self, model_name, layout):
|
||||
|
||||
if layout == 'web.basic_layout':
|
||||
arch_document = etree.fromstring("""
|
||||
<t t-name="studio_report_document">
|
||||
<div class="page"/>
|
||||
</t>
|
||||
""")
|
||||
else:
|
||||
arch_document = etree.fromstring("""
|
||||
<t t-name="studio_report_document">
|
||||
<t t-call="%(layout)s">
|
||||
<div class="page"/>
|
||||
</t>
|
||||
</t>
|
||||
""" % {'layout': layout})
|
||||
|
||||
view_document = request.env['ir.ui.view'].create({
|
||||
'name': 'studio_report_document',
|
||||
'type': 'qweb',
|
||||
'arch': etree.tostring(arch_document, encoding='utf-8', pretty_print=True),
|
||||
})
|
||||
|
||||
new_view_document_xml_id = view_document.get_external_id()[view_document.id]
|
||||
view_document.name = '%s_document' % new_view_document_xml_id
|
||||
view_document.key = '%s_document' % new_view_document_xml_id
|
||||
|
||||
if layout == 'web.basic_layout':
|
||||
arch = etree.fromstring("""
|
||||
<t t-name="studio_main_report">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="%(layout)s">
|
||||
<t t-call="%(document)s_document"/>
|
||||
<p style="page-break-after: always;"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
""" % {'layout': layout, 'document': new_view_document_xml_id})
|
||||
else:
|
||||
arch = etree.fromstring("""
|
||||
<t t-name="studio_main_report">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="%(document)s_document"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
""" % {'document': new_view_document_xml_id})
|
||||
|
||||
view = request.env['ir.ui.view'].create({
|
||||
'name': 'studio_main_report',
|
||||
'type': 'qweb',
|
||||
'arch': etree.tostring(arch, encoding='utf-8', pretty_print=True),
|
||||
})
|
||||
# FIXME: When website is installed, we need to set key as xmlid to search on a valid domain
|
||||
# See '_view_obj' in 'website/model/ir.ui.view'
|
||||
view.name = new_view_document_xml_id
|
||||
view.key = new_view_document_xml_id
|
||||
|
||||
model = request.env['ir.model']._get(model_name)
|
||||
report = request.env['ir.actions.report'].create({
|
||||
'name': _('%s Report', model.name),
|
||||
'model': model.model,
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': view.name,
|
||||
})
|
||||
# make it available in the print menu
|
||||
report.create_action()
|
||||
|
||||
return {
|
||||
'id': report.id,
|
||||
}
|
||||
|
||||
@http.route('/web_studio/print_report', type='json', auth='user')
|
||||
def print_report(self, report_name, record_id):
|
||||
report = request.env['ir.actions.report']._get_report_from_name(report_name)
|
||||
return report.report_action(record_id)
|
||||
|
||||
@http.route('/web_studio/edit_report', type='json', auth='user')
|
||||
def edit_report(self, report_id, values):
|
||||
report = request.env['ir.actions.report'].browse(report_id)
|
||||
if report:
|
||||
if 'attachment_use' in values:
|
||||
if values['attachment_use']:
|
||||
values['attachment'] = "'%s'" % report.name
|
||||
else:
|
||||
# disable saving as attachment altogether
|
||||
values['attachment'] = False
|
||||
if 'groups_id' in values:
|
||||
values['groups_id'] = [(6, 0, values['groups_id'])]
|
||||
if 'display_in_print' in values:
|
||||
if values['display_in_print']:
|
||||
report.create_action()
|
||||
else:
|
||||
report.unlink_action()
|
||||
values.pop('display_in_print')
|
||||
report.write(values)
|
||||
|
||||
return report.read()
|
||||
|
||||
@http.route('/web_studio/read_paperformat', type='json', auth='user')
|
||||
def read_paperformat(self, report_id):
|
||||
report = request.env['ir.actions.report'].browse(report_id)
|
||||
return report.get_paperformat().read()
|
||||
|
||||
@http.route('/web_studio/get_widgets_available_options', type='json', auth='user')
|
||||
def get_widgets_available_options(self):
|
||||
fields = dict()
|
||||
records = request.env['ir.model'].search([('model', 'like', 'ir.qweb.field.%')])
|
||||
for record in records:
|
||||
fields[record.model[14:]] = request.env[record.model].get_available_options()
|
||||
return fields
|
||||
|
||||
@http.route('/web_studio/get_report_views', type='json', auth='user')
|
||||
def get_report_views(self, report_name, record_id=None):
|
||||
if record_id is None:
|
||||
raise UserError(_("To edit this document please create a record first"))
|
||||
loaded = set()
|
||||
views = {}
|
||||
|
||||
def get_report_view(key):
|
||||
view = request.env['ir.ui.view'].search([
|
||||
('key', '=', key),
|
||||
('type', '=', 'qweb'),
|
||||
('mode', '=', 'primary'),
|
||||
], limit=1)
|
||||
if not view:
|
||||
raise UserError(_("No view found for the given report!"))
|
||||
return view
|
||||
|
||||
def process_template_groups(element):
|
||||
""" `get_template` only returns the groups names but we also need
|
||||
need their id and display name in Studio to edit them (many2many
|
||||
tags widget). These data are thus added on the node.
|
||||
This processing is quite similar to what has been done on views.
|
||||
"""
|
||||
for node in element.iter():
|
||||
if node.get('groups'):
|
||||
request.env['ir.ui.view'].set_studio_groups(node)
|
||||
|
||||
def load_arch(view_name):
|
||||
if view_name in loaded:
|
||||
return
|
||||
loaded.add(view_name)
|
||||
|
||||
view = get_report_view(view_name)
|
||||
studio_view = self._get_studio_view(view)
|
||||
element = request.env['ir.qweb'].with_context(full_branding=True)._get_template(view.id)[0]
|
||||
|
||||
process_template_groups(element)
|
||||
|
||||
views[view.id] = {
|
||||
'arch': etree.tostring(element),
|
||||
'key': view.key,
|
||||
'studio_arch': studio_view.arch_db or "<data/>",
|
||||
'studio_view_id': studio_view.id,
|
||||
'view_id': view.id,
|
||||
}
|
||||
|
||||
for node in element.getroottree().findall("//*[@t-call]"):
|
||||
tcall = node.get("t-call")
|
||||
if '{' in tcall:
|
||||
# this t-call value is dynamic (e.g. t-call="{{company.tmp}})
|
||||
# so its corresponding view cannot be read
|
||||
# this template won't be returned to the Editor so it won't
|
||||
# be customizable
|
||||
continue
|
||||
load_arch(tcall)
|
||||
|
||||
return view.id
|
||||
|
||||
load_arch(report_name)
|
||||
main_view_id = get_report_view(report_name).id
|
||||
report_html = self._test_report(report_name, record_id)
|
||||
|
||||
return {
|
||||
'report_html': report_html and report_html[0],
|
||||
'main_view_id': main_view_id,
|
||||
'views': views,
|
||||
}
|
||||
|
||||
@http.route('/web_studio/edit_report_view', type='json', auth='user')
|
||||
def edit_report_view(self, report_name, report_views, record_id, operations=None):
|
||||
# a report can be composed of multiple views (with t-call) ; we might
|
||||
# thus need to apply operations on multiple views
|
||||
|
||||
# create groups of operations by view
|
||||
groups = {}
|
||||
ops = []
|
||||
for op in operations:
|
||||
ops += op.get('inheritance', [op])
|
||||
for op in ops:
|
||||
if str(op['view_id']) not in groups:
|
||||
groups[str(op['view_id'])] = []
|
||||
groups[str(op['view_id'])].append(op)
|
||||
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
for group_view_id in groups:
|
||||
view = request.env['ir.ui.view'].browse(int(group_view_id))
|
||||
if view.key in request.env['ir.ui.view'].TEMPLATE_VIEWS_BLACKLIST:
|
||||
raise ValidationError(_("You cannot modify this view, it is part of the generic layout"))
|
||||
arch = etree.fromstring(report_views[group_view_id]['studio_arch'], parser=parser)
|
||||
|
||||
for op in groups[group_view_id]:
|
||||
if not op.get('type'):
|
||||
# apply changes
|
||||
content = etree.fromstring(op['content'], etree.HTMLParser())
|
||||
for node in content[0]:
|
||||
etree.SubElement(arch, 'xpath', {
|
||||
'expr': op['xpath'],
|
||||
'position': op['position'],
|
||||
}).append(node)
|
||||
else:
|
||||
# call the right operation handler
|
||||
op['position'] = op['type']
|
||||
op['target'] = {
|
||||
'xpath_info': [{
|
||||
'tag': g.split('[')[0],
|
||||
'indice': g.split('[')[1][:-1] if '[' in g else 1
|
||||
} for g in op['xpath'].split('/')[1:]]
|
||||
}
|
||||
getattr(self, '_operation_%s' % (op['type']))(arch, op)
|
||||
|
||||
# Save or create changes into studio view, identifiable by xmlid
|
||||
# Example for view id 42 of model crm.lead: web-studio_crm.lead-42
|
||||
new_arch = etree.tostring(arch, encoding='unicode', pretty_print=True)
|
||||
self._set_studio_view(view, new_arch)
|
||||
|
||||
# Normalize the view
|
||||
# studio_view = self._get_studio_view(view)
|
||||
# try:
|
||||
# normalized_view = studio_view.normalize()
|
||||
# self._set_studio_view(view, normalized_view)
|
||||
# except ValidationError: # Element '<...>' cannot be located in parent view
|
||||
# # If the studio view is not applicable after normalization, let's
|
||||
# # just ignore the normalization step, it's better to have a studio
|
||||
# # view that is not optimized than to prevent the user from making
|
||||
# # the change he would like to make.
|
||||
# self._set_studio_view(view, new_arch)
|
||||
|
||||
# in case of undo, there could be no operation anymore for a view so
|
||||
# the view thus need to be reset
|
||||
intact_view_ids = report_views.keys() - groups.keys()
|
||||
for view_id in intact_view_ids:
|
||||
intact_view = request.env['ir.ui.view'].browse(int(view_id))
|
||||
studio_view = self._get_studio_view(intact_view)
|
||||
if studio_view:
|
||||
studio_view.arch_db = report_views[view_id]['studio_arch']
|
||||
|
||||
result = self.get_report_views(report_name, record_id)
|
||||
|
||||
return result
|
||||
|
||||
@http.route('/web_studio/edit_report_view_arch', type='json', auth='user')
|
||||
def edit_report_view_arch(self, report_name, record_id, view_id, view_arch):
|
||||
view = request.env['ir.ui.view'].browse(view_id)
|
||||
view.write({'arch': view_arch})
|
||||
# TODO: we might need to keep studio_arch as it was before the changes
|
||||
result = self.get_report_views(report_name, record_id)
|
||||
return result
|
||||
|
||||
@http.route('/web_studio/edit_report/test_load_assets', type='json', auth='user')
|
||||
def edit_report_test_load_css(self):
|
||||
Qweb = request.env['ir.qweb']
|
||||
Attachment = request.env['ir.attachment']
|
||||
|
||||
html = Qweb._render('web.report_layout', values={
|
||||
'studio': True,
|
||||
})
|
||||
root = etree.fromstring(html).getroottree()
|
||||
links = [link.get('href') for link in root.findall("//link")]
|
||||
if 'assets' in request.session.debug:
|
||||
domain = []
|
||||
for link in links:
|
||||
if domain:
|
||||
domain = ['|'] + domain
|
||||
domain.append(('name', '=', link.replace('/web/assets/debug/', '')))
|
||||
attachments = Attachment.search(domain)
|
||||
else:
|
||||
link_ids = [int(link.replace('/web/assets/', '').split('-', 1)[0]) for link in links]
|
||||
attachments = Attachment.browse(link_ids)
|
||||
css = {a.name: base64.b64decode(a.datas) for a in attachments}
|
||||
|
||||
return {
|
||||
"css": css
|
||||
}
|
||||
|
||||
def _test_report(self, report_name, record_id):
|
||||
# render the report to catch a rendering error
|
||||
try:
|
||||
return request.env['ir.actions.report']._render_qweb_html(report_name, [record_id], {
|
||||
'full_branding': True,
|
||||
'studio': True,
|
||||
})
|
||||
except Exception as err:
|
||||
# the report could not be rendered which probably means the last
|
||||
# operation was incorrect
|
||||
return [{
|
||||
"error": err,
|
||||
"message": str(err),
|
||||
}]
|
||||
12
web_studio/data/mail_activity_type_data.xml
Normal file
12
web_studio/data/mail_activity_type_data.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mail_activity_data_approve" model="mail.activity.type">
|
||||
<field name="name">Grant Approval</field>
|
||||
<field name="icon">fa-check</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="category">grant_approval</field>
|
||||
<field name="sequence">99</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
7
web_studio/data/mail_templates.xml
Normal file
7
web_studio/data/mail_templates.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="notify_approval">
|
||||
<p t-if="approved">Approved as <em t-esc="group_name"/> <i class="fa fa-thumbs-up text-success"/></p>
|
||||
<p t-else="">Rejected as <em t-esc="group_name"/> <i class="fa fa-thumbs-down text-danger"/></p>
|
||||
</template>
|
||||
</odoo>
|
||||
1953
web_studio/i18n/af.po
Normal file
1953
web_studio/i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/am.po
Normal file
1952
web_studio/i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
web_studio/i18n/ar.po
Normal file
4590
web_studio/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
4509
web_studio/i18n/az.po
Normal file
4509
web_studio/i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
1953
web_studio/i18n/bg.po
Normal file
1953
web_studio/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
2868
web_studio/i18n/bs.po
Normal file
2868
web_studio/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
4589
web_studio/i18n/ca.po
Normal file
4589
web_studio/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
web_studio/i18n/cs.po
Normal file
4586
web_studio/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
3193
web_studio/i18n/da.po
Normal file
3193
web_studio/i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
4634
web_studio/i18n/de.po
Normal file
4634
web_studio/i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
3016
web_studio/i18n/el.po
Normal file
3016
web_studio/i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/en_GB.po
Normal file
1952
web_studio/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
4633
web_studio/i18n/es.po
Normal file
4633
web_studio/i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_AR.po
Normal file
1952
web_studio/i18n/es_AR.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_BO.po
Normal file
1952
web_studio/i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_CL.po
Normal file
1952
web_studio/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_CO.po
Normal file
1952
web_studio/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_CR.po
Normal file
1952
web_studio/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_DO.po
Normal file
1952
web_studio/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_EC.po
Normal file
1952
web_studio/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
4634
web_studio/i18n/es_MX.po
Normal file
4634
web_studio/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_PE.po
Normal file
1952
web_studio/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_PY.po
Normal file
1952
web_studio/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/es_VE.po
Normal file
1952
web_studio/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
2874
web_studio/i18n/et.po
Normal file
2874
web_studio/i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1954
web_studio/i18n/eu.po
Normal file
1954
web_studio/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
2872
web_studio/i18n/fa.po
Normal file
2872
web_studio/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
2876
web_studio/i18n/fi.po
Normal file
2876
web_studio/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/fo.po
Normal file
1952
web_studio/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
4663
web_studio/i18n/fr.po
Normal file
4663
web_studio/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
28
web_studio/i18n/fr_CA.po
Normal file
28
web_studio/i18n/fr_CA.po
Normal file
@@ -0,0 +1,28 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * web_studio
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux <mat@odoo.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 11.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-10-24 09:24+0000\n"
|
||||
"PO-Revision-Date: 2017-10-24 09:24+0000\n"
|
||||
"Last-Translator: Martin Trigaux <mat@odoo.com>, 2017\n"
|
||||
"Language-Team: French (Canada) (https://www.transifex.com/odoo/teams/41243/"
|
||||
"fr_CA/)\n"
|
||||
"Language: fr_CA\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: web_studio
|
||||
#. openerp-web
|
||||
#: code:addons/web_studio/static/src/xml/view_editor_sidebar.xml:0
|
||||
#: model:ir.model.fields,field_description:web_studio.field_studio_mixin__id
|
||||
#, python-format
|
||||
msgid "ID"
|
||||
msgstr "Identifiant"
|
||||
1952
web_studio/i18n/gl.po
Normal file
1952
web_studio/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
2871
web_studio/i18n/gu.po
Normal file
2871
web_studio/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
4558
web_studio/i18n/he.po
Normal file
4558
web_studio/i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
3174
web_studio/i18n/hr.po
Normal file
3174
web_studio/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
3170
web_studio/i18n/hu.po
Normal file
3170
web_studio/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
2215
web_studio/i18n/hy.po
Normal file
2215
web_studio/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load Diff
4507
web_studio/i18n/id.po
Normal file
4507
web_studio/i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
2869
web_studio/i18n/is.po
Normal file
2869
web_studio/i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
4633
web_studio/i18n/it.po
Normal file
4633
web_studio/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
4542
web_studio/i18n/ja.po
Normal file
4542
web_studio/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
1954
web_studio/i18n/ka.po
Normal file
1954
web_studio/i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/kab.po
Normal file
1952
web_studio/i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
2869
web_studio/i18n/km.po
Normal file
2869
web_studio/i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
4531
web_studio/i18n/ko.po
Normal file
4531
web_studio/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
3161
web_studio/i18n/lb.po
Normal file
3161
web_studio/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/lo.po
Normal file
1952
web_studio/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
3176
web_studio/i18n/lt.po
Normal file
3176
web_studio/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1953
web_studio/i18n/lv.po
Normal file
1953
web_studio/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/mk.po
Normal file
1952
web_studio/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
3170
web_studio/i18n/mn.po
Normal file
3170
web_studio/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
2217
web_studio/i18n/my.po
Normal file
2217
web_studio/i18n/my.po
Normal file
File diff suppressed because it is too large
Load Diff
4513
web_studio/i18n/nb.po
Normal file
4513
web_studio/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
1954
web_studio/i18n/ne.po
Normal file
1954
web_studio/i18n/ne.po
Normal file
File diff suppressed because it is too large
Load Diff
4636
web_studio/i18n/nl.po
Normal file
4636
web_studio/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/nl_BE.po
Normal file
1952
web_studio/i18n/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
3206
web_studio/i18n/pl.po
Normal file
3206
web_studio/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
3161
web_studio/i18n/pt.po
Normal file
3161
web_studio/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
3183
web_studio/i18n/pt_BR.po
Normal file
3183
web_studio/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
4620
web_studio/i18n/ro.po
Normal file
4620
web_studio/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
4561
web_studio/i18n/ru.po
Normal file
4561
web_studio/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
2870
web_studio/i18n/sk.po
Normal file
2870
web_studio/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/sl.po
Normal file
1952
web_studio/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1952
web_studio/i18n/sq.po
Normal file
1952
web_studio/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
2867
web_studio/i18n/sr.po
Normal file
2867
web_studio/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
1955
web_studio/i18n/sr@latin.po
Normal file
1955
web_studio/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
3020
web_studio/i18n/sv.po
Normal file
3020
web_studio/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
4597
web_studio/i18n/th.po
Normal file
4597
web_studio/i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
web_studio/i18n/tr.po
Normal file
4590
web_studio/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
4616
web_studio/i18n/uk.po
Normal file
4616
web_studio/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
4608
web_studio/i18n/vi.po
Normal file
4608
web_studio/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
4546
web_studio/i18n/zh_CN.po
Normal file
4546
web_studio/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
4510
web_studio/i18n/zh_TW.po
Normal file
4510
web_studio/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
22
web_studio/models/__init__.py
Normal file
22
web_studio/models/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from . import studio_mixin
|
||||
from . import base_automation
|
||||
from . import mail_activity
|
||||
from . import mail_activity_type
|
||||
from . import mail_template
|
||||
from . import mail_thread
|
||||
from . import ir_actions_act_window
|
||||
from . import ir_actions_report
|
||||
from . import ir_actions_server
|
||||
from . import ir_filters
|
||||
from . import ir_model
|
||||
from . import ir_model_data
|
||||
from . import ir_module_module
|
||||
from . import ir_rule
|
||||
from . import ir_ui_menu
|
||||
from . import ir_ui_view
|
||||
from . import res_groups
|
||||
from . import res_company
|
||||
from . import ir_qweb
|
||||
from . import report_paperformat
|
||||
from . import studio_approval
|
||||
from . import ir_default
|
||||
9
web_studio/models/base_automation.py
Normal file
9
web_studio/models/base_automation.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class BaseAutomation(models.Model):
|
||||
_name = 'base.automation'
|
||||
_inherit = ['studio.mixin', 'base.automation']
|
||||
14
web_studio/models/ir_actions_act_window.py
Normal file
14
web_studio/models/ir_actions_act_window.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrActionsActWindow(models.Model):
|
||||
_name = 'ir.actions.act_window'
|
||||
_inherit = ['studio.mixin', 'ir.actions.act_window']
|
||||
|
||||
|
||||
class IrActionsActWindowView(models.Model):
|
||||
_name = 'ir.actions.act_window.view'
|
||||
_inherit = ['studio.mixin', 'ir.actions.act_window.view']
|
||||
66
web_studio/models/ir_actions_report.py
Normal file
66
web_studio/models/ir_actions_report.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_name = 'ir.actions.report'
|
||||
_inherit = ['studio.mixin', 'ir.actions.report']
|
||||
|
||||
@api.model
|
||||
def _render_qweb_html(self, report_ref, docids, data=None):
|
||||
report = self._get_report(report_ref)
|
||||
if data and data.get('full_branding'):
|
||||
self = self.with_context(full_branding=True)
|
||||
if data and data.get('studio') and report.report_type == 'qweb-pdf':
|
||||
data['report_type'] = 'pdf'
|
||||
return super(IrActionsReport, self)._render_qweb_html(report_ref, docids, data)
|
||||
|
||||
def copy_report_and_template(self):
|
||||
new = self.copy()
|
||||
view = self.env['ir.ui.view'].search([
|
||||
('type', '=', 'qweb'),
|
||||
('key', '=', new.report_name),
|
||||
], limit=1)
|
||||
view.ensure_one()
|
||||
new_view = view.with_context(lang=None).copy_qweb_template()
|
||||
copy_no = int(new_view.key.split('_copy_').pop())
|
||||
|
||||
new.write({
|
||||
'xml_id': '%s_copy_%s' % (new.xml_id, copy_no),
|
||||
'name': '%s copy(%s)' % (new.name, copy_no),
|
||||
'report_name': '%s_copy_%s' % (new.report_name, copy_no),
|
||||
'report_file': new_view.key, # TODO: are we sure about this?
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _get_rendering_context_model(self, report):
|
||||
# If the report is a copy of another report, and this report is using a custom model to render its html,
|
||||
# we must use the custom model of the original report.
|
||||
report_model_name = 'report.%s' % report.report_name
|
||||
report_model = self.env.get(report_model_name)
|
||||
|
||||
if report_model is None:
|
||||
parts = report_model_name.split('_copy_')
|
||||
if any(not part.isdecimal() for part in parts[1:]):
|
||||
return report_model
|
||||
report_model_name = parts[0]
|
||||
report_model = self.env.get(report_model_name)
|
||||
|
||||
return report_model
|
||||
|
||||
def associated_view(self):
|
||||
action_data = super(IrActionsReport, self).associated_view()
|
||||
domain = expression.normalize_domain(action_data['domain'])
|
||||
|
||||
view_name = self.report_name.split('.')[1].split('_copy_')[0]
|
||||
|
||||
domain = expression.OR([
|
||||
domain,
|
||||
['&', ('name', 'ilike', view_name), ('type', '=', 'qweb')]
|
||||
])
|
||||
|
||||
action_data['domain'] = domain
|
||||
return action_data
|
||||
9
web_studio/models/ir_actions_server.py
Normal file
9
web_studio/models/ir_actions_server.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrActionsServer(models.Model):
|
||||
_name = 'ir.actions.server'
|
||||
_inherit = ['studio.mixin', 'ir.actions.server']
|
||||
9
web_studio/models/ir_default.py
Normal file
9
web_studio/models/ir_default.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrDefault(models.Model):
|
||||
_name = 'ir.default'
|
||||
_inherit = ['studio.mixin', 'ir.default']
|
||||
9
web_studio/models/ir_filters.py
Normal file
9
web_studio/models/ir_filters.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrFilters(models.Model):
|
||||
_name = 'ir.filters'
|
||||
_inherit = ['studio.mixin', 'ir.filters']
|
||||
643
web_studio/models/ir_model.py
Normal file
643
web_studio/models/ir_model.py
Normal file
@@ -0,0 +1,643 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import unicodedata
|
||||
import uuid
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from odoo.osv import expression
|
||||
from odoo import api, fields, models, _, Command
|
||||
from odoo.tools import ustr
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
OPTIONS_WL = [
|
||||
'use_mail', # add mail_thread to record
|
||||
'use_active', # allows to archive records (active field)
|
||||
'use_responsible', # add user field
|
||||
'use_partner', # adds partner and related phone and email fields
|
||||
'use_company', # add company field and corresponding access rules
|
||||
'use_notes', # html note field
|
||||
'use_date', # date field
|
||||
'use_double_dates', # date start and date begin
|
||||
'use_value', # value and currency
|
||||
'use_image', # image field
|
||||
'use_sequence', # allows to order records (sequence field)
|
||||
'lines', # create a default One2Many targeting a generated lines models
|
||||
'use_stages', # add stages and stage model to record (kanban)
|
||||
'use_tags' # add tags and tags model to record (kanban)
|
||||
]
|
||||
|
||||
|
||||
def sanitize_for_xmlid(s):
|
||||
""" Transforms a string to a name suitable for use in an xmlid.
|
||||
Strips leading and trailing spaces, converts unicode chars to ascii,
|
||||
lowers all chars, replaces spaces with underscores and truncates the
|
||||
resulting string to 20 characters.
|
||||
:param s: str
|
||||
:rtype: str
|
||||
"""
|
||||
s = ustr(s)
|
||||
uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
|
||||
|
||||
slug_str = re.sub('[\W]', ' ', uni).strip().lower()
|
||||
slug_str = re.sub('[-\s]+', '_', slug_str)
|
||||
return slug_str[:20]
|
||||
|
||||
|
||||
class Base(models.AbstractModel):
|
||||
_inherit = 'base'
|
||||
|
||||
def create_studio_model_data(self, name):
|
||||
""" We want to keep track of created records with studio
|
||||
(ex: model, field, view, action, menu, etc.).
|
||||
An ir.model.data is created whenever a record of one of these models
|
||||
is created, tagged with studio.
|
||||
"""
|
||||
IrModelData = self.env['ir.model.data']
|
||||
|
||||
# Check if there is already an ir.model.data for the given resource
|
||||
data = IrModelData.search([
|
||||
('model', '=', self._name), ('res_id', '=', self.id)
|
||||
])
|
||||
if data:
|
||||
data.write({}) # force a write to set the 'studio' and 'noupdate' flags to True
|
||||
else:
|
||||
module = self.env['ir.module.module'].get_studio_module()
|
||||
IrModelData.create({
|
||||
'name': '%s_%s' % (sanitize_for_xmlid(name), uuid.uuid4()),
|
||||
'model': self._name,
|
||||
'res_id': self.id,
|
||||
'module': module.name,
|
||||
})
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_name = 'ir.model'
|
||||
_inherit = ['studio.mixin', 'ir.model']
|
||||
|
||||
abstract = fields.Boolean(compute='_compute_abstract',
|
||||
store=False,
|
||||
help="Whether this model is abstract",
|
||||
search='_search_abstract')
|
||||
|
||||
def _compute_abstract(self):
|
||||
for record in self:
|
||||
record.abstract = self.env[record.model]._abstract
|
||||
|
||||
def _search_abstract(self, operator, value):
|
||||
abstract_models = [
|
||||
model._name
|
||||
for model in self.env.values()
|
||||
if model._abstract
|
||||
]
|
||||
dom_operator = 'in' if (operator, value) in [('=', True), ('!=', False)] else 'not in'
|
||||
|
||||
return [('model', dom_operator, abstract_models)]
|
||||
|
||||
@api.model
|
||||
def studio_model_create(self, name, options=()):
|
||||
""" Allow quick creation of models through Studio.
|
||||
|
||||
:param name: functional name of the model (_description attribute)
|
||||
:param options: list of options that can trigger automated behaviours,
|
||||
in the form of 'use_<behaviour>' (e.g. 'use_tags')
|
||||
:return: the main model created as well as extra models needed for the
|
||||
requested behaviours (e.g. tag or stage models) in the form of
|
||||
a tuple (main_model, extra_models)
|
||||
:rtype: tuple
|
||||
"""
|
||||
options = set(options)
|
||||
use_mail = 'use_mail' in options
|
||||
|
||||
model_values = {
|
||||
'name': name,
|
||||
'model': 'x_' + sanitize_for_xmlid(name),
|
||||
'is_mail_thread': use_mail,
|
||||
'is_mail_activity': use_mail,
|
||||
'field_id': [
|
||||
Command.create({
|
||||
'name': 'x_name',
|
||||
'ttype': 'char',
|
||||
'required': True,
|
||||
'field_description': _('Description'),
|
||||
'translate': True,
|
||||
'tracking': use_mail,
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
# now let's check other options and accumulate potential extra models (tags, stages)
|
||||
# created during this process, they will need to get their own action and menu
|
||||
# (which will be done at the controller level)
|
||||
if 'use_stages' in options:
|
||||
options.add('use_sequence')
|
||||
extra_models_keys = []
|
||||
extra_models_values = []
|
||||
|
||||
options.discard('use_mail')
|
||||
for option in OPTIONS_WL:
|
||||
if option in options:
|
||||
method = f'_create_option_{option}'
|
||||
model_to_create = getattr(self, method)(model_values)
|
||||
if model_to_create:
|
||||
extra_models_keys.append(option)
|
||||
extra_models_values.append(model_to_create)
|
||||
|
||||
all_models = self.create([model_values] + extra_models_values)
|
||||
main_model, *extra_models = all_models
|
||||
extra_models_dict = dict(zip(extra_models_keys, extra_models))
|
||||
|
||||
all_models._setup_access_rights()
|
||||
|
||||
for option in OPTIONS_WL:
|
||||
if option in options:
|
||||
method = f'_post_create_option_{option}'
|
||||
getattr(main_model, method, lambda m: None)(extra_models_dict.get(option))
|
||||
|
||||
self.env['ir.ui.view'].create_automatic_views(main_model.model)
|
||||
|
||||
ListEditableView = self.env['ir.ui.view'].with_context(list_editable="bottom")
|
||||
for extra_model in extra_models:
|
||||
ListEditableView.create_automatic_views(extra_model.model)
|
||||
|
||||
models_with_menu = self.browse(
|
||||
model.id
|
||||
for key, model in extra_models_dict.items()
|
||||
if key in ('use_stages', 'use_tags')
|
||||
)
|
||||
return (main_model, models_with_menu)
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
if self._context.get('studio'):
|
||||
(main_model, _) = self.studio_model_create(name)
|
||||
return main_model.name_get()[0]
|
||||
return super().name_create(name)
|
||||
|
||||
def _create_option_lines(self, model_vals):
|
||||
""" Creates a new model (with sequence and description fields) and a
|
||||
one2many field pointing to that model.
|
||||
"""
|
||||
# create the Line model
|
||||
line_model_values, field_values = self._values_lines(model_vals.get('model'))
|
||||
|
||||
model_vals['field_id'].append(
|
||||
Command.create(field_values)
|
||||
)
|
||||
return line_model_values
|
||||
|
||||
def _setup_one2many_lines(self):
|
||||
# create the Line model
|
||||
model_values, field_values = self._values_lines(self.model)
|
||||
line_model = self.create(model_values)
|
||||
line_model._setup_access_rights()
|
||||
self.env['ir.ui.view'].create_automatic_views(line_model.model)
|
||||
field_values['model_id'] = self.id
|
||||
return self.env['ir.model.fields'].create(field_values)
|
||||
|
||||
def _values_lines(self, model_name):
|
||||
""" Creates a new model (with sequence and description fields) and a
|
||||
one2many field pointing to that model.
|
||||
"""
|
||||
# create the Line model
|
||||
model_table = model_name.replace('.', '_')
|
||||
if not model_table.startswith('x_'):
|
||||
model_table = 'x_' + model_table
|
||||
model_line_name = model_table[2:] + '_line'
|
||||
model_line_model = model_table + '_line_' + uuid.uuid4().hex[:5]
|
||||
relation_field_name = model_table + '_id'
|
||||
line_model_values = {
|
||||
'name': model_line_name,
|
||||
'model': model_line_model,
|
||||
'field_id': [
|
||||
Command.create({
|
||||
'name': 'x_studio_sequence',
|
||||
'ttype': 'integer',
|
||||
'field_description': _('Sequence'),
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'x_name',
|
||||
'ttype': 'char',
|
||||
'required': True,
|
||||
'field_description': _('Description'),
|
||||
'translate': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': relation_field_name,
|
||||
'ttype': 'many2one',
|
||||
'relation': model_name,
|
||||
}),
|
||||
],
|
||||
}
|
||||
field_values = {
|
||||
'name': model_table + '_line_ids_' + uuid.uuid4().hex[:5],
|
||||
'ttype': 'one2many',
|
||||
'relation': model_line_model,
|
||||
'relation_field': relation_field_name,
|
||||
'field_description': _('New Lines'),
|
||||
}
|
||||
return line_model_values, field_values
|
||||
|
||||
def _create_option_use_active(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_active', # can't use x_studio_active as not supported by ORM
|
||||
'ttype': 'boolean',
|
||||
'field_description': _('Active'),
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
})
|
||||
)
|
||||
|
||||
def _post_create_option_use_active(self, _model):
|
||||
self.env['ir.default'].set(self.model, 'x_active', True)
|
||||
|
||||
def _create_option_use_sequence(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_sequence',
|
||||
'ttype': 'integer',
|
||||
'field_description': _('Sequence'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
model_vals['order'] = 'x_studio_sequence asc, id asc'
|
||||
|
||||
def _post_create_option_use_sequence(self, _model):
|
||||
self.env['ir.default'].set(self.model, 'x_studio_sequence', 10)
|
||||
|
||||
def _create_option_use_responsible(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_user_id',
|
||||
'ttype': 'many2one',
|
||||
'relation': 'res.users',
|
||||
'domain': "[('share', '=', False)]",
|
||||
'field_description': _('Responsible'),
|
||||
'copied': True,
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_partner(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_partner_id',
|
||||
'ttype': 'many2one',
|
||||
'relation': 'res.partner',
|
||||
'field_description': _('Contact'),
|
||||
'copied': True,
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
})
|
||||
)
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_partner_phone',
|
||||
'ttype': 'char',
|
||||
'related': 'x_studio_partner_id.phone',
|
||||
'field_description': _('Phone'),
|
||||
})
|
||||
)
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_partner_email',
|
||||
'ttype': 'char',
|
||||
'related': 'x_studio_partner_id.email',
|
||||
'field_description': _('Email'),
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_company(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_company_id',
|
||||
'ttype': 'many2one',
|
||||
'relation': 'res.company',
|
||||
'field_description': _('Company'),
|
||||
'copied': True,
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
})
|
||||
)
|
||||
|
||||
def _post_create_option_use_company(self, _model):
|
||||
# generate default for each company (note: also done when creating a new company)
|
||||
self.env['ir.rule'].create({
|
||||
'name': '%s - Multi-Company' % self.name,
|
||||
'model_id': self.id,
|
||||
'domain_force': "['|', ('x_studio_company_id', '=', False), ('x_studio_company_id', 'in', company_ids)]"
|
||||
})
|
||||
for company in self.env['res.company'].sudo().search([]):
|
||||
self.env['ir.default'].set(self.model, 'x_studio_company_id', company.id, company_id=company.id)
|
||||
|
||||
def _create_option_use_notes(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_notes',
|
||||
'ttype': 'html',
|
||||
'field_description': _('Notes'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_date(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_date',
|
||||
'ttype': 'date',
|
||||
'field_description': _('Date'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_double_dates(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_date_stop',
|
||||
'ttype': 'datetime',
|
||||
'field_description': _('End Date'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_date_start',
|
||||
'ttype': 'datetime',
|
||||
'field_description': _('Start Date'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_value(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_currency_id',
|
||||
'ttype': 'many2one',
|
||||
'relation': 'res.currency',
|
||||
'field_description': _('Currency'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_value',
|
||||
'ttype': 'float',
|
||||
'field_description': _('Value'),
|
||||
'copied': True,
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
})
|
||||
)
|
||||
|
||||
def _post_create_option_use_value(self, _model):
|
||||
for company in self.env['res.company'].sudo().search([]):
|
||||
self.env['ir.default'].set(self.model, 'x_studio_currency_id', company.currency_id.id, company_id=company.id)
|
||||
|
||||
def _create_option_use_image(self, model_vals):
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_image',
|
||||
'ttype': 'binary',
|
||||
'field_description': _('Image'),
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
|
||||
def _create_option_use_stages(self, model_vals):
|
||||
# 1. Create the stage model
|
||||
stage_model_vals = {
|
||||
'name': '%s Stages' % model_vals.get('name'),
|
||||
'model': '%s_stage' % model_vals.get('model'),
|
||||
'field_id': [
|
||||
Command.create({
|
||||
'name': 'x_name',
|
||||
'ttype': 'char',
|
||||
'required': True,
|
||||
'field_description': _('Stage Name'),
|
||||
'translate': True,
|
||||
'copied': True,
|
||||
})
|
||||
],
|
||||
}
|
||||
self._create_option_use_sequence(stage_model_vals)
|
||||
|
||||
# 2. Link our model with the tag model
|
||||
model_vals['field_id'].extend([
|
||||
Command.create({
|
||||
'name': 'x_studio_stage_id',
|
||||
'ttype': 'many2one',
|
||||
'relation': stage_model_vals['model'],
|
||||
'on_delete': 'restrict',
|
||||
'required': True,
|
||||
'field_description': _('Stage'),
|
||||
'tracking': model_vals.get('is_mail_thread'),
|
||||
'copied': True,
|
||||
'group_expand': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'x_studio_priority',
|
||||
'ttype': 'boolean',
|
||||
'field_description': _('High Priority'),
|
||||
'copied': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'x_color',
|
||||
'ttype': 'integer',
|
||||
'field_description': _('Color'),
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'x_studio_kanban_state',
|
||||
'ttype': 'selection',
|
||||
'selection_ids': [
|
||||
Command.create({'value': 'normal', 'name': _('In Progress'), 'sequence': 10}),
|
||||
Command.create({'value': 'done', 'name': _('Ready'), 'sequence': 20}),
|
||||
Command.create({'value': 'blocked', 'name': _('Blocked'), 'sequence': 30}),
|
||||
],
|
||||
'field_description': _('Kanban State'),
|
||||
'copied': True,
|
||||
}),
|
||||
])
|
||||
model_vals['order'] = 'x_studio_priority desc, x_studio_sequence asc, id asc'
|
||||
return stage_model_vals
|
||||
|
||||
def _post_create_option_use_stages(self, stage_model):
|
||||
# create stage 'New','In Progress','Done' and set 'New' as default
|
||||
stages = self.env[stage_model.model].create([
|
||||
{'x_name': _('New')},
|
||||
{'x_name': _('In Progress')},
|
||||
{'x_name': _('Done')}
|
||||
])
|
||||
self.env['ir.default'].set(self.model, 'x_studio_stage_id', stages[0].id)
|
||||
|
||||
def _create_option_use_tags(self, model_vals):
|
||||
# 1. Create the tag model
|
||||
tag_model_vals = {
|
||||
'name': '%s Tags' % model_vals.get('name'),
|
||||
'model': '%s_tag' % model_vals.get('model'),
|
||||
'field_id': [
|
||||
Command.create({
|
||||
'name': 'x_name',
|
||||
'ttype': 'char',
|
||||
'required': True,
|
||||
'field_description': _('Name'),
|
||||
'copied': True,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'x_color',
|
||||
'ttype': 'integer',
|
||||
'field_description': _('Color'),
|
||||
'copied': True,
|
||||
}),
|
||||
],
|
||||
}
|
||||
# 2. Link our model with the tag model
|
||||
model_vals['field_id'].append(
|
||||
Command.create({
|
||||
'name': 'x_studio_tag_ids',
|
||||
'ttype': 'many2many',
|
||||
'relation': tag_model_vals['model'],
|
||||
'field_description': _('Tags'),
|
||||
'relation_table': '%s_tag_rel' % model_vals.get('model'),
|
||||
'column1': '%s_id' % model_vals.get('model'),
|
||||
'column2': 'x_tag_id',
|
||||
'copied': True,
|
||||
})
|
||||
)
|
||||
return tag_model_vals
|
||||
|
||||
def _setup_access_rights(self):
|
||||
for model in self:
|
||||
# Give all access to the created model to Employees by default, except deletion. All access to System
|
||||
# Note: a better solution may be to create groups at the app creation but the model is created
|
||||
# before the app and for other models we need to have info about the app.
|
||||
self.env['ir.model.access'].create({
|
||||
'name': model.name + ' group_system',
|
||||
'model_id': model.id,
|
||||
'group_id': self.env.ref('base.group_system').id,
|
||||
'perm_read': True,
|
||||
'perm_write': True,
|
||||
'perm_create': True,
|
||||
'perm_unlink': True,
|
||||
})
|
||||
self.env['ir.model.access'].create({
|
||||
'name': model.name + ' group_user',
|
||||
'model_id': model.id,
|
||||
'group_id': self.env.ref('base.group_user').id,
|
||||
'perm_read': True,
|
||||
'perm_write': True,
|
||||
'perm_create': True,
|
||||
'perm_unlink': False,
|
||||
})
|
||||
return True
|
||||
|
||||
def _get_default_view(self, view_type, view_id=False, create=True):
|
||||
"""Get the default view for a given model.
|
||||
|
||||
By default, create a view if one does not exist.
|
||||
"""
|
||||
self.ensure_one()
|
||||
View = self.env['ir.ui.view']
|
||||
# If we have no view_id to inherit from, it's because we are adding
|
||||
# fields to the default view of a new model. We will materialize the
|
||||
# default view as a true view so we can keep using our xpath mechanism.
|
||||
if view_id:
|
||||
view = View.browse(view_id)
|
||||
elif create:
|
||||
arch = self.env[self.model].get_view(view_id, view_type)['arch']
|
||||
# set sample data when activating a pivot/graph view through studio
|
||||
if view_type in ['graph', 'pivot']:
|
||||
sample_view_arch = ET.fromstring(arch)
|
||||
sample_view_arch.set('sample', '1')
|
||||
arch = ET.tostring(sample_view_arch, encoding='unicode')
|
||||
view = View.create({
|
||||
'type': view_type,
|
||||
'model': self.model,
|
||||
'arch': arch,
|
||||
'name': "Default %s view for %s" % (view_type, self),
|
||||
})
|
||||
else:
|
||||
view = View.browse(View.default_view(self.model, view_type))
|
||||
return view
|
||||
|
||||
def _create_default_action(self, name):
|
||||
"""Create an ir.act_window record set up with the available view types set up."""
|
||||
self.ensure_one()
|
||||
model_views = self.env['ir.ui.view'].search_read([('model', '=', self.model), ('type', '!=', 'search')],
|
||||
fields=['type'])
|
||||
available_view_types = set(map(lambda v: v['type'], model_views))
|
||||
# in actions, kanban should be first, then list, etc.
|
||||
# this is arbitrary, but we need consistency!
|
||||
VIEWS_ORDER = {'kanban': 0, 'tree': 1, 'form': 2, 'calendar': 3, 'gantt': 4, 'map': 5,
|
||||
'pivot': 6, 'graph': 7, 'qweb': 8, 'activity': 9}
|
||||
sorted_view_types = list(sorted(available_view_types, key=lambda vt: VIEWS_ORDER.get(vt, 10)))
|
||||
view_mode = ','.join(sorted_view_types) if sorted_view_types else 'tree,form'
|
||||
action = self.env['ir.actions.act_window'].create({
|
||||
'name': name,
|
||||
'res_model': self.model,
|
||||
'view_mode': view_mode,
|
||||
'help': _("""
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
This is your new action.
|
||||
</p>
|
||||
<p>By default, it contains a list and a form view and possibly
|
||||
other view types depending on the options you chose for your model.
|
||||
</p>
|
||||
<p>
|
||||
You can start customizing these screens by clicking on the Studio icon on the
|
||||
top right corner (you can also customize this help message there).
|
||||
</p>
|
||||
"""),
|
||||
})
|
||||
return action
|
||||
|
||||
class IrModelField(models.Model):
|
||||
_name = 'ir.model.fields'
|
||||
_inherit = ['studio.mixin', 'ir.model.fields']
|
||||
|
||||
@property
|
||||
def _rec_names_search(self):
|
||||
if self._context.get('studio'):
|
||||
return ['name', 'field_description', 'model', 'model_id.name']
|
||||
return ['field_description']
|
||||
|
||||
def name_get(self):
|
||||
if self.env.context.get('studio'):
|
||||
return [(field.id, "%s (%s)" % (field.field_description, field.model_id.name)) for field in self]
|
||||
return super(IrModelField, self).name_get()
|
||||
|
||||
@api.constrains('name')
|
||||
def _check_name(self):
|
||||
super()._check_name()
|
||||
for field in self:
|
||||
if '__' in field.name:
|
||||
raise ValidationError(_("Custom field names cannot contain double underscores."))
|
||||
|
||||
@api.model
|
||||
def _get_next_relation(self, model_name, comodel_name):
|
||||
"""Prevent using the same m2m relation table when adding the same field.
|
||||
|
||||
If the same m2m field was already added on the model, the user is in fact
|
||||
trying to add another relation - not the same one. We need to create another
|
||||
relation table.
|
||||
"""
|
||||
result = super()._custom_many2many_names(model_name, comodel_name)[0]
|
||||
# check if there's already a m2m field from model_name to comodel_name;
|
||||
# if yes, check the relation table and add a sequence to it - we want to
|
||||
# be able to mirror these fields on the other side in the same order
|
||||
base = result
|
||||
attempt = 0
|
||||
existing_m2m = self.search([
|
||||
('model', '=', model_name),
|
||||
('relation', '=', comodel_name),
|
||||
('relation_table', '=', result)
|
||||
])
|
||||
while existing_m2m:
|
||||
attempt += 1
|
||||
result = '%s_%s' % (base, attempt)
|
||||
existing_m2m = self.search([
|
||||
('model', '=', model_name),
|
||||
('relation', '=', comodel_name),
|
||||
('relation_table', '=', result)
|
||||
])
|
||||
return result
|
||||
|
||||
|
||||
class IrModelAccess(models.Model):
|
||||
_name = 'ir.model.access'
|
||||
_inherit = ['studio.mixin', 'ir.model.access']
|
||||
44
web_studio/models/ir_model_data.py
Normal file
44
web_studio/models/ir_model_data.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class IrModelData(models.Model):
|
||||
_inherit = 'ir.model.data'
|
||||
|
||||
studio = fields.Boolean(help='Checked if it has been edited with Studio.')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if self._context.get('studio'):
|
||||
for vals in vals_list:
|
||||
vals['studio'] = True
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
""" When editing an ir.model.data with Studio, we put it in noupdate to
|
||||
avoid the customizations to be dropped when upgrading the module.
|
||||
"""
|
||||
if self._context.get('studio'):
|
||||
vals['noupdate'] = True
|
||||
vals['studio'] = True
|
||||
return super(IrModelData, self).write(vals)
|
||||
|
||||
def _build_update_xmlids_query(self, sub_rows, update):
|
||||
'''Override of the base method to include the `studio` attribute for studio module imports.'''
|
||||
if self._context.get('studio'):
|
||||
rowf = "(%s, %s, %s, %s, %s, 't')"
|
||||
return """
|
||||
INSERT INTO ir_model_data (module, name, model, res_id, noupdate, studio)
|
||||
VALUES {rows}
|
||||
ON CONFLICT (module, name)
|
||||
DO UPDATE SET (model, res_id, write_date, noupdate) =
|
||||
(EXCLUDED.model, EXCLUDED.res_id, now() at time zone 'UTC', 't')
|
||||
{where}
|
||||
""".format(
|
||||
rows=", ".join([rowf] * len(sub_rows)),
|
||||
where="WHERE NOT ir_model_data.noupdate" if update else "",
|
||||
)
|
||||
else:
|
||||
return super(IrModelData, self)._build_update_xmlids_query(sub_rows, update)
|
||||
32
web_studio/models/ir_module_module.py
Normal file
32
web_studio/models/ir_module_module.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class IrModuleModule(models.Model):
|
||||
_inherit = 'ir.module.module'
|
||||
|
||||
@api.model
|
||||
def get_studio_module(self):
|
||||
""" Returns the Studio module gathering all customizations done in
|
||||
Studio (freshly created apps and customizations of existing apps).
|
||||
Creates that module if it doesn't exist yet.
|
||||
"""
|
||||
studio_module = self.search([('name', '=', 'studio_customization')])
|
||||
if not studio_module:
|
||||
studio_module = self.create({
|
||||
'name': 'studio_customization',
|
||||
'application': False,
|
||||
'category_id': self.env.ref('base.module_category_customizations_studio').id,
|
||||
'shortdesc': 'Studio customizations',
|
||||
'description': """This module has been generated by Odoo Studio.
|
||||
It contains the apps created with Studio and the customizations of existing apps.""",
|
||||
'state': 'installed',
|
||||
'imported': True,
|
||||
'author': self.env.company.name,
|
||||
'icon': '/base/static/description/icon.png',
|
||||
'license': 'OPL-1',
|
||||
'dependencies_id': [(0, 0, {'name': 'web_studio'})],
|
||||
})
|
||||
return studio_module
|
||||
61
web_studio/models/ir_qweb.py
Normal file
61
web_studio/models/ir_qweb.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
from lxml import etree
|
||||
from textwrap import dedent
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools.json import scriptsafe
|
||||
from odoo.addons.base.models.ir_qweb import indent_code
|
||||
|
||||
|
||||
class IrQWeb(models.AbstractModel):
|
||||
"""
|
||||
allows to render reports with full branding on every node, including the context available
|
||||
to evaluate every node. The context is composed of all the variables available at this point
|
||||
in the report, and their type.
|
||||
"""
|
||||
_inherit = 'ir.qweb'
|
||||
|
||||
def _get_template(self, template):
|
||||
element, document, ref = super()._get_template(template)
|
||||
if self.env.context.get('full_branding'):
|
||||
if not isinstance(ref, int):
|
||||
raise ValueError("Template '%s' undefined" % template)
|
||||
|
||||
root = element.getroottree()
|
||||
basepath = len('/'.join(root.getpath(root.xpath('//*[@t-name]')[0]).split('/')[0:-1]))
|
||||
for node in element.iter(tag=etree.Element):
|
||||
node.set('data-oe-id', str(ref))
|
||||
node.set('data-oe-xpath', root.getpath(node)[basepath:])
|
||||
return (element, document, ref)
|
||||
|
||||
def _get_template_cache_keys(self):
|
||||
return super()._get_template_cache_keys() + ['full_branding']
|
||||
|
||||
def _prepare_environment(self, values):
|
||||
values['json'] = scriptsafe
|
||||
return super()._prepare_environment(values)
|
||||
|
||||
def _is_static_node(self, el, options):
|
||||
return not options.get('full_branding') and super()._is_static_node(el, options)
|
||||
|
||||
def _compile_directive_att(self, el, options, level):
|
||||
code = super()._compile_directive_att(el, options, level)
|
||||
|
||||
if options.get('full_branding'):
|
||||
code.append(indent_code("""
|
||||
attrs['data-oe-context'] = values['json'].dumps({
|
||||
key: values[key].__class__.__name__
|
||||
for key in values.keys()
|
||||
if key
|
||||
and key != 'true'
|
||||
and key != 'false'
|
||||
and not key.startswith('_')
|
||||
and ('_' not in key or key.rsplit('_', 1)[0] not in values or key.rsplit('_', 1)[1] not in ['even', 'first', 'index', 'last', 'odd', 'parity', 'size', 'value'])
|
||||
and (values[key].__class__.__name__ not in ['LocalProxy', 'function', 'method', 'Environment', 'module', 'type'])
|
||||
})
|
||||
""", level))
|
||||
|
||||
return code
|
||||
10
web_studio/models/ir_rule.py
Normal file
10
web_studio/models/ir_rule.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrRule(models.Model):
|
||||
_name = 'ir.rule'
|
||||
_description = 'Rule'
|
||||
_inherit = ['studio.mixin', 'ir.rule']
|
||||
93
web_studio/models/ir_ui_menu.py
Normal file
93
web_studio/models/ir_ui_menu.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.http import request
|
||||
|
||||
class IrUiMenu(models.Model):
|
||||
_name = 'ir.ui.menu'
|
||||
_description = 'Menu'
|
||||
_inherit = ['studio.mixin', 'ir.ui.menu']
|
||||
|
||||
is_studio_configuration = fields.Boolean(
|
||||
string='Studio Configuration Menu',
|
||||
help='Indicates that this menu was created by Studio to hold configuration sub-menus',
|
||||
readonly=True)
|
||||
|
||||
def write(self, vals):
|
||||
""" When renaming a menu will rename the windows action.
|
||||
"""
|
||||
for menu in self:
|
||||
if menu._context.get('studio') and 'name' in vals and menu.action:
|
||||
menu.action.name = vals['name']
|
||||
return super().write(vals)
|
||||
|
||||
@api.model
|
||||
def load_menus(self, debug):
|
||||
menus = super(IrUiMenu, self).load_menus(debug)
|
||||
cids = request and request.httprequest.cookies.get('cids')
|
||||
if cids:
|
||||
cids = [int(cid) for cid in cids.split(',')]
|
||||
company = self.env['res.company'].browse(cids[0]) \
|
||||
if cids and all([cid in self.env.user.company_ids.ids for cid in cids]) \
|
||||
else self.env.user.company_id
|
||||
menus['root']['backgroundImage'] = bool(company.background_image)
|
||||
return menus
|
||||
|
||||
@api.model
|
||||
def customize(self, to_move, to_delete):
|
||||
""" Apply customizations on menus. The deleted elements will no longer be active.
|
||||
When moving a menu, we needed to resequence it. Note that this customization will
|
||||
not be kept when upgrading the module (we don't put the ir.model.data in noupdate)
|
||||
|
||||
:param to_move: a dict of modifications with menu ids as keys
|
||||
ex: {10: {'parent_id': 1, 'sequence': 0}, 11: {'sequence': 1}}
|
||||
:param to_delete: a list of ids
|
||||
"""
|
||||
|
||||
for menu in to_move:
|
||||
menu_id = self.browse(int(menu))
|
||||
if 'parent_menu_id' in to_move[menu]:
|
||||
menu_id.parent_id = to_move[menu]['parent_menu_id']
|
||||
if 'sequence' in to_move[menu]:
|
||||
menu_id.sequence = to_move[menu]['sequence']
|
||||
|
||||
self.browse(to_delete).write({'active': False})
|
||||
|
||||
return True
|
||||
|
||||
def _get_studio_configuration_menu(self):
|
||||
"""
|
||||
Get (or create) a configuration menu that will hold some Studio models.
|
||||
|
||||
Creating a model through Studio can create secondary models, such as tags
|
||||
or stages. These models need their own menu+action, which should be stored
|
||||
under a config menu (child of the app root menu). If this is a Studio app,
|
||||
find or create the Configuration menu; if the app is not a Studio app, find or
|
||||
create the 'Custom Configuration' menu, to avoid confusion with a potential
|
||||
'Configuration' menu which could already be present.
|
||||
"""
|
||||
self.ensure_one()
|
||||
root_id = int(self.parent_path.split('/')[0])
|
||||
root_xmlids = self.env['ir.model.data'].search_read(
|
||||
domain=[('model', '=', 'ir.ui.menu'), ('res_id', '=', root_id)],
|
||||
fields=['module', 'name', 'studio']
|
||||
)
|
||||
# look for a studio config menu in the submenus
|
||||
parent_path = '%s/' % root_id
|
||||
new_context = dict(self._context)
|
||||
new_context.update({'ir.ui.menu.full_list': True}) # allows to create a menu without action
|
||||
config_menu = self.with_context(new_context).search([
|
||||
('parent_path', 'like', parent_path), ('is_studio_configuration', '=', True)
|
||||
])
|
||||
if not config_menu:
|
||||
is_studio_app = root_xmlids and any(map(lambda xmlid: xmlid['studio'], root_xmlids))
|
||||
menu_name = _('Configuration') if is_studio_app else _('Custom Configuration')
|
||||
config_menu = self.create({
|
||||
'name': menu_name,
|
||||
'is_studio_configuration': True,
|
||||
'parent_id': root_id,
|
||||
'sequence': 1000,
|
||||
})
|
||||
return config_menu
|
||||
1195
web_studio/models/ir_ui_view.py
Normal file
1195
web_studio/models/ir_ui_view.py
Normal file
File diff suppressed because it is too large
Load Diff
30
web_studio/models/mail_activity.py
Normal file
30
web_studio/models/mail_activity.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from odoo import models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MailActivity(models.Model):
|
||||
_inherit = "mail.activity"
|
||||
|
||||
def _action_done(self, feedback=False, attachment_ids=False):
|
||||
approval_activities = self.filtered(lambda a: a.activity_category == 'grant_approval')
|
||||
if approval_activities:
|
||||
ApprovalRequestSudo = self.env["studio.approval.request"].sudo()
|
||||
approval_requests = ApprovalRequestSudo.search([("mail_activity_id", "in", approval_activities.ids)])
|
||||
for activity in approval_activities:
|
||||
res_id = activity.res_id
|
||||
request = approval_requests.filtered(lambda r: r.mail_activity_id == activity)
|
||||
if not request:
|
||||
continue
|
||||
try:
|
||||
request.rule_id.with_context(
|
||||
prevent_approval_request_unlink=True
|
||||
).set_approval(res_id, True)
|
||||
except UserError:
|
||||
# the rule has already been rejected/approved or the user does not enough enough rights (or has
|
||||
# already approved exclusive rules) and is trying to "mark ad done" for another user
|
||||
# this should not prevent the user from marking this as done and should not modify any
|
||||
# approval entry
|
||||
# this means that if another user marks this as done and they have "all the rights" necessary
|
||||
# to approve the action, then their approval will be accepted (under their own name)
|
||||
pass
|
||||
return super()._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||
7
web_studio/models/mail_activity_type.py
Normal file
7
web_studio/models/mail_activity_type.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class MailActivityType(models.Model):
|
||||
_inherit = "mail.activity.type"
|
||||
|
||||
category = fields.Selection(selection_add=[('grant_approval', 'Grant Approval')], ondelete={'grant_approval': 'set default'})
|
||||
10
web_studio/models/mail_template.py
Normal file
10
web_studio/models/mail_template.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailTemplate(models.Model):
|
||||
_name = 'mail.template'
|
||||
_description = 'Email Templates'
|
||||
_inherit = ['studio.mixin', 'mail.template']
|
||||
29
web_studio/models/mail_thread.py
Normal file
29
web_studio/models/mail_thread.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
""" Returns suggested recipients for ids. Those are a list of
|
||||
tuple (partner_id, partner_name, reason), to be managed by Chatter.
|
||||
|
||||
This Studio override adds the field 'x_studio_partner_id' in the auto-suggested
|
||||
list."""
|
||||
result = super(MailThread, self)._message_get_suggested_recipients()
|
||||
# TODO: also support x_studio_user_id?
|
||||
field = self._fields.get('x_studio_partner_id')
|
||||
if field and field.type == 'many2one' and field.comodel_name == 'res.partner':
|
||||
for obj in self:
|
||||
if not obj.x_studio_partner_id:
|
||||
continue
|
||||
obj._message_add_suggested_recipient(result, partner=obj.x_studio_partner_id, reason=self._fields['x_studio_partner_id'].string)
|
||||
return result
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
"""Include partner field set automatically by studio as an SMS recipient."""
|
||||
fields = super()._sms_get_partner_fields()
|
||||
field = self._fields.get('x_studio_partner_id')
|
||||
if field and field.type == 'many2one' and field.comodel_name == 'res.partner':
|
||||
fields.append('x_studio_partner_id')
|
||||
return fields
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user