[MERGE] Sync with trunk until revision 4957.
authorThibault Delavallée <tde@openerp.com>
Wed, 2 Oct 2013 13:38:20 +0000 (15:38 +0200)
committerThibault Delavallée <tde@openerp.com>
Wed, 2 Oct 2013 13:38:20 +0000 (15:38 +0200)
bzr revid: tde@openerp.com-20131002133820-8xomdumexexqp3pk

1  2 
openerp/addons/base/ir/ir_ui_view.py
openerp/addons/base/res/res_partner.py
openerp/addons/base/security/ir.model.access.csv
openerp/addons/base/tests/test_views.py
openerp/osv/orm.py
openerp/tools/convert.py
openerp/tools/mail.py

  #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  #
  ##############################################################################
 -
 +import collections
 +import copy
  import logging
+ from lxml import etree
+ from operator import itemgetter
  import os
 +import sys
 +import re
 +import time
 +
 +import HTMLParser
 +from lxml import etree, html
 +from functools import partial
  
  from openerp import tools
 -from openerp.osv import fields,osv
 -from openerp.tools import graph
 +from openerp.osv import fields, osv, orm
 +from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
  from openerp.tools.safe_eval import safe_eval as eval
  from openerp.tools.view_validation import valid_view
 +from openerp.tools import misc, qweb
  
  _logger = logging.getLogger(__name__)
  
@@@ -102,30 -115,54 +104,34 @@@ class view(osv.osv)
                  frng.close()
          return self._relaxng_validator
  
 -    def _check_render_view(self, cr, uid, view, context=None):
 -        """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
 -           its inherited views, by rendering it using ``fields_view_get()``.
 -           
 -           @param browse_record view: view to validate
 -           @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
 -               if no error occurred, else False.  
 -        """
 -        if view.model not in self.pool:
 -            return False
 -        try:
 -            fvg = self.pool[view.model].fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
 -            return fvg['arch']
 -        except Exception:
 -            _logger.exception('cannot render view %s', view.xml_id)
 -            return False
 -
      def _check_xml(self, cr, uid, ids, context=None):
