去除多余企业版模块

This commit is contained in:
qihao.gong@jikimo.com
2023-04-23 10:15:20 +08:00
parent 9b05b87cb4
commit d74a5ef835
329 changed files with 0 additions and 272362 deletions

View File

@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import wizard

View File

@@ -1,156 +0,0 @@
# -*- 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',
],
}
}

View File

@@ -1,6 +0,0 @@
# -*- 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

View File

@@ -1,373 +0,0 @@
# -*- 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))

View File

@@ -1,19 +0,0 @@
# -*- 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,316 +0,0 @@
# -*- 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),
}]

View File

@@ -1,12 +0,0 @@
<?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>

View File

@@ -1,7 +0,0 @@
<?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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
# 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"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
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

View File

@@ -1,9 +0,0 @@
# -*- 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']

View File

@@ -1,14 +0,0 @@
# -*- 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']

View File

@@ -1,66 +0,0 @@
# -*- 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

View File

@@ -1,9 +0,0 @@
# -*- 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']

View File

@@ -1,9 +0,0 @@
# -*- 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']

View File

@@ -1,9 +0,0 @@
# -*- 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']

View File

@@ -1,643 +0,0 @@
# -*- 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']

View File

@@ -1,44 +0,0 @@
# -*- 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)

View File

@@ -1,32 +0,0 @@
# -*- 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

View File

@@ -1,61 +0,0 @@
# -*- 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

View File

@@ -1,10 +0,0 @@
# -*- 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']

View File

@@ -1,93 +0,0 @@
# -*- 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
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)

View File

@@ -1,7 +0,0 @@
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'})

View File

@@ -1,10 +0,0 @@
# -*- 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']

View File

@@ -1,29 +0,0 @@
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