diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 1a75c08c695c32a03362ae5bbb93e9fb23e1e1a8..fc0edc24676cc497aedfd853ef860836ef5f24f3 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -141,8 +141,23 @@ class view(osv.osv):
         'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
         'create_date': fields.datetime('Create Date', readonly=True),
         'write_date': fields.datetime('Last Modification Date', readonly=True),
+
+        'mode': fields.selection(
+            [('primary', "Base view"), ('extension', "Extension View")],
+            string="View inheritance mode", required=True,
+            help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
+
+* if extension (default), if this view is requested the closest primary view
+  is looked up (via inherit_id), then all views inheriting from it with this
+  view's model are applied
+* if primary, the closest primary view is fully resolved (even if it uses a
+  different model than this one), then this view's inheritance specs
+  (<xpath/>) are applied, and the result is used as if it were this view's
+  actual arch.
+"""),
     }
     _defaults = {
+        'mode': 'primary',
         'priority': 16,
     }
     _order = "priority,name"
@@ -191,8 +206,14 @@ class view(osv.osv):
                         return False
         return True
 
+    _sql_constraints = [
+        ('inheritance_mode',
+         "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
+         "Invalid inheritance mode: if the mode is 'extension', the view must"
+         " extend an other view"),
+    ]
     _constraints = [
-        (_check_xml, 'Invalid view definition', ['arch'])
+        (_check_xml, 'Invalid view definition', ['arch']),
     ]
 
     def _auto_init(self, cr, context=None):
@@ -201,6 +222,12 @@ class view(osv.osv):
         if not cr.fetchone():
             cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
 
+    def _compute_defaults(self, cr, uid, values, context=None):
+        if 'inherit_id' in values:
+            values.setdefault(
+                'mode', 'extension' if values['inherit_id'] else 'primary')
+        return values
+
     def create(self, cr, uid, values, context=None):
         if 'type' not in values:
             if values.get('inherit_id'):
@@ -209,10 +236,13 @@ class view(osv.osv):
                 values['type'] = etree.fromstring(values['arch']).tag
 
         if not values.get('name'):
-            values['name'] = "%s %s" % (values['model'], values['type'])
+            values['name'] = "%s %s" % (values.get('model'), values['type'])
 
         self.read_template.clear_cache(self)
-        return super(view, self).create(cr, uid, values, context)
+        return super(view, self).create(
+            cr, uid,
+            self._compute_defaults(cr, uid, values, context=context),
+            context=context)
 
     def write(self, cr, uid, ids, vals, context=None):
         if not isinstance(ids, (list, tuple)):
@@ -227,7 +257,10 @@ class view(osv.osv):
             self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
 
         self.read_template.clear_cache(self)
-        ret = super(view, self).write(cr, uid, ids, vals, context)
+        ret = super(view, self).write(
+            cr, uid, ids,
+            self._compute_defaults(cr, uid, vals, context=context),
+            context)
         return ret
 
     def copy(self, cr, uid, id, default=None, context=None):
@@ -241,7 +274,7 @@ class view(osv.osv):
     # default view selection
     def default_view(self, cr, uid, model, view_type, context=None):
         """ Fetches the default view for the provided (model, view_type) pair:
-         view with no parent (inherit_id=Fase) with the lowest priority.
+         primary view with the lowest priority.
 
         :param str model:
         :param int view_type:
@@ -251,7 +284,7 @@ class view(osv.osv):
         domain = [
             ['model', '=', model],
             ['type', '=', view_type],
-            ['inherit_id', '=', False],
+            ['mode', '=', 'primary'],
         ]
         ids = self.search(cr, uid, domain, limit=1, context=context)
         if not ids:
@@ -278,15 +311,18 @@ class view(osv.osv):
         user = self.pool['res.users'].browse(cr, 1, uid, context=context)
         user_groups = frozenset(user.groups_id or ())
 
-        check_view_ids = context and context.get('check_view_ids') or (0,)
-        conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
+        conditions = [
+            ['inherit_id', '=', view_id],
+            ['model', '=', model],
+            ['mode', '=', 'extension'],
+        ]
         if self.pool._init:
             # Module init currently in progress, only consider views from
             # modules whose code is already loaded
             conditions.extend([
                 '|',
                 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
-                ['id', 'in', check_view_ids],
+                ['id', 'in', context and context.get('check_view_ids') or (0,)],
             ])
         view_ids = self.search(cr, uid, conditions, context=context)
 
@@ -442,7 +478,7 @@ class view(osv.osv):
         if context is None: context = {}
         if root_id is None:
             root_id = source_id
-        sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
+        sql_inherit = self.pool['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'):
@@ -465,7 +501,7 @@ class view(osv.osv):
 
         # if view_id is not a root view, climb back to the top.
         base = v = self.browse(cr, uid, view_id, context=context)
-        while v.inherit_id:
+        while v.mode != 'primary':
             v = v.inherit_id
         root_id = v.id
 
@@ -475,7 +511,16 @@ class view(osv.osv):
 
         # read the view arch
         [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
-        arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
+        view_arch = etree.fromstring(view['arch'].encode('utf-8'))
+        if not v.inherit_id:
+            arch_tree = view_arch
+        else:
+            parent_view = self.read_combined(
+                cr, uid, v.inherit_id.id, fields=fields, context=context)
+            arch_tree = etree.fromstring(parent_view['arch'])
+            self.apply_inheritance_specs(
+                cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
+
 
         if context.get('inherit_branding'):
             arch_tree.attrib.update({
diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py
index feb22eb2be9a17226704b3d0a13fd32618404450..07cd4715b83ec9809339ffef4875d82d479836e7 100644
--- a/openerp/addons/base/tests/test_views.py
+++ b/openerp/addons/base/tests/test_views.py
@@ -1,12 +1,16 @@
 # -*- encoding: utf-8 -*-
 from functools import partial
+import itertools
 
 import unittest2
 
 from lxml import etree as ET
 from lxml.builder import E
 
+from psycopg2 import IntegrityError
+
 from openerp.tests import common
+import openerp.tools
 
 Field = E.field
 
@@ -14,9 +18,15 @@ class ViewCase(common.TransactionCase):
     def setUp(self):
         super(ViewCase, self).setUp()
         self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual)
+        self.Views = self.registry('ir.ui.view')
+
+    def browse(self, id, context=None):
+        return self.Views.browse(self.cr, self.uid, id, context=context)
+    def create(self, value, context=None):
+        return self.Views.create(self.cr, self.uid, value, context=context)
 
     def assertTreesEqual(self, n1, n2, msg=None):
-        self.assertEqual(n1.tag, n2.tag)
+        self.assertEqual(n1.tag, n2.tag, msg)
         self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)
         self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)
 
@@ -24,8 +34,8 @@ class ViewCase(common.TransactionCase):
         # equality (!?!?!?!)
         self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
 
-        for c1, c2 in zip(n1, n2):
-            self.assertTreesEqual(c1, c2, msg)
+        for c1, c2 in itertools.izip_longest(n1, n2):
+            self.assertEqual(c1, c2, msg)
 
 
 class TestNodeLocator(common.TransactionCase):
@@ -374,13 +384,6 @@ class TestApplyInheritedArchs(ViewCase):
     """ Applies a sequence of modificator archs to a base view
     """
 
-class TestViewCombined(ViewCase):
-    """
-    Test fallback operations of View.read_combined:
-    * defaults mapping
-    * ?
-    """
-
 class TestNoModel(ViewCase):
     def test_create_view_nomodel(self):
         View = self.registry('ir.ui.view')
@@ -628,6 +631,7 @@ class test_views(ViewCase):
     def _insert_view(self, **kw):
         """Insert view into database via a query to passtrough validation"""
         kw.pop('id', None)
+        kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary')
 
         keys = sorted(kw.keys())
         fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)
@@ -805,6 +809,273 @@ class test_views(ViewCase):
                 string="Replacement title", version="7.0"
             ))
 