+         if context is None:
+             context = {}
+         context['check_view_ids'] = ids
          for view in self.browse(cr, uid, ids, context):
              # Sanity check: the view should not break anything upon rendering!
 -            view_arch_utf8 = self._check_render_view(cr, uid, view, context=context)
 -            # always utf-8 bytestring - legacy convention
 -            if not view_arch_utf8: return False
 -
 -            # RNG-based validation is not possible anymore with 7.0 forms
 -            # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
 -            view_docs = [etree.fromstring(view_arch_utf8)]
 -            if view_docs[0].tag == 'data':
 -                # A <data> element is a wrapper for multiple root nodes
 -                view_docs = view_docs[0]
 -            validator = self._relaxng()
 -            for view_arch in view_docs:
 -                if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
 -                    for error in validator.error_log:
 -                        _logger.error(tools.ustr(error))
 -                    return False
 -                if not valid_view(view_arch):
 -                    return False
 -        return True
 -
 -    def _check_model(self, cr, uid, ids, context=None):
 -        for view in self.browse(cr, uid, ids, context):
 -            if view.model not in self.pool:
 +            try:
 +                fvg = self.read_combined(cr, uid, view.id, None, context=context)
 +                view_arch_utf8 = fvg['arch']
 +            except Exception, e:
 +                _logger.exception(e)
                  return False
 +            if view.type != 'qweb':
 +                # RNG-based validation is not possible anymore with 7.0 forms
 +                # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
 +                view_docs = [etree.fromstring(view_arch_utf8)]
 +                if view_docs[0].tag == 'data':
 +                    # A <data> element is a wrapper for multiple root nodes
 +                    view_docs = view_docs[0]
 +                validator = self._relaxng()
 +                for view_arch in view_docs:
 +                    if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
 +                        for error in validator.error_log:
 +                            _logger.error(tools.ustr(error))
 +                        return False
 +                    if not valid_view(view_arch):
 +                        return False
          return True
  
      _constraints = [
             :rtype: list of tuples
             :return: [(view_arch,view_id), ...]
          """
          user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
 +
++        check_view_ids = context and context.get('check_view_ids') or (0,)
 +        conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
          if self.pool._init:
 -            # Module init currently in progress, only consider views from modules whose code was already loaded 
 -            check_view_ids = context and context.get('check_view_ids') or (0,)
 -            query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
 -                       WHERE v.inherit_id=%s AND v.model=%s AND (md.module in %s OR v.id in %s)
 -                       ORDER BY priority"""
 -            query_params = (view_id, model, tuple(self.pool._init_modules), tuple(check_view_ids))
 -        else:
 -            # Modules fully loaded, consider all views
 -            query = """SELECT v.id FROM ir_ui_view v
 -                       WHERE v.inherit_id=%s AND v.model=%s  
 -                       ORDER BY priority"""
 -            query_params = (view_id, model)
 -        cr.execute(query, query_params)
 -        view_ids = [v[0] for v in cr.fetchall()]
 -        # filter views based on user groups
 +            # Module init currently in progress, only consider views from
 +            # modules whose code is already loaded
 +            conditions.extend([
 +                ['model_ids.model', '=', 'ir.ui.view'],
++                '|',
 +                ['model_ids.module', 'in', tuple(self.pool._init_modules)],
++                ['id', 'in', check_view_ids],
 +            ])
 +        view_ids = self.search(cr, uid, conditions, context=context)
 +
          return [(view.arch, view.id)
                  for view in self.browse(cr, 1, view_ids, context)
                  if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
  
 -    def write(self, cr, uid, ids, vals, context=None):
 -        if not isinstance(ids, (list, tuple)):
 -            ids = [ids]
 +    def raise_view_error(self, cr, uid, message, view_id, context=None):
 +        view = self.browse(cr, uid, [view_id], context)[0]
 +        message = "Inherit error: %s view_id: %s, xml_id: %s, model: %s, parent_view: %s" % (message, view_id, view.xml_id, view.model, view.inherit_id)
 +        raise AttributeError(message)
  
 -        # drop the corresponding view customizations (used for dashboards for example), otherwise
 -        # not all users would see the updated views
 -        custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
 -        if custom_view_ids:
 -            self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
 +    def locate_node(self, arch, spec):
 +        """ Locate a node in a source (parent) architecture.
  
 -        return super(view, self).write(cr, uid, ids, vals, context)
 +        Given a complete source (parent) architecture (i.e. the field
 +        `arch` in a view), and a 'spec' node (a node in an inheriting
 +        view that specifies the location in the source view of what
 +        should be changed), return (if it exists) the node in the
 +        source view matching the specification.
 +
 +        :param arch: a parent architecture to modify
 +        :param spec: a modifying node in an inheriting view
 +        :return: a node in the source matching the spec
 +        """
 +        if spec.tag == 'xpath':
 +            nodes = arch.xpath(spec.get('expr'))
 +            return nodes[0] if nodes else None
 +        elif spec.tag == 'field':
 +            # Only compare the field name: a field can be only once in a given view
 +            # at a given level (and for multilevel expressions, we should use xpath
 +            # inheritance spec anyway).
 +            for node in arch.iter('field'):
 +                if node.get('name') == spec.get('name'):
 +                    return node
 +            return None
 +
 +        for node in arch.iter(spec.tag):
 +            if isinstance(node, SKIPPED_ELEMENT_TYPES):
 +                continue
 +            if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
 +                   if attr not in ('position','version')):
 +                # Version spec should match parent's root element's version
 +                if spec.get('version') and spec.get('version') != arch.get('version'):
 +                    return None
 +                return node
 +        return None
 +
 +    def inherit_branding(self, specs_tree, view_id, source_id):
 +        for node in specs_tree.iterchildren(tag=etree.Element):
 +            xpath = node.getroottree().getpath(node)
 +            if node.tag == 'data' or node.tag == 'xpath':
 +                self.inherit_branding(node, view_id, source_id)
 +            else:
 +                node.set('data-oe-id', str(view_id))
 +                node.set('data-oe-source-id', str(source_id))
 +                node.set('data-oe-xpath', xpath)
 +                node.set('data-oe-model', 'ir.ui.view')
 +                node.set('data-oe-field', 'arch')
 +
 +        return specs_tree
 +
 +    def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
 +        """ Apply an inheriting view (a descendant of the base view)
 +
 +        Apply to a source architecture all the spec nodes (i.e. nodes
 +        describing where and what changes to apply to some parent
 +        architecture) given by an inheriting view.
 +
 +        :param Element source: a parent architecture to modify
 +        :param Elepect specs_tree: a modifying architecture in an inheriting view
 +        :param inherit_id: the database id of specs_arch
 +        :return: a modified source where the specs are applied
 +        :rtype: Element
 +        """
 +        # Queue of specification nodes (i.e. nodes describing where and
 +        # changes to apply to some parent architecture).
 +        specs = [specs_tree]
 +
 +        while len(specs):
 +            spec = specs.pop(0)
 +            if isinstance(spec, SKIPPED_ELEMENT_TYPES):
 +                continue
 +            if spec.tag == 'data':
 +                specs += [ c for c in specs_tree ]
 +                continue
 +            node = self.locate_node(source, spec)
 +            if node is not None:
 +                pos = spec.get('position', 'inside')
 +                if pos == 'replace':
 +                    if node.getparent() is None:
 +                        source = copy.deepcopy(spec[0])
 +                    else:
 +                        for child in spec:
 +                            node.addprevious(child)
 +                        node.getparent().remove(node)
 +                elif pos == 'attributes':
 +                    for child in spec.getiterator('attribute'):
 +                        attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
 +                        if attribute[1]:
 +                            node.set(attribute[0], attribute[1])
 +                        else:
 +                            del(node.attrib[attribute[0]])
 +                else:
 +                    sib = node.getnext()
 +                    for child in spec:
 +                        if pos == 'inside':
 +                            node.append(child)
 +                        elif pos == 'after':
 +                            if sib is None:
 +                                node.addnext(child)
 +                                node = child
 +                            else:
 +                                sib.addprevious(child)
 +                        elif pos == 'before':
 +                            node.addprevious(child)
 +                        else:
 +                            self.raise_view_error(cr, uid, "Invalid position value: '%s'" % pos, inherit_id, context=context)
 +            else:
 +                attrs = ''.join([
 +                    ' %s="%s"' % (attr, spec.get(attr))
 +                    for attr in spec.attrib
 +                    if attr != 'position'
 +                ])
 +                tag = "<%s%s>" % (spec.tag, attrs)
 +                self.raise_view_error(cr, uid, "Element '%s' not found in parent view " % tag, inherit_id, context=context)
 +
 +        return source
 +
 +    def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None):
 +        """ Apply all the (directly and indirectly) inheriting views.
 +
 +        :param source: a parent architecture to modify (with parent modifications already applied)
 +        :param source_id: the database view_id of the parent view
 +        :param model: the original model for which we create a view (not
 +            necessarily the same as the source's model); only the inheriting
 +            views with that specific model will be applied.
 +        :return: a modified source where all the modifying architecture are applied
 +        """
 +        if context is None: context = {}
