Files
tryton/trytond/model/modelview.py
T

841 lines
33 KiB
Python
Raw Normal View History

2014-12-02 12:54:01 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2014-10-29 20:38:17 +01:00
import collections
2021-12-11 17:58:45 +01:00
from functools import wraps
2021-12-11 17:58:45 +01:00
from lxml import etree
from trytond.cache import Cache
2018-12-29 14:19:08 +01:00
from trytond.exceptions import UserError
from trytond.i18n import gettext
2011-07-03 13:20:58 +02:00
from trytond.pool import Pool
2022-01-23 13:32:59 +01:00
from trytond.pyson import PYSONEncoder
2012-09-24 22:16:53 +02:00
from trytond.rpc import RPC
2022-01-23 13:32:59 +01:00
from trytond.tools import is_instance_method
2021-12-11 17:58:45 +01:00
from trytond.transaction import Transaction
2012-09-24 22:16:53 +02:00
2021-12-11 17:58:45 +01:00
from . import fields
2017-02-01 16:11:00 +01:00
from .fields import on_change_result
2021-12-11 17:58:45 +01:00
from .model import Model
2017-02-01 16:11:00 +01:00
2012-09-24 22:16:53 +02:00
__all__ = ['ModelView']
2012-03-25 00:15:17 +01:00
2009-01-29 20:34:26 +01:00
2018-12-29 14:19:08 +01:00
class AccessButtonError(UserError):
pass
2014-10-29 20:38:17 +01:00
def on_change(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
assert result is None, func
return self
wrapper.on_change = True
return wrapper
2009-01-29 20:34:26 +01:00
class ModelView(Model):
"""
Define a model with views in Tryton.
"""
2019-09-07 23:45:08 +02:00
__slots__ = ()
2012-09-24 22:16:53 +02:00
_fields_view_get_cache = Cache('modelview.fields_view_get')
_view_toolbar_get_cache = Cache('modelview.view_toolbar_get')
2012-09-24 22:16:53 +02:00
@classmethod
def __setup__(cls):
super(ModelView, cls).__setup__()
2019-04-08 10:10:40 +02:00
cls.__rpc__['fields_view_get'] = RPC(cache=dict(days=1))
cls.__rpc__['view_toolbar_get'] = RPC(cache=dict(days=1))
2014-10-29 20:38:17 +01:00
cls.__rpc__['on_change'] = RPC(instantiate=0)
cls.__rpc__['on_change_with'] = RPC(instantiate=0)
2012-09-24 22:16:53 +02:00
cls._buttons = {}
2009-01-29 20:34:26 +01:00
fields_ = {}
2015-03-16 12:56:11 +01:00
for name in dir(cls):
if name.startswith('__'):
continue
2015-03-16 12:56:11 +01:00
attr = getattr(cls, name)
if isinstance(attr, fields.Field):
fields_[name] = attr
@classmethod
def __post_setup__(cls):
super(ModelView, cls).__post_setup__()
methods = {
'_done': set(),
'depends': collections.defaultdict(set),
'depend_methods': collections.defaultdict(set),
'change': collections.defaultdict(set),
}
cls.__change_buttons = methods['change']
def set_methods(name):
if name in methods['_done']:
return
methods['_done'].add(name)
for parent_cls in cls.__mro__:
parent_meth = getattr(parent_cls, name, None)
if not parent_meth:
continue
for attr in ['depends', 'depend_methods', 'change']:
if isinstance(parent_meth, property):
parent_value = getattr(parent_meth.fget, attr, set())
parent_value |= getattr(parent_meth.fset, attr, set())
else:
parent_value = getattr(parent_meth, attr, set())
if parent_value:
methods[attr][name] |= parent_value
def setup_field(field_name, field, attribute):
if attribute == 'selection_change_with':
if isinstance(
2018-06-26 19:11:19 +02:00
getattr(field, 'selection', None), str):
function_name = field.selection
else:
return
else:
function_name = '%s_%s' % (attribute, field_name)
function = getattr(cls, function_name, None)
if not function:
return
set_methods(function_name)
setattr(field, attribute, methods['depends'][function_name])
meth_names = list(methods['depend_methods'][function_name])
meth_done = set()
while meth_names:
meth_name = meth_names.pop()
method = getattr(cls, meth_name)
assert callable(method) or isinstance(method, property), \
"%s.%s not callable or property" % (cls, meth_name)
set_methods(meth_name)
setattr(field, attribute,
getattr(field, attribute) | methods['depends'][meth_name])
meth_names += list(
methods['depend_methods'][meth_name] - meth_done)
meth_done.add(meth_name)
if (attribute == 'on_change'
and not getattr(function, 'on_change', None)):
# Decorate on_change to always return self
setattr(cls, function_name, on_change(function))
for name, field in cls._fields.items():
for attribute in [
'on_change',
'on_change_with',
'autocomplete',
'selection_change_with',
]:
setup_field(name, field, attribute)
2015-03-16 12:56:11 +01:00
2014-10-29 20:38:17 +01:00
# Update __rpc__
2018-06-26 19:11:19 +02:00
for field_name, field in cls._fields.items():
2017-02-01 16:11:00 +01:00
field.set_rpc(cls)
2014-10-29 20:38:17 +01:00
2015-03-16 12:56:11 +01:00
for button in cls._buttons:
if not is_instance_method(cls, button):
2015-03-16 12:56:11 +01:00
cls.__rpc__.setdefault(button,
RPC(readonly=False, instantiate=0))
else:
cls.__rpc__.setdefault(button,
RPC(instantiate=0, result=on_change_result))
meth_names = set()
meth_done = set()
for parent_cls in cls.__mro__:
parent_meth = getattr(parent_cls, button, None)
if not parent_meth:
continue
cls.__change_buttons[button] |= getattr(
parent_meth, 'change', set())
meth_names |= getattr(parent_meth, 'change_methods', set())
while meth_names:
meth_name = meth_names.pop()
method = getattr(cls, meth_name)
assert callable(method) or isinstance(method, property), \
"%s.%s not callable or property" % (cls, meth_name)
set_methods(meth_name)
cls.__change_buttons[button] |= methods['depends'][meth_name]
meth_names |= (
methods['depend_methods'][meth_name] - meth_done)
meth_done.add(meth_name)
2012-09-24 22:16:53 +02:00
@classmethod
def fields_view_get(cls, view_id=None, view_type='form', level=None):
2009-01-29 20:34:26 +01:00
'''
2009-09-13 15:37:11 +02:00
Return a view definition.
2012-09-24 22:16:53 +02:00
If view_id is None the first one will be used of view_type.
The definition is a dictionary with keys:
2009-03-01 13:37:31 +01:00
- model: the model name
- type: the type of the view
- view_id: the id of the view
2009-03-01 13:37:31 +01:00
- arch: the xml description of the view
2009-09-13 15:37:11 +02:00
- fields: a dictionary with the definition of each field in the view
- field_childs: the name of the childs field for tree
2009-01-29 20:34:26 +01:00
'''
key = (cls.__name__, view_id, view_type, level)
2012-09-24 22:16:53 +02:00
result = cls._fields_view_get_cache.get(key)
if result:
return result
result = {'model': cls.__name__}
2011-07-03 13:20:58 +02:00
pool = Pool()
2012-09-26 19:38:20 +02:00
View = pool.get('ir.ui.view')
2009-01-29 20:34:26 +01:00
2015-10-15 10:44:30 +02:00
if view_id:
view = View(view_id)
else:
domain = [
('model', '=', cls.__name__),
['OR',
('inherit', '=', None),
('inherit.model', '!=', cls.__name__),
],
]
views = View.search(domain)
2018-06-26 19:11:19 +02:00
views = [v for v in views if v.rng_type == view_type]
2015-10-15 10:44:30 +02:00
if views:
view = views[0]
2022-01-23 13:32:59 +01:00
view_id = view.id
else:
view = None
2009-01-29 20:34:26 +01:00
# if a view was found
2012-09-26 19:38:20 +02:00
if view:
2022-01-23 13:32:59 +01:00
result = view.view_get(model=cls.__name__)
2009-01-29 20:34:26 +01:00
# otherwise, build some kind of default view
else:
if view_type == 'form':
2012-09-24 22:16:53 +02:00
res = cls.fields_get()
xml = '''<?xml version="1.0"?>''' \
'''<form col="4">'''
2009-01-29 20:34:26 +01:00
for i in res:
if i in ('create_uid', 'create_date',
'write_uid', 'write_date', 'id', 'rec_name'):
2009-01-29 20:34:26 +01:00
continue
if res[i]['type'] not in ('one2many', 'many2many'):
xml += '<label name="%s"/>' % (i,)
xml += '<field name="%s"/>' % (i,)
if res[i]['type'] == 'text':
xml += "<newline/>"
2012-05-14 15:27:06 +02:00
else:
xml += '<field name="%s" colspan="4"/>' % (i,)
2020-12-05 11:43:59 +01:00
if cls._buttons:
xml += '<group id="buttons" col="-1" colspan="4">'
for button in cls._buttons:
xml += '<button name="%s"/>' % button
xml += '</group>'
2009-01-29 20:34:26 +01:00
xml += "</form>"
elif view_type == 'tree':
field = 'id'
2012-09-24 22:16:53 +02:00
if cls._rec_name in cls._fields:
field = cls._rec_name
xml = '''<?xml version="1.0"?>''' \
'''<tree><field name="%s"/></tree>''' \
% (field,)
2009-01-29 20:34:26 +01:00
else:
xml = ''
result['type'] = view_type
2022-01-23 13:32:59 +01:00
result['view_id'] = view_id
2009-01-29 20:34:26 +01:00
result['arch'] = xml
result['field_childs'] = None
2022-01-23 13:32:59 +01:00
result['model'] = cls.__name__
2009-01-29 20:34:26 +01:00
if level is None:
level = 1 if result['type'] == 'tree' else 0
2009-01-29 20:34:26 +01:00
# Update arch and compute fields from arch
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.fromstring(result['arch'], parser)
2022-02-03 23:13:28 +01:00
result['arch'], result['fields'] = cls.parse_view(
tree, result['type'], view_id=view_id,
field_children=result['field_childs'], level=level)
2009-01-29 20:34:26 +01:00
2012-09-24 22:16:53 +02:00
cls._fields_view_get_cache.set(key, result)
2009-01-29 20:34:26 +01:00
return result
2012-09-24 22:16:53 +02:00
@classmethod
def view_toolbar_get(cls):
2011-07-20 18:34:11 +02:00
"""
2019-03-22 17:21:21 +01:00
Returns the model specific actions and exports.
2012-09-24 22:16:53 +02:00
A dictionary with keys:
2011-07-20 18:34:11 +02:00
- print: a list of available reports
- action: a list of available actions
- relate: a list of available relations
2019-03-22 17:21:21 +01:00
- exports: a list of available exports
2011-07-20 18:34:11 +02:00
"""
2019-03-22 17:21:21 +01:00
pool = Pool()
Action = pool.get('ir.action.keyword')
Export = pool.get('ir.export')
2020-09-17 17:18:02 +02:00
Email = pool.get('ir.email.template')
key = cls.__name__
2012-09-24 22:16:53 +02:00
result = cls._view_toolbar_get_cache.get(key)
if result:
return result
prints = Action.get_keyword('form_print', (cls.__name__, -1))
actions = Action.get_keyword('form_action', (cls.__name__, -1))
relates = Action.get_keyword('form_relate', (cls.__name__, -1))
2019-03-22 17:21:21 +01:00
exports = Export.search_read(
[('resource', '=', cls.__name__)],
fields_names=['name', 'export_fields.name'])
2020-09-17 17:18:02 +02:00
emails = Email.search_read(
[('model.model', '=', cls.__name__)],
fields_names=['name'])
2012-09-24 22:16:53 +02:00
result = {
2011-07-20 18:34:11 +02:00
'print': prints,
'action': actions,
'relate': relates,
2019-03-22 17:21:21 +01:00
'exports': exports,
2020-09-17 17:18:02 +02:00
'emails': emails,
2012-09-24 22:16:53 +02:00
}
cls._view_toolbar_get_cache.set(key, result)
return result
2011-07-20 18:34:11 +02:00
2015-03-09 19:40:41 +01:00
@classmethod
def view_attributes(cls):
'Return a list of xpath, attribute name and value'
return []
2012-09-24 22:16:53 +02:00
@classmethod
2021-02-14 15:56:18 +01:00
def parse_view(
2022-02-03 23:13:28 +01:00
cls, tree, type, view_id=None, field_children=None, level=0,
view_depends=None):
2021-02-14 15:56:18 +01:00
"""
Return sanitized XML and the corresponding fields definition
"""
2011-07-03 13:20:58 +02:00
pool = Pool()
2012-09-24 22:16:53 +02:00
ModelAccess = pool.get('ir.model.access')
FieldAccess = pool.get('ir.model.field.access')
2011-01-22 23:56:13 +01:00
2015-03-09 19:40:41 +01:00
encoder = PYSONEncoder()
2021-02-14 15:56:18 +01:00
if view_depends is None:
view_depends = []
else:
view_depends = view_depends.copy()
2022-02-03 23:13:28 +01:00
with Transaction().set_context(view_id=view_id):
for xpath, attribute, value, *extra in cls.view_attributes():
depends = []
if extra:
depends, = extra
nodes = tree.xpath(xpath)
for element in nodes:
element.set(attribute, encoder.encode(value))
if nodes and depends:
view_depends.extend(depends)
2015-03-09 19:40:41 +01:00
2009-01-29 20:34:26 +01:00
fields_width = {}
2022-02-03 23:13:28 +01:00
fields_optional = {}
2009-01-29 20:34:26 +01:00
tree_root = tree.getroottree().getroot()
2011-01-22 23:56:13 +01:00
# Find field without read access
2012-09-24 22:16:53 +02:00
fread_accesses = FieldAccess.check(cls.__name__,
2018-06-26 19:11:19 +02:00
list(cls._fields.keys()), 'read', access=True)
fields_to_remove = set(
x for x, y in fread_accesses.items() if not y)
# Find relation field without read access
2018-06-26 19:11:19 +02:00
for name, field in cls._fields.items():
if not ModelAccess.check_relation(cls.__name__, name, mode='read'):
fields_to_remove.add(name)
checked = set()
while checked < fields_to_remove:
to_check = fields_to_remove - checked
for name, field in cls._fields.items():
for field_to_remove in to_check:
if field_to_remove in field.depends:
fields_to_remove.add(name)
checked |= to_check
2011-01-22 23:56:13 +01:00
buttons_to_remove = set()
for name, definition in cls._buttons.items():
if fields_to_remove & set(definition.get('depends', [])):
buttons_to_remove.add(name)
field_xpath = ('//field[@name="%(name)s"]'
'| //label[@name="%(name)s"] | //page[@name="%(name)s"]'
'| //group[@name="%(name)s"] | //separator[@name="%(name)s"]')
button_xpath = '//button[@name="%(name)s"]'
# Remove field and button without read acces
for xpath, names in (
(field_xpath, fields_to_remove),
(button_xpath, buttons_to_remove),
):
for name in names:
path = xpath % {'name': name}
for i, element in enumerate(tree.xpath(path)):
if type == 'tree' or element.tag == 'page':
parent = element.getparent()
parent.remove(element)
elif type == 'form':
element.tag = 'label'
colspan = element.attrib.get('colspan')
element.attrib.clear()
element.attrib['id'] = 'hidden %s-%s' % (name, i)
if colspan is not None:
element.attrib['colspan'] = colspan
2011-01-22 23:56:13 +01:00
2017-06-09 16:07:03 +02:00
# Remove empty pages
if type == 'form':
for page in tree.xpath('//page[not(descendant::*)]'):
page.getparent().remove(page)
2022-02-03 23:13:28 +01:00
if type == 'tree':
user = Transaction().user
if Transaction().context.get('view_tree_width'):
ViewTreeWidth = pool.get('ir.ui.view_tree_width')
viewtreewidths = ViewTreeWidth.search([
('model', '=', cls.__name__),
('user', '=', user),
])
for viewtreewidth in viewtreewidths:
if viewtreewidth.width > 0:
fields_width[viewtreewidth.field] = viewtreewidth.width
if view_id:
ViewTreeOptional = pool.get('ir.ui.view_tree_optional')
viewtreeoptionals = ViewTreeOptional.search([
('view_id', '=', view_id),
('user', '=', user),
])
fields_optional = {o.field: o.value for o in viewtreeoptionals}
2009-01-29 20:34:26 +01:00
2021-02-14 15:56:18 +01:00
fields_def = cls.__parse_fields(
2022-02-03 23:13:28 +01:00
tree_root, type,
fields_width=fields_width,
fields_optional=fields_optional)
2009-01-29 20:34:26 +01:00
if hasattr(cls, 'active'):
fields_def.setdefault('active', {'name': 'active'})
2011-04-03 15:02:28 +02:00
if field_children:
fields_def.setdefault(field_children, {'name': field_children})
2012-09-24 22:16:53 +02:00
if field_children in cls._fields:
field = cls._fields[field_children]
2014-11-24 16:36:58 +00:00
if hasattr(field, 'field'):
fields_def.setdefault(field.field, {'name': field.field})
2011-04-03 15:02:28 +02:00
for depend in view_depends:
if depend not in fields_to_remove:
fields_def.setdefault(depend, {'name': depend})
field_names = list(fields_def.keys())
while field_names:
field_name = field_names.pop()
2012-09-24 22:16:53 +02:00
if field_name in cls._fields:
field = cls._fields[field_name]
2009-01-29 20:34:26 +01:00
else:
continue
for depend in field.depends:
if depend not in fields_def:
fields_def[depend] = {'name': depend}
field_names.append(depend)
2020-03-30 09:33:15 +02:00
2016-02-07 16:46:16 +01:00
arch = etree.tostring(
tree, encoding='utf-8', pretty_print=False).decode('utf-8')
# Do not call fields_def without fields as it returns all fields
if fields_def:
fields2 = cls.fields_get(list(fields_def.keys()), level=level)
else:
fields2 = {}
2009-01-29 20:34:26 +01:00
for field in fields_def:
if field in fields2:
fields2[field].update(fields_def[field])
return arch, fields2
2012-09-24 22:16:53 +02:00
@classmethod
2021-02-14 15:56:18 +01:00
def __parse_fields(
2022-02-03 23:13:28 +01:00
cls, element, type, fields_width=None, fields_optional=None,
_fields_attrs=None):
2011-07-03 13:20:58 +02:00
pool = Pool()
2012-09-24 22:16:53 +02:00
Translation = pool.get('ir.translation')
ModelData = pool.get('ir.model.data')
ModelAccess = pool.get('ir.model.access')
2012-09-24 22:16:53 +02:00
Button = pool.get('ir.model.button')
User = pool.get('res.user')
2020-04-13 12:21:55 +02:00
ActionWindow = pool.get('ir.action.act_window')
2009-01-29 20:34:26 +01:00
if fields_width is None:
fields_width = {}
2022-02-03 23:13:28 +01:00
if fields_optional is None:
fields_optional = {}
2020-09-18 16:27:29 +02:00
if _fields_attrs is None:
fields_attrs = {}
else:
2020-09-18 16:27:29 +02:00
fields_attrs = _fields_attrs
2009-01-29 20:34:26 +01:00
2015-11-10 12:55:06 +01:00
def set_view_ids(element):
view_ids = []
if element.get('view_ids'):
for view_id in element.get('view_ids').split(','):
2009-01-29 20:34:26 +01:00
try:
2015-11-10 12:55:06 +01:00
view_ids.append(int(view_id))
except ValueError:
view_ids.append(ModelData.get_id(*view_id.split('.')))
element.attrib['view_ids'] = ','.join(map(str, view_ids))
2015-11-10 12:55:06 +01:00
return view_ids
def get_relation(field):
if hasattr(field, 'model_name'):
return field.model_name
elif hasattr(field, 'get_target'):
return field.get_target().__name__
def get_views(relation, widget, view_ids, mode):
2015-11-10 12:55:06 +01:00
Relation = pool.get(relation)
views = {}
if widget in {'one2many', 'many2many'}:
2015-11-10 12:55:06 +01:00
# Prefetch only the first view to prevent infinite loop
if view_ids:
for view_id in view_ids:
view = Relation.fields_view_get(view_id=view_id)
views[str(view_id)] = view
break
else:
for view_type in mode:
views[view_type] = (
Relation.fields_view_get(view_type=view_type))
break
return views
2020-10-09 19:09:06 +02:00
for attr in ('name', 'icon', 'symbol'):
2015-11-10 12:55:06 +01:00
if not element.get(attr):
continue
fields_attrs.setdefault(element.get(attr), {})
if element.tag == 'field' and type in ['tree', 'form']:
for attr in ('name', 'icon'):
fname = element.get(attr)
if not fname:
continue
view_ids = set_view_ids(element)
if type != 'form':
continue
field = cls._fields[fname]
relation = get_relation(field)
if not relation:
continue
mode = (
element.attrib.pop('mode', None) or 'tree,form').split(',')
widget = element.attrib.get('widget', field._type)
views = get_views(relation, widget, view_ids, mode)
2015-11-10 12:55:06 +01:00
element.attrib['mode'] = ','.join(mode)
fields_attrs[fname].setdefault('views', {}).update(views)
2022-02-03 23:13:28 +01:00
if type == 'tree':
if element.get('name') in fields_width:
element.set(
'width', str(fields_width[element.get('name')]))
if element.get('optional'):
if element.get('name') in fields_optional:
optional = str(int(
fields_optional[element.get('name')]))
element.set('optional', optional)
2009-01-29 20:34:26 +01:00
encoder = PYSONEncoder()
2012-03-25 00:15:17 +01:00
if element.tag == 'button':
2012-09-10 15:26:34 +02:00
button_name = element.attrib['name']
2012-09-24 22:16:53 +02:00
if button_name in cls._buttons:
states = cls._buttons[button_name]
2012-09-10 15:26:34 +02:00
else:
states = {}
2012-09-24 22:16:53 +02:00
groups = set(User.get_groups())
button_attr = Button.get_view_attributes(
cls.__name__, button_name)
2018-06-26 19:11:19 +02:00
for attr, value in button_attr.items():
if not element.get(attr):
element.set(attr, value or '')
2012-09-24 22:16:53 +02:00
button_groups = Button.get_groups(cls.__name__, button_name)
if ((button_groups and not groups & button_groups)
or (not button_groups
and not ModelAccess.check(
cls.__name__, 'write', raise_exception=False))):
2012-09-10 15:26:34 +02:00
states = states.copy()
states['readonly'] = True
element.set('states', encoder.encode(states))
2012-03-25 00:15:17 +01:00
2016-10-26 09:44:09 +02:00
button_rules = Button.get_rules(cls.__name__, button_name)
if button_rules:
element.set('rule', '1')
2015-03-16 12:56:11 +01:00
change = cls.__change_buttons[button_name]
if change:
change = list(change)
# Add id to change if the button is not cached
# Not having the id increase the efficiency of the cache
if cls.__rpc__[button_name].cache:
change.append('id')
element.set('change', encoder.encode(change))
if not is_instance_method(cls, button_name):
2015-03-16 12:56:11 +01:00
element.set('type', 'class')
else:
element.set('type', 'instance')
2018-02-19 11:35:37 +01:00
for depend in states.get('depends', []):
fields_attrs.setdefault(depend, {})
2020-04-13 12:21:55 +02:00
if element.tag == 'link':
link_name = element.attrib['name']
action_id = ModelData.get_id(*link_name.split('.'))
try:
2021-04-10 23:44:39 +02:00
with Transaction().set_context(_check_access=True):
action, = ActionWindow.search([('id', '=', action_id)])
except ValueError:
action = None
if (not action
or not action.res_model
2020-04-13 12:21:55 +02:00
or not ModelAccess.check(
action.res_model, 'read', raise_exception=False)):
element.tag = 'label'
colspan = element.attrib.get('colspan')
link_name = element.attrib['name']
2020-04-13 12:21:55 +02:00
element.attrib.clear()
element.attrib['id'] = link_name
2020-04-13 12:21:55 +02:00
if colspan is not None:
element.attrib['colspan'] = colspan
else:
element.attrib['id'] = str(action.action.id)
2009-01-29 20:34:26 +01:00
# translate view
2016-09-10 23:25:03 +02:00
if Transaction().language != 'en':
2009-01-29 20:34:26 +01:00
for attr in ('string', 'sum', 'confirm', 'help'):
if element.get(attr):
2012-09-24 22:16:53 +02:00
trans = Translation.get_source(cls.__name__, 'view',
2010-08-19 22:01:12 +02:00
Transaction().language, element.get(attr))
2009-01-29 20:34:26 +01:00
if trans:
element.set(attr, trans)
if element.tag == 'tree' and element.get('sequence'):
fields_attrs.setdefault(element.get('sequence'), {})
2013-06-09 20:14:14 +02:00
if element.tag == 'calendar':
for attr in ['dtstart', 'dtend', 'color', 'background_color']:
2013-06-09 20:14:14 +02:00
if element.get(attr):
fields_attrs.setdefault(element.get(attr), {})
2015-11-10 12:55:06 +01:00
for field in element:
2021-02-14 15:56:18 +01:00
fields_attrs = cls.__parse_fields(
2022-02-03 23:13:28 +01:00
field, type,
fields_width=fields_width,
fields_optional=fields_optional,
2021-02-14 15:56:18 +01:00
_fields_attrs=fields_attrs)
2009-01-29 20:34:26 +01:00
return fields_attrs
2012-03-25 00:15:17 +01:00
@staticmethod
def button(func):
@wraps(func)
2016-10-26 09:44:09 +02:00
def wrapper(cls, records, *args, **kwargs):
2021-02-14 15:47:00 +01:00
from .modelstorage import ModelStorage
2012-03-25 00:15:17 +01:00
pool = Pool()
2014-09-17 21:43:53 +02:00
ModelAccess = pool.get('ir.model.access')
2012-09-24 22:16:53 +02:00
Button = pool.get('ir.model.button')
2016-10-26 09:44:09 +02:00
ButtonClick = pool.get('ir.model.button.click')
2012-09-24 22:16:53 +02:00
User = pool.get('res.user')
2012-03-25 00:15:17 +01:00
2016-10-26 09:44:09 +02:00
transaction = Transaction()
check_access = transaction.context.get('_check_access')
assert len(records) == len(set(records)), "Duplicate records"
2016-10-26 09:44:09 +02:00
if (transaction.user != 0) and check_access:
2014-09-17 21:43:53 +02:00
ModelAccess.check(cls.__name__, 'read')
2021-02-14 15:47:00 +01:00
if issubclass(cls, ModelStorage):
# Check record rule access
cls.read([r.id for r in records
if r.id is not None and r.id >= 0],
['id'])
2012-09-24 22:16:53 +02:00
groups = set(User.get_groups())
button_groups = Button.get_groups(cls.__name__,
2012-04-01 23:08:15 +02:00
func.__name__)
if button_groups:
if not groups & button_groups:
2018-12-29 14:19:08 +01:00
raise AccessButtonError(
gettext('ir.msg_access_button_error',
button=func.__name__,
model=cls.__name__))
else:
ModelAccess.check(cls.__name__, 'write')
2016-10-26 09:44:09 +02:00
2014-09-17 21:43:53 +02:00
with Transaction().set_context(_check_access=False):
2016-10-26 09:44:09 +02:00
if (transaction.user != 0) and check_access:
button_rules = Button.get_rules(
cls.__name__, func.__name__)
if button_rules:
clicks = ButtonClick.register(
cls.__name__, func.__name__, records)
2018-09-01 17:06:28 +02:00
records = [r for r in records
if all(br.test(r, clicks.get(r.id, []))
2018-06-26 19:11:19 +02:00
for br in button_rules)]
2016-10-26 09:44:09 +02:00
# Reset click after filtering in case the button also has rules
names = Button.get_reset(cls.__name__, func.__name__)
if names:
ButtonClick.reset(cls.__name__, names, records)
return func(cls, records, *args, **kwargs)
2012-03-25 00:15:17 +01:00
return wrapper
2012-09-10 15:26:34 +02:00
@staticmethod
def button_action(action):
def decorator(func):
func = ModelView.button(func)
@wraps(func)
def wrapper(*args, **kwargs):
pool = Pool()
2012-09-24 22:16:53 +02:00
ModelData = pool.get('ir.model.data')
Action = pool.get('ir.action')
2012-09-10 15:26:34 +02:00
2019-12-07 16:03:36 +01:00
value = func(*args, **kwargs)
2012-09-10 15:26:34 +02:00
module, fs_id = action.split('.')
2012-09-24 22:16:53 +02:00
action_id = Action.get_action_id(
ModelData.get_id(module, fs_id))
2019-12-07 16:03:36 +01:00
if value:
action_value = Action(action_id).get_action_value()
action_value.update(value)
return action_value
else:
return action_id
2012-09-10 15:26:34 +02:00
return wrapper
return decorator
2014-10-29 20:38:17 +01:00
2015-03-16 12:56:11 +01:00
@staticmethod
def button_change(*fields, **kwargs):
methods = kwargs.pop('methods', None)
assert not kwargs
2015-03-16 12:56:11 +01:00
def decorator(func):
func = on_change(func)
func.change = set(fields)
if methods:
func.change_methods = set(methods)
2015-03-16 12:56:11 +01:00
return func
return decorator
2014-10-29 20:38:17 +01:00
def on_change(self, fieldnames):
for fieldname in sorted(fieldnames):
method = getattr(self, 'on_change_%s' % fieldname, None)
if method:
method()
# XXX remove backward compatibility
return [self._changed_values]
def on_change_with(self, fieldnames):
changes = {}
for fieldname in fieldnames:
method_name = 'on_change_with_%s' % fieldname
changes[fieldname] = getattr(self, method_name)()
return changes
@property
def _changed_values(self):
"""Return the values changed since the instantiation.
By default, the value of a field is its internal representation except:
- for Many2One and One2One field: the id.
- for Reference field: the string model,id
- for Many2Many: the list of ids
- for One2Many: a dictionary composed of three keys:
- add: a list of tuple, the first element is the index where
the new line is added, the second element is
`_default_values`
- update: a list of dictionary of `_changed_values` including
the `id`
- remove: a list of ids
"""
from .modelstorage import ModelStorage
changed = {}
2020-06-13 09:48:03 +02:00
init_values = self._init_values or self._record()
2014-10-29 20:38:17 +01:00
if not self._values:
return changed
init_record = self.__class__(self.id)
2020-06-13 09:48:03 +02:00
for fname, value in self._values._items():
2014-10-29 20:38:17 +01:00
field = self._fields[fname]
# Always test key presence in case value is None
if (fname in init_values
and value == init_values[fname]
2014-11-21 10:08:35 +01:00
and field._type != 'one2many'):
continue
2014-10-29 20:38:17 +01:00
if field._type in ('many2one', 'one2one', 'reference'):
if value:
if isinstance(value, ModelStorage):
2019-09-11 23:49:34 +02:00
changed['%s.' % fname] = {
'rec_name': value.rec_name,
}
if value.id is None:
# Don't consider temporary instance as a change
continue
2014-10-29 20:38:17 +01:00
if field._type == 'reference':
value = str(value)
else:
value = value.id
elif field._type in ['one2many', 'many2many']:
2014-10-29 20:38:17 +01:00
targets = value
if fname in init_values:
init_targets = init_values._get(fname)
else:
init_targets = getattr(init_record, fname, [])
2014-10-29 20:38:17 +01:00
value = collections.defaultdict(list)
previous = [t.id for t in init_targets if t.id]
2014-10-29 20:38:17 +01:00
for i, target in enumerate(targets):
if (field._type == 'one2many'
and field.field
and target._values):
2020-06-13 09:48:03 +02:00
t_values = target._values._copy()
# Don't look at reverse field
2020-06-13 09:48:03 +02:00
target._values._pop(field.field, None)
2014-10-29 20:38:17 +01:00
else:
t_values = None
try:
if target.id in previous:
previous.remove(target.id)
if isinstance(target, ModelView):
target_changed = target._changed_values
if target_changed:
target_changed['id'] = target.id
value['update'].append(target_changed)
else:
if isinstance(target, ModelView):
# Ensure initial values are returned because
# target was instantiated on server side.
target_init_values = target._init_values
target._init_values = None
try:
added_values = target._changed_values
finally:
target._init_values = target_init_values
else:
added_values = target._default_values
added_values['id'] = target.id
value['add'].append((i, added_values))
finally:
if t_values:
target._values = t_values
if previous:
to_delete, to_remove = [], []
deleted = removed = None
if self._deleted:
deleted = self._deleted[fname]
if self._removed:
removed = self._removed[fname]
for id_ in previous:
if deleted and id_ in deleted:
to_delete.append(id_)
elif removed and id_ in removed:
to_remove.append(id_)
elif field._type == 'one2many':
to_delete.append(id_)
else:
to_remove.append(id_)
if to_delete:
value['delete'] = to_delete
if to_remove:
value['remove'] = to_remove
2014-11-21 10:08:35 +01:00
if not value:
continue
2017-11-18 11:12:45 +01:00
value = dict(value)
2014-10-29 20:38:17 +01:00
changed[fname] = value
return changed