合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
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']
|
||||
Reference in New Issue
Block a user