+class ViewModeField(ViewCase):
+    """
+    This should probably, eventually, be folded back into other test case
+    classes, integrating the test (or not) of the mode field to regular cases
+    """
+
+    def testModeImplicitValue(self):
+        """ mode is auto-generated from inherit_id:
+        * inherit_id -> mode=extension
+        * not inherit_id -> mode=primary
+        """
+        view = self.browse(self.create({
+            'inherit_id': None,
+            'arch': '<qweb/>'
+        }))
+        self.assertEqual(view.mode, 'primary')
+
+        view2 = self.browse(self.create({
+            'inherit_id': view.id,
+            'arch': '<qweb/>'
+        }))
+        self.assertEqual(view2.mode, 'extension')
+
+    @openerp.tools.mute_logger('openerp.sql_db')
+    def testModeExplicit(self):
+        view = self.browse(self.create({
+            'inherit_id': None,
+            'arch': '<qweb/>'
+        }))
+        view2 = self.browse(self.create({
+            'inherit_id': view.id,
+            'mode': 'primary',
+            'arch': '<qweb/>'
+        }))
+        self.assertEqual(view.mode, 'primary')
+
+        with self.assertRaises(IntegrityError):
+            self.create({
+                'inherit_id': None,
+                'mode': 'extension',
+                'arch': '<qweb/>'
+            })
+
+    @openerp.tools.mute_logger('openerp.sql_db')
+    def testPurePrimaryToExtension(self):
+        """
+        A primary view with inherit_id=None can't be converted to extension
+        """
+        view_pure_primary = self.browse(self.create({
+            'inherit_id': None,
+            'arch': '<qweb/>'
+        }))
+        with self.assertRaises(IntegrityError):
+            view_pure_primary.write({'mode': 'extension'})
+
+    def testInheritPrimaryToExtension(self):
+        """
+        A primary view with an inherit_id can be converted to extension
+        """
+        base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
+        view = self.browse(self.create({
+            'inherit_id': base,
+            'mode': 'primary',
+            'arch': '<qweb/>'
+        }))
+
+        view.write({'mode': 'extension'})
+
+    def testDefaultExtensionToPrimary(self):
+        """
+        An extension view can be converted to primary
+        """
+        base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
+        view = self.browse(self.create({
+            'inherit_id': base,
+            'arch': '<qweb/>'
+        }))
+
+        view.write({'mode': 'primary'})
+
+class TestDefaultView(ViewCase):
+    def testDefaultViewBase(self):
+        self.create({
+            'inherit_id': False,
+            'priority': 10,
+            'mode': 'primary',
+            'arch': '<qweb/>',
+        })
+        v2 = self.create({
+            'inherit_id': False,
+            'priority': 1,
+            'mode': 'primary',
+            'arch': '<qweb/>',
+        })
+
+        default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
+        self.assertEqual(
+            default, v2,
+            "default_view should get the view with the lowest priority for "
+            "a (model, view_type) pair"
+        )
+
+    def testDefaultViewPrimary(self):
+        v1 = self.create({
+            'inherit_id': False,
+            'priority': 10,
+            'mode': 'primary',
+            'arch': '<qweb/>',
+        })
+        self.create({
+            'inherit_id': False,
+            'priority': 5,
+            'mode': 'primary',
+            'arch': '<qweb/>',
+        })
+        v3 = self.create({
+            'inherit_id': v1,
+            'priority': 1,
+            'mode': 'primary',
+            'arch': '<qweb/>',
+        })
+
+        default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
+        self.assertEqual(
+            default, v3,
+            "default_view should get the view with the lowest priority for "
+            "a (model, view_type) pair in all the primary tables"
+        )
+
+class TestViewCombined(ViewCase):
+    """
+    * When asked for a view, instead of looking for the closest parent with
+      inherit_id=False look for mode=primary
+    * If root.inherit_id, resolve the arch for root.inherit_id (?using which
+      model?), then apply root's inheritance specs to it
+    * Apply inheriting views on top
+    """
+
+    def setUp(self):
+        super(TestViewCombined, self).setUp()
+
+        self.a1 = self.create({
+            'model': 'a',
+            'arch': '<qweb><a1/></qweb>'
+        })
+        self.a2 = self.create({
+            'model': 'a',
+            'inherit_id': self.a1,
+            'priority': 5,
+            'arch': '<xpath expr="//a1" position="after"><a2/></xpath>'
+        })
+        self.a3 = self.create({
+            'model': 'a',
+            'inherit_id': self.a1,
+            'arch': '<xpath expr="//a1" position="after"><a3/></xpath>'
+        })
+        # mode=primary should be an inheritance boundary in both direction,
+        # even within a model it should not extend the parent
+        self.a4 = self.create({
+            'model': 'a',
+            'inherit_id': self.a1,
+            'mode': 'primary',
+            'arch': '<xpath expr="//a1" position="after"><a4/></xpath>',
+        })
+
+        self.b1 = self.create({
+            'model': 'b',
+            'inherit_id': self.a3,
+            'mode': 'primary',
+            'arch': '<xpath expr="//a1" position="after"><b1/></xpath>'
+        })
+        self.b2 = self.create({
+            'model': 'b',
+            'inherit_id': self.b1,
+            'arch': '<xpath expr="//a1" position="after"><b2/></xpath>'
+        })
+
+        self.c1 = self.create({
+            'model': 'c',
+            'inherit_id': self.a1,
+            'mode': 'primary',
+            'arch': '<xpath expr="//a1" position="after"><c1/></xpath>'
+        })
+        self.c2 = self.create({
+            'model': 'c',
+            'inherit_id': self.c1,
+            'priority': 5,
+            'arch': '<xpath expr="//a1" position="after"><c2/></xpath>'
+        })
+        self.c3 = self.create({
+            'model': 'c',
+            'inherit_id': self.c2,
+            'priority': 10,
+            'arch': '<xpath expr="//a1" position="after"><c3/></xpath>'
+        })
+
+        self.d1 = self.create({
+            'model': 'd',
+            'inherit_id': self.b1,
+            'mode': 'primary',
+            'arch': '<xpath expr="//a1" position="after"><d1/></xpath>'
+        })
+
+    def read_combined(self, id):
+        return self.Views.read_combined(
+            self.cr, self.uid,
+            id, ['arch'],
+            context={'check_view_ids': self.Views.search(self.cr, self.uid, [])}
+        )
+
+    def test_basic_read(self):
+        arch = self.read_combined(self.a1)['arch']
+        self.assertEqual(
+            ET.fromstring(arch),
+            E.qweb(
+                E.a1(),
+                E.a3(),
+                E.a2(),
+            ), arch)
+
+    def test_read_from_child(self):
+        arch = self.read_combined(self.a3)['arch']
+        self.assertEqual(
+            ET.fromstring(arch),
+            E.qweb(
+                E.a1(),
+                E.a3(),
+                E.a2(),
+            ), arch)
+
+    def test_read_from_child_primary(self):
+        arch = self.read_combined(self.a4)['arch']
+        self.assertEqual(
+            ET.fromstring(arch),
+            E.qweb(
+                E.a1(),
+                E.a4(),
+                E.a3(),
+                E.a2(),
+            ), arch)
+
+    def test_cross_model_simple(self):
+        arch = self.read_combined(self.c2)['arch']
+        self.assertEqual(
+            ET.fromstring(arch),
+            E.qweb(
+                E.a1(),
+                E.c3(),
+                E.c2(),
+                E.c1(),
+                E.a3(),
+                E.a2(),
+            ), arch)
+
+    def test_cross_model_double(self):
+        arch = self.read_combined(self.d1)['arch']
+        self.assertEqual(
+            ET.fromstring(arch),
+            E.qweb(
+                E.a1(),
+                E.d1(),
+                E.b2(),
+                E.b1(),
+                E.a3(),
+                E.a2(),
+            ), arch)
+
 class TestXPathExtentions(common.BaseCase):
     def test_hasclass(self):
         tree = E.node(
diff --git a/openerp/import_xml.rng b/openerp/import_xml.rng
index 04922268e27521371aeae0773604943ae214859e..8584267a2451e5b860b0eb10aada8e402dc446af 100644
--- a/openerp/import_xml.rng
+++ b/openerp/import_xml.rng
@@ -217,7 +217,14 @@
             <rng:optional><rng:attribute name="priority"/></rng:optional>
             <rng:choice>
                 <rng:group>
-                    <rng:optional><rng:attribute name="inherit_id"/></rng:optional>
+                    <rng:optional>
+                        <rng:attribute name="inherit_id"/>
+                        <rng:optional>
+                            <rng:attribute name="primary">
+                                <rng:value>True</rng:value>
+                            </rng:attribute>
+                        </rng:optional>
+                    </rng:optional>
                     <rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>
                     <rng:optional><rng:attribute name="groups"/></rng:optional>
                 </rng:group>
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index c84a961d2d3fb3dc811c04d61a12ed9c8e47d970..76b4d7ee3ffa93d8454db22222dca52b8e64449c 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -729,7 +729,6 @@ class BaseModel(object):
     _all_columns = {}
 
     _table = None
-    _invalids = set()
     _log_create = False
     _sql_constraints = []
     _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
@@ -1543,9 +1542,6 @@ class BaseModel(object):
 
             yield dbid, xid, converted, dict(extras, record=stream.index)
 
-    def get_invalid_fields(self, cr, uid):
-        return list(self._invalids)
-
     def _validate(self, cr, uid, ids, context=None):
         context = context or {}
         lng = context.get('lang')
@@ -1566,12 +1562,9 @@ class BaseModel(object):
                 # Check presence of __call__ directly instead of using
                 # callable() because it will be deprecated as of Python 3.0
                 if hasattr(msg, '__call__'):
-                    tmp_msg = msg(self, cr, uid, ids, context=context)
-                    if isinstance(tmp_msg, tuple):
-                        tmp_msg, params = tmp_msg
-                        translated_msg = tmp_msg % params
-                    else:
-                        translated_msg = tmp_msg
+                    translated_msg = msg(self, cr, uid, ids, context=context)
+                    if isinstance(translated_msg, tuple):
+                        translated_msg = translated_msg[0] % translated_msg[1]
                 else:
                     translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
                 if extra_error:
@@ -1579,11 +1572,8 @@ class BaseModel(object):
                 error_msgs.append(
                         _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
                 )
-                self._invalids.update(fields)
         if error_msgs:
             raise except_orm('ValidateError', '\n'.join(error_msgs))
-        else:
-            self._invalids.clear()
 
     def default_get(self, cr, uid, fields_list, context=None):
         """
diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py
index af6f3431229b08947e21d64e2090fa5e098d9874..8993002b1ece02ceb0ba63d72e5e9bc3ffdf9eb0 100644
--- a/openerp/tools/convert.py
+++ b/openerp/tools/convert.py
@@ -898,6 +898,8 @@ form: module.record_id""" % (xml_id,)
             record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
         if el.attrib.pop('page', None) == 'True':
             record.append(Field(name="page", eval="True"))
+        if el.get('primary') == 'True':
+            record.append(Field('primary', name='mode'))
 
         return self._tag_record(cr, record, data_node)