diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 2ced177fdb9..c8df66c55da 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -52,6 +52,7 @@ import re import time import traceback import types +import simplejson import openerp.netsvc as netsvc from lxml import etree @@ -75,6 +76,88 @@ module_class_list = {} # Super-user identifier (aka Administrator aka root) ROOT_USER_ID = 1 +def transfer_field_to_modifiers(field, modifiers): + for a in ('invisible', 'readonly', 'required'): + if field.get(a): + modifiers[a] = bool(field.get(a)) + + +# Don't deal with groups, it is done by check_group(). +# Need the context to evaluate the invisible attribute on tree views. +# For non-tree views, the context shouldn't be given. +def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False): + if node.get('attrs'): + modifiers.update(eval(node.get('attrs'))) + + if node.get('states'): + if 'invisible' in modifiers and isinstance(modifiers['invisible'], list): + # TODO combine with AND or OR, use implicit AND for now. + modifiers['invisible'].append(('state', 'not in', node.get('states').split(','))) + else: + modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))] + + for a in ('invisible', 'readonly', 'required'): + if node.get(a): + v = bool(eval(node.get(a), {'context': context or {}})) + if in_tree_view and a == 'invisible': + # Invisible in a tree view has a specific meaning, make it a + # new key in the modifiers attribute. + modifiers['tree_invisible'] = v + elif v or (a not in modifiers or not isinstance(modifiers[a], list)): + # Don't set the attribute to False if a dynamic value was + # provided (i.e. a domain from attrs or states). + modifiers[a] = v + + +def simplify_modifiers(modifiers): + for a in ('invisible', 'readonly', 'required'): + if a in modifiers and not modifiers[a]: + del modifiers[a] + + +def transfer_modifiers_to_node(modifiers, node): + if modifiers: + simplify_modifiers(modifiers) + node.set('modifiers', simplejson.dumps(modifiers)) + + +def test_modifiers(what, expected): + modifiers = {} + if isinstance(what, basestring): + node = etree.fromstring(what) + transfer_node_to_modifiers(node, modifiers) + simplify_modifiers(modifiers) + json = simplejson.dumps(modifiers) + assert json == expected, "%s != %s" % (json, expected) + elif isinstance(what, dict): + transfer_field_to_modifiers(what, modifiers) + simplify_modifiers(modifiers) + json = simplejson.dumps(modifiers) + assert json == expected, "%s != %s" % (json, expected) + + +# To use this test: +# import openerp +# openerp.osv.orm.modifiers_tests() +def modifiers_tests(): + test_modifiers('', '{}') + test_modifiers('', '{"invisible": true}') + test_modifiers('', '{"readonly": true}') + test_modifiers('', '{"required": true}') + test_modifiers('', '{}') + test_modifiers('', '{}') + test_modifiers('', '{}') + test_modifiers('', '{"invisible": true, "required": true}') # TODO order is not guaranteed + test_modifiers('', '{"invisible": true}') + test_modifiers('', '{"required": true}') + test_modifiers("""""", '{"invisible": [["b", "=", "c"]]}') + + # The dictionary is supposed to be the result of fields_get(). + test_modifiers({}, '{}') + test_modifiers({"invisible": True}, '{"invisible": true}') + test_modifiers({"invisible": False}, '{}') + + def check_object_name(name): """ Check if the given name is a valid openerp object name. @@ -1346,13 +1429,26 @@ class orm_template(object): def view_header_get(self, cr, user, view_id=None, view_type='form', context=None): return False - def __view_look_dom(self, cr, user, node, view_id, context=None): + def __view_look_dom(self, cr, user, node, view_id, in_tree_view, model_fields, context=None): + """ Return the description of the fields in the node. + + In a normal call to this method, node is a complete view architecture + but it is actually possible to give some sub-node (this is used so + that the method can call itself recursively). + + Originally, the field descriptions are drawn from the node itself. + But there is now some code calling fields_get() in order to merge some + of those information in the architecture. + + """ if context is None: context = {} result = False fields = {} children = True + modifiers = {} + def encode(s): if isinstance(s, unicode): return s.encode('utf8') @@ -1366,6 +1462,7 @@ class orm_template(object): can_see = any(access_pool.check_groups(cr, user, group) for group in groups) if not can_see: node.set('invisible', '1') + modifiers['invisible'] = True if 'attrs' in node.attrib: del(node.attrib['attrs']) #avoid making field visible later del(node.attrib['groups']) @@ -1437,10 +1534,14 @@ class orm_template(object): attrs['selection'].append((False, '')) fields[node.get('name')] = attrs + field = model_fields[node.get('name')] + transfer_field_to_modifiers(field, modifiers) + elif node.tag in ('form', 'tree'): result = self.view_header_get(cr, user, False, node.tag, context) if result: node.set('string', result) + in_tree_view = node.tag == 'tree' elif node.tag == 'calendar': for additional_field in ('date_start', 'date_delay', 'date_stop', 'color'): @@ -1449,6 +1550,12 @@ class orm_template(object): check_group(node) + # The view architeture overrides the python model. + # Get the attrs before they are (possibly) deleted by check_group below + transfer_node_to_modifiers(node, modifiers, context, in_tree_view) + + # TODO remove attrs couterpart in modifiers when invisible is true ? + # translate view if 'lang' in context: if node.get('string') and not result: @@ -1474,8 +1581,9 @@ class orm_template(object): for f in node: if children or (node.tag == 'field' and f.tag in ('filter','separator')): - fields.update(self.__view_look_dom(cr, user, f, view_id, context)) + fields.update(self.__view_look_dom(cr, user, f, view_id, in_tree_view, model_fields, context)) + transfer_modifiers_to_node(modifiers, node) return fields def _disable_workflow_buttons(self, cr, user, node): @@ -1504,19 +1612,32 @@ class orm_template(object): return node def __view_look_dom_arch(self, cr, user, node, view_id, context=None): - fields_def = self.__view_look_dom(cr, user, node, view_id, context=context) - node = self._disable_workflow_buttons(cr, user, node) - arch = etree.tostring(node, encoding="utf-8").replace('\t', '') + """ Return an architecture and a description of all the fields. + + The field description combines the result of fields_get() and + __view_look_dom(). + + :param node: the architecture as as an etree + :return: a tuple (arch, fields) where arch is the given node as a + string and fields is the description of all the fields. + + """ fields = {} if node.tag == 'diagram': if node.getchildren()[0].tag == 'node': - node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, fields_def.keys(), context) + node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, None, context) fields.update(node_fields) if node.getchildren()[1].tag == 'arrow': - arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, fields_def.keys(), context) + arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, None, context) fields.update(arrow_fields) else: - fields = self.fields_get(cr, user, fields_def.keys(), context) + fields = self.fields_get(cr, user, None, context) + fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context) + node = self._disable_workflow_buttons(cr, user, node) + arch = etree.tostring(node, encoding="utf-8").replace('\t', '') + for k in fields.keys(): + if k not in fields_def: + del fields[k] for field in fields_def: if field == 'id': # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references) @@ -1537,6 +1658,7 @@ class orm_template(object): def __get_default_calendar_view(self): """Generate a default calendar view (For internal use only). """ + # TODO could return an etree instead of a string arch = ('\n' '