-         sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model)
++        sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
 +        for (specs, view_id) in sql_inherit:
 +            specs_tree = etree.fromstring(specs.encode('utf-8'))
 +            if context.get('inherit_branding'):
 +                self.inherit_branding(specs_tree, view_id, source_id)
 +            source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
 +            source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context)
 +        return source
 +
 +    def read_combined(self, cr, uid, view_id, fields=None, context=None):
 +        """
 +        Utility function to get a view combined with its inherited views.
 +
 +        * Gets the top of the view tree if a sub-view is requested
 +        * Applies all inherited archs on the root view
 +        * Returns the view with all requested fields
 +          .. note:: ``arch`` is always added to the fields list even if not
 +                    requested (similar to ``id``)
 +        """
 +        if context is None: context = {}
 +
 +        # if view_id is not a root view, climb back to the top.
 +        v = self.browse(cr, uid, view_id, context=context)
 +        while v.inherit_id:
 +            v = v.inherit_id
 +        root_id = v.id
 +
 +        # arch and model fields are always returned
 +        if fields:
 +            fields = list(set(fields) | set(['arch', 'model']))
 +
 +        # read the view arch
 +        [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
 +        arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
 +
 +        if context.get('inherit_branding'):
 +            arch_tree.attrib.update({
 +                'data-oe-model': 'ir.ui.view',
 +                'data-oe-id': str(root_id),
 +                'data-oe-field': 'arch',
 +            })
 +
 +        # and apply inheritance
 +        arch = self.apply_view_inheritance(cr, uid, arch_tree, root_id, v.model, context=context)
 +
 +        return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
 +
 +    # postprocessing: groups, modifiers, ...
 +
 +    def postprocess(self, cr, user, model, 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 = {}
 +        Model = self.pool.get(model)
 +
 +        def encode(s):
 +            if isinstance(s, unicode):
 +                return s.encode('utf8')
 +            return s
 +
 +        def check_group(node):
 +            """Apply group restrictions,  may be set at view level or model level::
 +               * at view level this means the element should be made invisible to
 +                 people who are not members
 +               * at model level (exclusively for fields, obviously), this means
 +                 the field should be completely removed from the view, as it is
 +                 completely unavailable for non-members
 +
 +               :return: True if field should be included in the result of fields_view_get
 +            """
 +            if Model and node.tag == 'field' and node.get('name') in Model._all_columns:
 +                column = Model._all_columns[node.get('name')].column
 +                if column.groups and not self.user_has_groups(
 +                        cr, user, groups=column.groups, context=context):
 +                    node.getparent().remove(node)
 +                    fields.pop(node.get('name'), None)
 +                    # no point processing view-level ``groups`` anymore, return
 +                    return False
 +            if node.get('groups'):
 +                can_see = self.user_has_groups(
 +                    cr, user, groups=node.get('groups'), context=context)
 +                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'])
 +            return True
 +
 +        if node.tag in ('field', 'node', 'arrow'):
 +            if node.get('object'):
 +                attrs = {}
 +                views = {}
 +                xml = "<form>"
 +                for f in node:
 +                    if f.tag == 'field':
 +                        xml += etree.tostring(f, encoding="utf-8")
 +                xml += "</form>"
 +                new_xml = etree.fromstring(encode(xml))
 +                ctx = context.copy()
 +                ctx['base_model_name'] = model
 +                xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
 +                views['form'] = {
 +                    'arch': xarch,
 +                    'fields': xfields
 +                }
 +                attrs = {'views': views}
 +                fields = xfields
 +            if node.get('name'):
 +                attrs = {}
 +                try:
 +                    if node.get('name') in Model._columns:
 +                        column = Model._columns[node.get('name')]
 +                    else:
 +                        column = Model._inherit_fields[node.get('name')][2]
 +                except Exception:
 +                    column = False
 +
 +                if column:
 +                    relation = self.pool[column._obj] if column._obj else None
 +
 +                    children = False
 +                    views = {}
 +                    for f in node:
 +                        if f.tag in ('form', 'tree', 'graph', 'kanban'):
 +                            node.remove(f)
 +                            ctx = context.copy()
 +                            ctx['base_model_name'] = Model
 +                            xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
 +                            views[str(f.tag)] = {
 +                                'arch': xarch,
 +                                'fields': xfields
 +                            }
 +                    attrs = {'views': views}
 +                    if node.get('widget') and node.get('widget') == 'selection':
 +                        # Prepare the cached selection list for the client. This needs to be
 +                        # done even when the field is invisible to the current user, because
 +                        # other events could need to change its value to any of the selectable ones
 +                        # (such as on_change events, refreshes, etc.)
 +
 +                        # If domain and context are strings, we keep them for client-side, otherwise
 +                        # we evaluate them server-side to consider them when generating the list of
 +                        # possible values
 +                        # TODO: find a way to remove this hack, by allow dynamic domains
 +                        dom = []
 +                        if column._domain and not isinstance(column._domain, basestring):
 +                            dom = list(column._domain)
 +                        dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time})
 +                        search_context = dict(context)
 +                        if column._context and not isinstance(column._context, basestring):
 +                            search_context.update(column._context)
 +                        attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
 +                        if (node.get('required') and not int(node.get('required'))) or not column.required:
 +                            attrs['selection'].append((False, ''))
 +                fields[node.get('name')] = attrs
 +
 +                field = model_fields.get(node.get('name'))
 +                if field:
 +                    orm.transfer_field_to_modifiers(field, modifiers)
 +
 +
 +        elif node.tag in ('form', 'tree'):
 +            result = Model.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'):
 +                if node.get(additional_field):
 +                    fields[node.get(additional_field)] = {}
 +
 +        if not check_group(node):
 +            # node must be removed, no need to proceed further with its children
 +            return fields
 +
 +        # The view architeture overrides the python model.
 +        # Get the attrs before they are (possibly) deleted by check_group below
 +        orm.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:
 +            Translations = self.pool['ir.translation']
 +            if node.text and node.text.strip():
 +                trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
 +                if trans:
 +                    node.text = node.text.replace(node.text.strip(), trans)
 +            if node.tail and node.tail.strip():
 +                trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
 +                if trans:
 +                    node.tail =  node.tail.replace(node.tail.strip(), trans)
 +
 +            if node.get('string') and not result:
 +                trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
 +                if trans == node.get('string') and ('base_model_name' in context):
 +                    # If translation is same as source, perhaps we'd have more luck with the alternative model name
 +                    # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
 +                    trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
 +                if trans:
 +                    node.set('string', trans)
 +
 +            for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
 +                attr_value = node.get(attr_name)
 +                if attr_value:
 +                    trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
 +                    if trans:
 +                        node.set(attr_name, trans)
 +
 +        for f in node:
 +            if children or (node.tag == 'field' and f.tag in ('filter','separator')):
 +                fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
 +
 +        orm.transfer_modifiers_to_node(modifiers, node)
 +        return fields
 +
 +    def _disable_workflow_buttons(self, cr, user, model, node):
 +        """ Set the buttons in node to readonly if the user can't activate them. """
 +        if model is None or user == 1:
 +            # admin user can always activate workflow buttons
 +            return node
 +
 +        # TODO handle the case of more than one workflow for a model or multiple
 +        # transitions with different groups and same signal
 +        usersobj = self.pool.get('res.users')
 +        buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
 +        for button in buttons:
 +            user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
 +            cr.execute("""SELECT DISTINCT t.group_id
 +                        FROM wkf
 +                  INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
 +                  INNER JOIN wkf_transition t ON (t.act_to = a.id)
 +                       WHERE wkf.osv = %s
 +                         AND t.signal = %s
 +                         AND t.group_id is NOT NULL
 +                   """, (model, button.get('name')))
 +            group_ids = [x[0] for x in cr.fetchall() if x[0]]
 +            can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
 +            button.set('readonly', str(int(not can_click)))
 +        return node
 +
 +    def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
 +        """ Return an architecture and a description of all the fields.
 +
 +        The field description combines the result of fields_get() and
 +        postprocess().
 +
 +        :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 = {}
 +        Model = self.pool.get(model)
 +
 +        if node.tag == 'diagram':
 +            if node.getchildren()[0].tag == 'node':
 +                node_model = self.pool[node.getchildren()[0].get('object')]
 +                node_fields = node_model.fields_get(cr, user, None, context)
 +                fields.update(node_fields)
 +                if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
 +                    node.set("create", 'false')
 +            if node.getchildren()[1].tag == 'arrow':
 +                arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
 +                fields.update(arrow_fields)
 +        elif Model:
 +            fields = Model.fields_get(cr, user, None, context)
 +
 +        fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
 +        node = self._disable_workflow_buttons(cr, user, model, node)
 +        if node.tag in ('kanban', 'tree', 'form', 'gantt'):
 +            for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
 +                if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
 +                    node.set(action, 'false')
 +        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)
 +                fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
 +            elif field in fields:
 +                fields[field].update(fields_def[field])
 +            else:
 +                cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
 +                res = cr.fetchall()[:]
 +                model = res[0][1]
 +                res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
 +                msg = "\n * ".join([r[0] for r in res])
 +                msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
 +                _logger.error(msg)
 +                raise orm.except_orm('View error', msg)
 +        return arch, fields
 +
 +    # view used as templates
 +
 +    def read_template(self, cr, uid, id_, context=None):
 +        try:
 +            id_ = int(id_)
 +        except ValueError:
 +            if '.' not in id_:
 +                raise ValueError('Invalid id: %r' % (id_,))
 +            IMD = self.pool['ir.model.data']
 +            m, _, n = id_.partition('.')
 +            _, id_ = IMD.get_object_reference(cr, uid, m, n)
 +
 +        arch = self.read_combined(cr, uid, id_, fields=['arch'], context=context)['arch']
 +        arch_tree = etree.fromstring(arch)
 +        if 'lang' in context:
 +            arch_tree = self.translate_qweb(cr, uid, id_, arch_tree, context['lang'], context)
 +        self.distribute_branding(arch_tree)
 +        root = etree.Element('tpl')
 +        root.append(arch_tree)
 +        arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
 +        return arch
 +
 +    def distribute_branding(self, e, branding=None, parent_xpath='',
 +                            index_map=misc.ConstantMapping(1)):
 +        if e.get('t-ignore') or e.tag == 'head':
 +            # TODO: find a better name and check if we have a string to boolean helper
 +            return
 +
 +        node_path = e.get('data-oe-xpath')
 +        if node_path is None:
 +            node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
 +        if branding and not (e.get('data-oe-model') or e.get('t-field')):
 +            e.attrib.update(branding)
 +            e.set('data-oe-xpath', node_path)
 +        if not e.get('data-oe-model'): return
 +
 +        # if a branded element contains branded elements distribute own
 +        # branding to children unless it's t-raw, then just remove branding
 +        # on current element
 +        if e.tag == 't' or 't-raw' in e.attrib or \
 +                any(self.is_node_branded(child) for child in e.iterdescendants()):
 +            distributed_branding = dict(
 +                (attribute, e.attrib.pop(attribute))
 +                for attribute in MOVABLE_BRANDING
 +                if e.get(attribute))
 +
 +            if 't-raw' not in e.attrib:
 +                # TODO: collections.Counter if remove p2.6 compat
 +                # running index by tag type, for XPath query generation
 +                indexes = collections.defaultdict(lambda: 0)
 +                for child in e.iterchildren(tag=etree.Element):
 +                    indexes[child.tag] += 1
 +                    self.distribute_branding(child, distributed_branding,
 +                                             parent_xpath=node_path,
 +                                             index_map=indexes)
 +
 +    def is_node_branded(self, node):
 +        """ Finds out whether a node is branded or qweb-active (bears a
 +        @data-oe-model or a @t-* *which is not t-field* as t-field does not
 +        section out views)
 +
 +        :param node: an etree-compatible element to test
 +        :type node: etree._Element
 +        :rtype: boolean
 +        """
 +        return any(
 +            (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
 +            for attr in node.attrib
 +        )
 +
 +    def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
 +        # TODO: this should be moved in a place before inheritance is applied
 +        #       but process() is only called on fields_view_get()
 +        Translations = self.pool['ir.translation']
 +        h = HTMLParser.HTMLParser()
 +        def get_trans(text):
 +            if not text or not text.strip():
 +                return None
 +            text = h.unescape(text.strip())
 +            if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
 +                return None
 +            # if text == 'Our Events':
 +            #     from pudb import set_trace;set_trace() ############################## Breakpoint ##############################
 +            return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
 +
 +        if arch.tag not in ['script']:
 +            text = get_trans(arch.text)
 +            if text:
 +                arch.text = arch.text.replace(arch.text.strip(), text)
 +            tail = get_trans(arch.tail)
 +            if tail:
 +                arch.tail = arch.tail.replace(arch.tail.strip(), tail)
 +
 +            for attr_name in ('title', 'alt', 'placeholder'):
 +                attr = get_trans(arch.get(attr_name))
 +                if attr:
 +                    arch.set(attr_name, attr)
 +            for node in arch.iterchildren("*"):
 +                self.translate_qweb(cr, uid, id_, node, lang, context)
 +        return arch
 +
 +    def render(self, cr, uid, id_or_xml_id, values, context=None):
 +        if not context:
 +            context = {}
 +        def loader(name):
 +            return self.read_template(cr, uid, name, context=context)
 +
 +        engine = qweb.QWebXml(loader=loader, undefined_handler=lambda key, v: None)
 +        return engine.render(id_or_xml_id, values)
 +
 +    # maybe used to print the workflow ?
  
      def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
          nodes=[]
                  'blank_nodes': blank_nodes,
                  'node_parent_field': _Model_Field,}
  
 +    def copy(self, cr, uid, id, default=None, context=None):
 +        if not default:
 +            default = {}
 +        default.update({
 +            'model_ids': [],
 +        })
 +        return super(view, self).copy(cr, uid, id, default, context=context)
 +
+     def _validate_custom_views(self, cr, uid, model):
+         """Validate architecture of custom views (= without xml id) for a given model.
+             This method is called at the end of registry update.
+         """
+         cr.execute("""SELECT max(v.id)
+                         FROM ir_ui_view v
+                    LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
+                        WHERE md.module IS NULL
+                          AND v.model = %s
+                     GROUP BY coalesce(v.inherit_id, v.id)
+                    """, (model,))
+         ids = map(itemgetter(0), cr.fetchall())
+         return self._check_xml(cr, uid, ids)
 +MOVABLE_BRANDING = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath']
 +
  class view_sc(osv.osv):
      _name = 'ir.ui.view_sc'
      _columns = {
 +# -*- encoding: utf-8 -*-
+ from functools import partial
 +from lxml import etree as ET
 +from lxml.builder import E
+ import unittest2
  
 -import openerp.tests.common as common
 +from openerp.tests import common
+ from openerp.osv.orm import except_orm
+ from openerp.tools import mute_logger
  
 +Field = E.field
 +
++
 +class TestNodeLocator(common.BaseCase):
 +    """
 +    The node locator returns None when it can not find a node, and the first
 +    match when it finds something (no jquery-style node sets)
 +    """
 +    def setUp(self):
 +        super(TestNodeLocator, self).setUp()
 +        self.Views = self.registry('ir.ui.view')
 +
 +    def test_no_match_xpath(self):
 +        """
 +        xpath simply uses the provided @expr pattern to find a node
 +        """
 +        node = self.Views.locate_node(
 +            E.root(E.foo(), E.bar(), E.baz()),
 +            E.xpath(expr="//qux"))
 +        self.assertIsNone(node)
 +
 +    def test_match_xpath(self):
 +        bar = E.bar()
 +        node = self.Views.locate_node(
 +            E.root(E.foo(), bar, E.baz()),
 +            E.xpath(expr="//bar"))
 +        self.assertIs(node, bar)
 +
 +
 +    def test_no_match_field(self):
 +        """
 +        A field spec will match by @name against all fields of the view
 +        """
 +        node = self.Views.locate_node(
 +            E.root(E.foo(), E.bar(), E.baz()),
 +            Field(name="qux"))
 +        self.assertIsNone(node)
 +
 +        node = self.Views.locate_node(
 +            E.root(Field(name="foo"), Field(name="bar"), Field(name="baz")),
 +            Field(name="qux"))
 +        self.assertIsNone(node)
 +
 +    def test_match_field(self):
 +        bar = Field(name="bar")
 +        node = self.Views.locate_node(
 +            E.root(Field(name="foo"), bar, Field(name="baz")),
 +            Field(name="bar"))
 +        self.assertIs(node, bar)
 +
 +
 +    def test_no_match_other(self):
 +        """
 +        Non-xpath non-fields are matched by node name first
 +        """
 +        node = self.Views.locate_node(
 +            E.root(E.foo(), E.bar(), E.baz()),
 +            E.qux())
 +        self.assertIsNone(node)
 +
 +    def test_match_other(self):
 +        bar = E.bar()
 +        node = self.Views.locate_node(
 +            E.root(E.foo(), bar, E.baz()),
 +            E.bar())
 +        self.assertIs(bar, node)
 +
 +    def test_attribute_mismatch(self):
 +        """
 +        Non-xpath non-field are filtered by matching attributes on spec and
 +        matched nodes
 +        """
 +        node = self.Views.locate_node(
 +            E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')),
 +            E.bar(attr='5'))
 +        self.assertIsNone(node)
 +
 +    def test_attribute_filter(self):
 +        match = E.bar(attr='2')
 +        node = self.Views.locate_node(
 +            E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))),
 +            E.bar(attr='2'))
 +        self.assertIs(node, match)
 +
 +    def test_version_mismatch(self):
 +        """
 +        A @version on the spec will be matched against the view's version
 +        """
 +        node = self.Views.locate_node(
 +            E.root(E.foo(attr='1'), version='4'),
 +            E.foo(attr='1', version='3'))
 +        self.assertIsNone(node)
 +
 +class TestViewInheritance(common.TransactionCase):
 +    def arch_for(self, name, view_type='form', parent=None):
 +        """ Generates a trivial view of the specified ``view_type``.
 +
 +        The generated view is empty but ``name`` is set as its root's ``@string``.
 +
 +        If ``parent`` is not falsy, generates an extension view (instead of
 +        a root view) replacing the parent's ``@string`` by ``name``
 +
 +        :param str name: ``@string`` value for the view root
 +        :param str view_type:
 +        :param bool parent:
 +        :return: generated arch
 +        :rtype: str
 +        """
 +        if not parent:
 +            element = E(view_type, string=name)
 +        else:
 +            element = E(view_type,
 +                E.attribute(name, name='string'),
 +                position='attributes'
 +            )
 +        return ET.tostring(element)
 +
 +    def makeView(self, name, parent=None, arch=None):
 +        """ Generates a basic ir.ui.view with the provided name, parent and arch.
 +
 +        If no parent is provided, the view is top-level.
 +
 +        If no arch is provided, generates one by calling :meth:`~.arch_for`.
 +
 +        :param str name:
 +        :param int parent: id of the parent view, if any
 +        :param str arch:
 +        :returns: the created view's id.
 +        :rtype: int
 +        """
 +        view_id = self.View.create(self.cr, self.uid, {
 +            'model': self.model,
 +            'name': name,
 +            'arch': arch or self.arch_for(name, parent=parent),
 +            'inherit_id': parent,
 +        })
 +        self.ids[name] = view_id
 +        return view_id
 +
 +    def setUp(self):
 +        super(TestViewInheritance, self).setUp()
 +
 +        self.model = 'dummy'
 +        self.View = self.registry('ir.ui.view')
 +        self._init = self.View.pool._init
 +        self.View.pool._init = False
 +        self.ids = {}
 +
 +        a = self.makeView("A")
 +        a1 = self.makeView("A1", a)
 +        a11 = self.makeView("A11", a1)
 +        self.makeView("A111", a11)
 +        self.makeView("A12", a1)
 +        a2 = self.makeView("A2", a)
 +        self.makeView("A21", a2)
 +        a22 = self.makeView("A22", a2)
 +        self.makeView("A221", a22)
 +
 +        b = self.makeView('B', arch=self.arch_for("B", 'tree'))
 +        self.makeView('B1', b, arch=self.arch_for("B1", 'tree', parent=b))
 +        c = self.makeView('C', arch=self.arch_for("C", 'tree'))
 +        self.View.write(self.cr, self.uid, c, {'priority': 1})
 +
 +    def tearDown(self):
 +        self.View.pool._init = self._init
 +        super(TestViewInheritance, self).tearDown()
 +
 +    def test_get_inheriting_views_arch(self):
 +        self.assertEqual(self.View.get_inheriting_views_arch(
 +            self.cr, self.uid, self.ids['A'], self.model), [
 +            (self.arch_for('A1', parent=True), self.ids['A1']),
 +            (self.arch_for('A2', parent=True), self.ids['A2']),
 +        ])
 +
 +        self.assertEqual(self.View.get_inheriting_views_arch(
 +            self.cr, self.uid, self.ids['A21'], self.model),
 +            [])
 +
 +        self.assertEqual(self.View.get_inheriting_views_arch(
 +            self.cr, self.uid, self.ids['A11'], self.model),
 +            [(self.arch_for('A111', parent=True), self.ids['A111'])])
 +
 +    def test_default_view(self):
 +        default = self.View.default_view(
 +            self.cr, self.uid, model=self.model, view_type='form')
 +        self.assertEqual(default, self.ids['A'])
 +
 +        default_tree = self.View.default_view(
 +            self.cr, self.uid, model=self.model, view_type='tree')
 +        self.assertEqual(default_tree, self.ids['C'])
 +
 +    def test_no_default_view(self):
 +        self.assertFalse(
 +            self.View.default_view(
 +                self.cr, self.uid, model='does.not.exist', view_type='form'))
 +
 +        self.assertFalse(
 +            self.View.default_view(
 +                self.cr, self.uid, model=self.model, view_type='graph'))
 +
 +class TestApplyInheritanceSpecs(common.TransactionCase):
 +    """ Applies a sequence of inheritance specification nodes to a base
 +    architecture. IO state parameters (cr, uid, model, context) are used for
 +    error reporting
 +
 +    The base architecture is altered in-place.
 +    """
 +    def setUp(self):
 +        super(TestApplyInheritanceSpecs, self).setUp()
 +        self.View = self.registry('ir.ui.view')
 +        self.base_arch = E.form(
 +            Field(name="target"),
 +            string="Title")
 +
 +    def test_replace(self):
 +        spec = Field(
 +                Field(name="replacement"),
 +                name="target", position="replace")
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(Field(name="replacement"), string="Title")))
 +
 +    def test_delete(self):
 +        spec = Field(name="target", position="replace")
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(string="Title")))
 +
 +    def test_insert_after(self):
 +        spec = Field(
 +                Field(name="inserted"),
 +                name="target", position="after")
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(
 +                Field(name="target"),
 +                Field(name="inserted"),
 +                string="Title"
 +            )))
 +
 +    def test_insert_before(self):
 +        spec = Field(
 +                Field(name="inserted"),
 +                name="target", position="before")
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(
 +                Field(name="inserted"),
 +                Field(name="target"),
 +                string="Title")))
 +
 +    def test_insert_inside(self):
 +        default = Field(Field(name="inserted"), name="target")
 +        spec = Field(Field(name="inserted 2"), name="target", position='inside')
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          default, None)
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(
 +                Field(
 +                    Field(name="inserted"),
 +                    Field(name="inserted 2"),
 +                    name="target"),
 +                string="Title")))
 +
 +    def test_unpack_data(self):
 +        spec = E.data(
 +                Field(Field(name="inserted 0"), name="target"),
 +                Field(Field(name="inserted 1"), name="target"),
 +                Field(Field(name="inserted 2"), name="target"),
 +                Field(Field(name="inserted 3"), name="target"),
 +            )
 +
 +        self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                          self.base_arch,
 +                                          spec, None)
 +
 +        self.assertEqual(
 +            ET.tostring(self.base_arch),
 +            ET.tostring(E.form(
 +                Field(
 +                    Field(name="inserted 0"),
 +                    Field(name="inserted 1"),
 +                    Field(name="inserted 2"),
 +                    Field(name="inserted 3"),
 +                    name="target"),
 +                string="Title")))
 +
 +    def test_invalid_position(self):
 +        spec = Field(
 +                Field(name="whoops"),
 +                name="target", position="serious_series")
 +
 +        with self.assertRaises(AttributeError):
 +            self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                              self.base_arch,
 +                                              spec, None)
 +
 +    def test_incorrect_version(self):
 +        # Version ignored on //field elements, so use something else
 +        arch = E.form(E.element(foo="42"))
 +        spec = E.element(
 +            Field(name="placeholder"),
 +            foo="42", version="7.0")
 +
 +        with self.assertRaises(AttributeError):
 +            self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                              arch,
 +                                              spec, None)
 +
 +    def test_target_not_found(self):
 +        spec = Field(name="targut")
 +
 +        with self.assertRaises(AttributeError):
 +            self.View.apply_inheritance_specs(self.cr, self.uid,
 +                                              self.base_arch,
 +                                              spec, None)
 +
 +class TestApplyInheritedArchs(common.TransactionCase):
 +    """ Applies a sequence of modificator archs to a base view
 +    """
 +
 +class TestViewCombined(common.TransactionCase):
 +    """
 +    Test fallback operations of View.read_combined:
 +    * defaults mapping
 +    * ?
 +    """
 +
 +class TestNoModel(common.TransactionCase):
 +    def test_create_view_nomodel(self):
 +        View = self.registry('ir.ui.view')
 +        view_id = View.create(self.cr, self.uid, {
 +            'name': 'dummy',
 +            'arch': '<form string=""/>',
 +            'inherit_id': False
 +        })
 +        fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model']
 +        [view] = View.read(self.cr, self.uid, [view_id], fields)
 +        self.assertEqual(view, {
 +            'id': view_id,
 +            'name': 'dummy',
 +            'arch': '<form string=""/>',
 +            'type': 'form',
 +            'priority': 16,
 +            'inherit_id': False,
 +            'model': False,
 +        })
 +
 +    arch = E.body(
 +        E.div(
 +            E.h1("Title"),
 +            id="header"),
 +        E.p("Welcome!"),
 +        E.div(
 +            E.hr(),
 +            E.p("Copyright copyrighter", {'class': 'legalese'}),
 +            id="footer"),
 +        {'class': "index"},)
 +    def test_fields_mess(self):
 +        """
 +        Try to call __view_look_dom_arch without a model provided, will need
 +        to be altered once it's broken up into sane components
 +        """
 +        View = self.registry('ir.ui.view')
 +
 +        sarch, fields = View.postprocess_and_fields(
 +            self.cr, self.uid, None, self.arch, None)
 +
 +        self.assertEqual(sarch, ET.tostring(self.arch, encoding='utf-8'))
 +        self.assertEqual(fields, {})
 +
 +    def test_mess_translation(self):
 +        """
 +        Test if translations work correctly without a model
 +        """
 +        View = self.registry('ir.ui.view')
 +        self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
 +        self.registry('ir.translation').create(self.cr, self.uid, {
 +            'name': '',
 +            'type': 'view',
 +            'lang': 'fr_FR',
 +            'src': 'Copyright copyrighter',
 +            'value': u"Copyrighter, tous droits réservés",
 +        })
 +        sarch, fields = View.postprocess_and_fields(
 +            self.cr, self.uid, None, self.arch, None, {'lang': 'fr_FR'})
 +        self.assertEqual(
 +            sarch,
 +            ET.tostring(self.arch, encoding='utf-8')
 +                .replace('Copyright copyrighter',
 +                         'Copyrighter, tous droits réservés'))
 +        self.assertEqual(fields, {})
 +
++
+ class test_views(common.TransactionCase):
+     @mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view')
+     def test_00_init_check_views(self):
+         Views = self.registry('ir.ui.view')
+         self.assertTrue(Views.pool._init)
+         error_msg = "The model name does not exist or the view architecture cannot be rendered"
+         # test arch check is call for views without xmlid during registry initialization
+         with self.assertRaisesRegexp(except_orm, error_msg):
+             Views.create(self.cr, self.uid, {
+                 'name': 'Test View #1',
+                 'model': 'ir.ui.view',
+                 'arch': """<?xml version="1.0"?>
+                             <tree>
+                               <field name="test_1"/>
+                             </tree>
+                         """,
+             })
+         # same for inherited views
+         with self.assertRaisesRegexp(except_orm, error_msg):
+             # Views.pudb = True
+             Views.create(self.cr, self.uid, {
+                 'name': 'Test View #2',
+                 'model': 'ir.ui.view',
+                 'inherit_id': self.browse_ref('base.view_view_tree').id,
+                 'arch': """<?xml version="1.0"?>
+                             <xpath expr="//field[@name='name']" position="after">
+                               <field name="test_2"/>
+                             </xpath>
+                         """,
+             })
+     def _insert_view(self, **kw):
+         """Insert view into database via a query to passtrough validation"""
+         kw.pop('id', None)
+         keys = sorted(kw.keys())
+         fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)
+         params = ','.join('%%(%s)s' % (k,) for k in keys)
+         query = 'INSERT INTO ir_ui_view(%s) VALUES(%s) RETURNING id' % (fields, params)
+         self.cr.execute(query, kw)
+         return self.cr.fetchone()[0]
+     def test_10_validate_custom_views(self):
+         Views = self.registry('ir.ui.view')
+         model = 'ir.actions.act_url'
+         validate = partial(Views._validate_custom_views, self.cr, self.uid, model)
+         # validation of a single view
+         vid = self._insert_view(**{
+             'name': 'base view',
+             'model': model,
+             'priority': 1,
+             'arch': """<?xml version="1.0"?>
+                         <tree string="view">
+                           <field name="url"/>
+                         </tree>
+                     """,
+         })
+         self.assertTrue(validate())     # single view
+         # validation of a inherited view
+         self._insert_view(**{
+             'name': 'inherited view',
+             'model': model,
+             'priority': 1,
+             'inherit_id': vid,
+             'arch': """<?xml version="1.0"?>
+                         <xpath expr="//field[@name='url']" position="before">
+                           <field name="name"/>
+                         </xpath>
+                     """,
+         })
+         self.assertTrue(validate())     # inherited view
+         # validation of a bad inherited view
+         self._insert_view(**{
+             'name': 'bad inherited view',
+             'model': model,
+             'priority': 2,
+             'inherit_id': vid,
+             'arch': """<?xml version="1.0"?>
+                         <xpath expr="//field[@name='url']" position="after">
+                           <field name="bad"/>
+                         </xpath>
+                     """,
+         })
+         with mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view'):
+             self.assertFalse(validate())    # bad inherited view
+ if __name__ == '__main__':
+     unittest2.main()
Simple merge
Simple merge
Simple merge