542 lines
22 KiB
Python
542 lines
22 KiB
Python
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
#this repository contains the full copyright notices and license terms.
|
|
from lxml import etree
|
|
from functools import wraps
|
|
from trytond.model import Model
|
|
from trytond.tools import safe_eval, ClassProperty
|
|
from trytond.pyson import PYSONEncoder, CONTEXT
|
|
from trytond.transaction import Transaction
|
|
from trytond.cache import Cache
|
|
from trytond.pool import Pool
|
|
from trytond.exceptions import UserError
|
|
from trytond.rpc import RPC
|
|
|
|
__all__ = ['ModelView']
|
|
|
|
|
|
def _find(tree, element):
|
|
if element.tag == 'xpath':
|
|
res = tree.xpath(element.get('expr'))
|
|
if res:
|
|
return res[0]
|
|
return None
|
|
|
|
|
|
def _inherit_apply(src, inherit):
|
|
tree_src = etree.fromstring(src)
|
|
tree_inherit = etree.fromstring(inherit)
|
|
root_inherit = tree_inherit.getroottree().getroot()
|
|
for element2 in root_inherit:
|
|
if element2.tag != 'xpath':
|
|
continue
|
|
element = _find(tree_src, element2)
|
|
if element is not None:
|
|
pos = element2.get('position', 'inside')
|
|
if pos == 'replace':
|
|
parent = element.getparent()
|
|
enext = element.getnext()
|
|
if enext is not None:
|
|
for child in element2:
|
|
index = parent.index(enext)
|
|
parent.insert(index, child)
|
|
else:
|
|
parent.extend(element2.getchildren())
|
|
parent.remove(element)
|
|
elif pos == 'replace_attributes':
|
|
child = element2.getchildren()[0]
|
|
for attr in child.attrib:
|
|
element.set(attr, child.get(attr))
|
|
elif pos == 'inside':
|
|
element.extend(element2.getchildren())
|
|
elif pos == 'after':
|
|
parent = element.getparent()
|
|
enext = element.getnext()
|
|
if enext is not None:
|
|
for child in element2:
|
|
index = parent.index(enext)
|
|
parent.insert(index, child)
|
|
else:
|
|
parent.extend(element2.getchildren())
|
|
elif pos == 'before':
|
|
parent = element.getparent()
|
|
for child in element2:
|
|
index = parent.index(element)
|
|
parent.insert(index, child)
|
|
else:
|
|
raise AttributeError('Unknown position ' \
|
|
'in inherited view %s!' % pos)
|
|
else:
|
|
raise AttributeError(
|
|
'Couldn\'t find tag (%s: %s) in parent view!' % \
|
|
(element2.tag, element2.get('expr')))
|
|
return etree.tostring(tree_src, encoding='utf-8')
|
|
|
|
|
|
class ModelView(Model):
|
|
"""
|
|
Define a model with views in Tryton.
|
|
"""
|
|
__modules_list = None # Cache for the modules list sorted by dependency
|
|
_fields_view_get_cache = Cache('modelview.fields_view_get')
|
|
_view_toolbar_get_cache = Cache('modelview.view_toolbar_get')
|
|
|
|
@staticmethod
|
|
def _reset_modules_list():
|
|
ModelView.__modules_list = None
|
|
|
|
@ClassProperty
|
|
@classmethod
|
|
def _modules_list(cls):
|
|
from trytond.modules import create_graph, get_module_list
|
|
if ModelView.__modules_list:
|
|
return ModelView.__modules_list
|
|
graph = create_graph(get_module_list())[0]
|
|
ModelView.__modules_list = [x.name for x in graph] + [None]
|
|
return ModelView.__modules_list
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ModelView, cls).__setup__()
|
|
cls.__rpc__['fields_view_get'] = RPC()
|
|
cls.__rpc__['view_toolbar_get'] = RPC()
|
|
cls._buttons = {}
|
|
|
|
@classmethod
|
|
def fields_view_get(cls, view_id=None, view_type='form'):
|
|
'''
|
|
Return a view definition.
|
|
If view_id is None the first one will be used of view_type.
|
|
The definition is a dictionary with keys:
|
|
- model: the model name
|
|
- arch: the xml description of the view
|
|
- fields: a dictionary with the definition of each field in the view
|
|
'''
|
|
key = (cls.__name__, view_id, view_type)
|
|
result = cls._fields_view_get_cache.get(key)
|
|
if result:
|
|
return result
|
|
result = {'model': cls.__name__}
|
|
pool = Pool()
|
|
|
|
test = True
|
|
model = True
|
|
sql_res = False
|
|
inherit_view_id = False
|
|
cursor = Transaction().cursor
|
|
while test:
|
|
if view_id:
|
|
where = (model and (" and model='%s'" % (cls.__name__,))) or ''
|
|
cursor.execute('SELECT arch, field_childs, id, type, ' \
|
|
'inherit, model ' \
|
|
'FROM ir_ui_view WHERE id = %s ' + where, (view_id,))
|
|
else:
|
|
cursor.execute('SELECT arch, field_childs, id, type, ' \
|
|
'inherit, model ' \
|
|
'FROM ir_ui_view ' \
|
|
'WHERE model = %s AND type = %s ' \
|
|
'ORDER BY inherit DESC, priority ASC, id ASC',
|
|
(cls.__name__, view_type))
|
|
sql_res = cursor.fetchone()
|
|
if not sql_res:
|
|
break
|
|
test = sql_res[4]
|
|
if test:
|
|
inherit_view_id = sql_res[2]
|
|
view_id = test or sql_res[2]
|
|
model = False
|
|
|
|
# if a view was found
|
|
if sql_res:
|
|
result['type'] = sql_res[3]
|
|
result['view_id'] = view_id
|
|
result['arch'] = sql_res[0]
|
|
result['field_childs'] = sql_res[1] or False
|
|
|
|
# Check if view is not from an inherited model
|
|
if sql_res[5] != cls.__name__:
|
|
Inherit = pool.get(sql_res[5])
|
|
result['arch'] = Inherit.fields_view_get(
|
|
result['view_id'])['arch']
|
|
view_id = inherit_view_id
|
|
|
|
# get all views which inherit from (ie modify) this view
|
|
cursor.execute('SELECT arch, domain, module FROM ir_ui_view ' \
|
|
'WHERE (inherit = %s AND model = %s) OR ' \
|
|
' (id = %s AND inherit IS NOT NULL) '
|
|
'ORDER BY priority ASC, id ASC',
|
|
(view_id, cls.__name__, view_id))
|
|
sql_inherit = cursor.fetchall()
|
|
raise_p = False
|
|
while True:
|
|
try:
|
|
sql_inherit.sort(key=lambda x:
|
|
cls._modules_list.index(x[2] or None))
|
|
break
|
|
except ValueError:
|
|
if raise_p:
|
|
raise
|
|
# There is perhaps a new module in the directory
|
|
ModelView._reset_modules_list()
|
|
raise_p = True
|
|
for arch, domain, _ in sql_inherit:
|
|
if domain:
|
|
if not safe_eval(domain,
|
|
{'context': Transaction().context}):
|
|
continue
|
|
if not arch or not arch.strip():
|
|
continue
|
|
result['arch'] = _inherit_apply(result['arch'], arch)
|
|
|
|
# otherwise, build some kind of default view
|
|
else:
|
|
if view_type == 'form':
|
|
res = cls.fields_get()
|
|
xml = '''<?xml version="1.0" encoding="utf-8"?>''' \
|
|
'''<form string="%s" col="4">''' % (cls.__doc__,)
|
|
for i in res:
|
|
if i in ('create_uid', 'create_date',
|
|
'write_uid', 'write_date', 'id', 'rec_name'):
|
|
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/>"
|
|
else:
|
|
xml += '<field name="%s" colspan="4"/>' % (i,)
|
|
xml += "</form>"
|
|
elif view_type == 'tree':
|
|
field = 'id'
|
|
if cls._rec_name in cls._fields:
|
|
field = cls._rec_name
|
|
xml = '''<?xml version="1.0" encoding="utf-8"?>''' \
|
|
'''<tree string="%s"><field name="%s"/></tree>''' \
|
|
% (cls.__doc__, field)
|
|
else:
|
|
xml = ''
|
|
result['type'] = view_type
|
|
result['arch'] = xml
|
|
result['field_childs'] = False
|
|
result['view_id'] = 0
|
|
|
|
# Update arch and compute fields from arch
|
|
parser = etree.XMLParser(remove_blank_text=True)
|
|
tree = etree.fromstring(result['arch'], parser)
|
|
xarch, xfields = cls._view_look_dom_arch(tree, result['type'],
|
|
result['field_childs'])
|
|
result['arch'] = xarch
|
|
result['fields'] = xfields
|
|
|
|
cls._fields_view_get_cache.set(key, result)
|
|
return result
|
|
|
|
@classmethod
|
|
def view_toolbar_get(cls):
|
|
"""
|
|
Returns the model specific actions.
|
|
A dictionary with keys:
|
|
- print: a list of available reports
|
|
- action: a list of available actions
|
|
- relate: a list of available relations
|
|
"""
|
|
Action = Pool().get('ir.action.keyword')
|
|
key = (cls.__name__, repr(Transaction().context))
|
|
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))
|
|
result = {
|
|
'print': prints,
|
|
'action': actions,
|
|
'relate': relates,
|
|
}
|
|
cls._view_toolbar_get_cache.set(key, result)
|
|
return result
|
|
|
|
@classmethod
|
|
def view_header_get(cls, value, view_type='form'):
|
|
"""
|
|
Overload this method if you need a window title.
|
|
which depends on the context
|
|
|
|
:param value: the default header string
|
|
:param view_type: the type of the view
|
|
:return: the header string of the view
|
|
"""
|
|
return value
|
|
|
|
@classmethod
|
|
def _view_look_dom_arch(cls, tree, type, field_children=None):
|
|
pool = Pool()
|
|
ModelAccess = pool.get('ir.model.access')
|
|
FieldAccess = pool.get('ir.model.field.access')
|
|
|
|
fields_width = {}
|
|
tree_root = tree.getroottree().getroot()
|
|
|
|
# Find field without read access
|
|
fread_accesses = FieldAccess.check(cls.__name__,
|
|
cls._fields.keys(), 'read', access=True)
|
|
fields_to_remove = list(x for x, y in fread_accesses.iteritems()
|
|
if not y)
|
|
|
|
def check_relation(model, field):
|
|
if field._type in ('one2many', 'many2one'):
|
|
if not ModelAccess.check(field.model_name, mode='read',
|
|
raise_exception=False):
|
|
return False
|
|
if field._type in ('many2many', 'one2one'):
|
|
if not ModelAccess.check(field.target, mode='read',
|
|
raise_exception=False):
|
|
return False
|
|
elif not ModelAccess.check(field.relation_name,
|
|
mode='read', raise_exception=False):
|
|
return False
|
|
if field._type == 'reference':
|
|
selection = field.selection
|
|
if isinstance(selection, basestring):
|
|
selection = getattr(model, field.selection)()
|
|
for model_name, _ in selection:
|
|
if not ModelAccess.check(model_name, mode='read',
|
|
raise_exception=False):
|
|
return False
|
|
return True
|
|
|
|
# Find relation field without read access
|
|
for name, field in cls._fields.iteritems():
|
|
if not check_relation(cls, field):
|
|
fields_to_remove.append(name)
|
|
|
|
for name, field in cls._fields.iteritems():
|
|
for field_to_remove in fields_to_remove:
|
|
if field_to_remove in field.depends:
|
|
fields_to_remove.append(name)
|
|
|
|
# Find field inherited without read access
|
|
for inherit_name in cls._inherits:
|
|
Inherit = pool.get(inherit_name)
|
|
fread_accesses = FieldAccess.check(Inherit.__name__,
|
|
Inherit._fields.keys(), 'read', access=True)
|
|
fields_to_remove += list(x for x, y in fread_accesses.iteritems()
|
|
if not y and x not in cls._fields.keys())
|
|
|
|
# Find relation field without read access
|
|
for name, field in Inherit._fields.iteritems():
|
|
if not check_relation(Inherit, field):
|
|
fields_to_remove.append(name)
|
|
|
|
for name, field in Inherit._fields.iteritems():
|
|
for field_to_remove in fields_to_remove:
|
|
if field_to_remove in field.depends:
|
|
fields_to_remove.append(name)
|
|
|
|
# Remove field without read access
|
|
for field in fields_to_remove:
|
|
for element in tree.xpath(
|
|
'//field[@name="%s"] | //label[@name="%s"]'
|
|
% (field, field)):
|
|
if type == 'form':
|
|
element.tag = 'label'
|
|
element.attrib.clear()
|
|
elif type == 'tree':
|
|
parent = element.getparent()
|
|
parent.remove(element)
|
|
|
|
if type == 'tree':
|
|
ViewTreeWidth = pool.get('ir.ui.view_tree_width')
|
|
viewtreewidth_ids = ViewTreeWidth.search([
|
|
('model', '=', cls.__name__),
|
|
('user', '=', Transaction().user),
|
|
])
|
|
for viewtreewidth in ViewTreeWidth.browse(viewtreewidth_ids):
|
|
if viewtreewidth.width > 0:
|
|
fields_width[viewtreewidth.field] = viewtreewidth.width
|
|
|
|
fields_def = cls.__view_look_dom(tree_root, type,
|
|
fields_width=fields_width)
|
|
|
|
if field_children:
|
|
fields_def.setdefault(field_children, {'name': field_children})
|
|
model, field = None, None
|
|
if field_children in cls._fields:
|
|
model = cls
|
|
field = cls._fields[field_children]
|
|
elif field_children in cls._inherit_fields:
|
|
model_name, model, field = cls._inherit_fields[field_children]
|
|
if model and field and field.model_name == model.__name__:
|
|
fields_def.setdefault(field.field, {'name': field.field})
|
|
|
|
for field_name in fields_def.keys():
|
|
if field_name in cls._fields:
|
|
field = cls._fields[field_name]
|
|
elif field_name in cls._inherit_fields:
|
|
field = cls._inherit_fields[field_name][2]
|
|
else:
|
|
continue
|
|
for depend in field.depends:
|
|
fields_def.setdefault(depend, {'name': depend})
|
|
|
|
if ('active' in cls._fields) or ('active' in cls._inherit_fields):
|
|
fields_def.setdefault('active', {'name': 'active'})
|
|
|
|
arch = etree.tostring(tree, encoding='utf-8', pretty_print=False)
|
|
fields2 = cls.fields_get(fields_def.keys())
|
|
for field in fields_def:
|
|
if field in fields2:
|
|
fields2[field].update(fields_def[field])
|
|
return arch, fields2
|
|
|
|
@classmethod
|
|
def __view_look_dom(cls, element, type, fields_width=None):
|
|
pool = Pool()
|
|
Translation = pool.get('ir.translation')
|
|
ModelData = pool.get('ir.model.data')
|
|
Button = pool.get('ir.model.button')
|
|
User = pool.get('res.user')
|
|
|
|
if fields_width is None:
|
|
fields_width = {}
|
|
fields_attrs = {}
|
|
childs = True
|
|
|
|
if element.tag in ('field', 'label', 'separator', 'group'):
|
|
for attr in ('name', 'icon'):
|
|
if element.get(attr):
|
|
attrs = {}
|
|
try:
|
|
if element.get(attr) in cls._fields:
|
|
field = cls._fields[element.get(attr)]
|
|
else:
|
|
field = cls._inherit_fields[element.get(
|
|
attr)][2]
|
|
if hasattr(field, 'model_name'):
|
|
relation = field.model_name
|
|
else:
|
|
relation = field.get_target().__name__
|
|
except Exception:
|
|
relation = False
|
|
if relation and element.tag == 'field':
|
|
childs = False
|
|
views = {}
|
|
mode = (element.attrib.pop('mode', None)
|
|
or 'tree,form').split(',')
|
|
view_ids = []
|
|
if element.get('view_ids'):
|
|
for view_id in element.get('view_ids').split(','):
|
|
try:
|
|
view_ids.append(int(view_id))
|
|
except ValueError:
|
|
view_ids.append(ModelData.get_id(
|
|
*view_id.split('.')))
|
|
Relation = pool.get(relation)
|
|
if (not len(element)
|
|
and type == 'form'
|
|
and field._type in ('one2many', 'many2many')):
|
|
# 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[view['type']] = view
|
|
break
|
|
else:
|
|
for view_type in mode:
|
|
views[view_type] = \
|
|
Relation.fields_view_get(
|
|
view_type=view_type)
|
|
break
|
|
element.attrib['mode'] = ','.join(mode)
|
|
element.attrib['view_ids'] = ','.join(
|
|
map(str, view_ids))
|
|
attrs = {
|
|
'views': views,
|
|
}
|
|
fields_attrs[element.get(attr)] = attrs
|
|
if element.get('name') in fields_width:
|
|
element.set('width', str(fields_width[element.get('name')]))
|
|
|
|
# convert attributes into pyson
|
|
encoder = PYSONEncoder()
|
|
for attr in ('states', 'domain', 'context', 'digits', 'add_remove',
|
|
'spell', 'colors'):
|
|
if element.get(attr):
|
|
element.set(attr, encoder.encode(safe_eval(element.get(attr),
|
|
CONTEXT)))
|
|
|
|
if element.tag == 'button':
|
|
button_name = element.attrib['name']
|
|
if button_name in cls._buttons:
|
|
states = cls._buttons[button_name]
|
|
else:
|
|
states = {}
|
|
groups = set(User.get_groups())
|
|
button_groups = Button.get_groups(cls.__name__, button_name)
|
|
if button_groups and not groups & button_groups:
|
|
states = states.copy()
|
|
states['readonly'] = True
|
|
element.set('states', encoder.encode(states))
|
|
|
|
# translate view
|
|
if Transaction().language != 'en_US':
|
|
for attr in ('string', 'sum', 'confirm', 'help'):
|
|
if element.get(attr):
|
|
trans = Translation.get_source(cls.__name__, 'view',
|
|
Transaction().language, element.get(attr))
|
|
if trans:
|
|
element.set(attr, trans)
|
|
|
|
# Set header string
|
|
if element.tag in ('form', 'tree', 'graph'):
|
|
element.set('string', cls.view_header_get(
|
|
element.get('string') or '', view_type=element.tag))
|
|
|
|
if element.tag == 'tree' and element.get('sequence'):
|
|
fields_attrs.setdefault(element.get('sequence'), {})
|
|
|
|
if childs:
|
|
for field in element:
|
|
fields_attrs.update(cls.__view_look_dom(field, type,
|
|
fields_width=fields_width))
|
|
return fields_attrs
|
|
|
|
@staticmethod
|
|
def button(func):
|
|
@wraps(func)
|
|
def wrapper(cls, *args, **kwargs):
|
|
pool = Pool()
|
|
Button = pool.get('ir.model.button')
|
|
User = pool.get('res.user')
|
|
|
|
if Transaction().user != 0:
|
|
groups = set(User.get_groups())
|
|
button_groups = Button.get_groups(cls.__name__,
|
|
func.__name__)
|
|
if button_groups and not groups & button_groups:
|
|
raise UserError('Calling button %s on %s is not allowed!'
|
|
% (func.__name__, cls.__name__))
|
|
return func(cls, *args, **kwargs)
|
|
return wrapper
|
|
|
|
@staticmethod
|
|
def button_action(action):
|
|
def decorator(func):
|
|
func = ModelView.button(func)
|
|
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
Action = pool.get('ir.action')
|
|
|
|
func(*args, **kwargs)
|
|
|
|
module, fs_id = action.split('.')
|
|
action_id = Action.get_action_id(
|
|
ModelData.get_id(module, fs_id))
|
|
return action_id
|
|
return wrapper
|
|
return decorator
|