diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index c19364fa7a99da5958c89fccdf57c124241f3cad..ef9273bb48498d7229157e316b431a9b10b290c2 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -689,6 +689,7 @@ ActionManager.include({
                 };
             }
             var options = {on_close: ev.data.on_closed};
+            action.flags = _.extend({}, action.flags, {withSearchPanel: false});
             return self.doAction(action, options).then(ev.data.on_success, ev.data.on_fail);
         });
     },
@@ -711,8 +712,10 @@ ActionManager.include({
             // only switch to the requested view if the controller that
             // triggered the request is the current controller
             var action = this.actions[currentController.actionID];
+            var currentControllerState = currentController.widget.exportState();
+            action.controllerState = _.extend({}, action.controllerState, currentControllerState);
             var options = {
-                controllerState: currentController.widget.exportState(),
+                controllerState: action.controllerState,
                 currentId: ev.data.res_id,
             };
             if (ev.data.mode) {
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index d8cc6baa8f5a4028473fecd5161f9071bcd84bb6..58a011599d1946dc29f98be6a898eaa2f23e3652 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -28,6 +28,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         open_record: '_onOpenRecord',
         search: '_onSearch',
         switch_view: '_onSwitchView',
+        search_panel_domain_updated: '_onSearchPanelDomainUpdated',
     },
     events: {
         'click a[type="action"]': '_onActionClicked',
@@ -58,6 +59,11 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         this.viewType = params.viewType;
         // use a DropPrevious to correctly handle concurrent updates
         this.dp = new concurrency.DropPrevious();
+
+        // the following attributes are used when there is a searchPanel
+        this._searchPanel = params.searchPanel;
+        this.controlPanelDomain = params.controlPanelDomain || [];
+        this.searchPanelDomain = this._searchPanel ? this._searchPanel.getDomain() : [];
     },
     /**
      * Simply renders and updates the url.
@@ -66,6 +72,11 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      */
     start: function () {
         var self = this;
+        if (this._searchPanel) {
+            this.$('.o_content')
+                .addClass('o_controller_with_searchpanel')
+                .prepend(this._searchPanel.$el);
+        }
 
         this.$el.addClass('o_view_controller');
 
@@ -123,7 +134,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      */
     canBeRemoved: function () {
         // AAB: get rid of this option when on_hashchange mechanism is improved
-        return this.discardChanges(undefined, { readonlyIfRealDiscard: true });
+        return this.discardChanges(undefined, {readonlyIfRealDiscard: true});
     },
     /**
      * Discards the changes made on the record associated to the given ID, or
@@ -154,6 +165,9 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         if (this._controlPanel) {
             state.cpState = this._controlPanel.exportState();
         }
+        if (this._searchPanel) {
+            state.spState = this._searchPanel.exportState();
+        }
         return state;
     },
     /**
@@ -180,20 +194,33 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      * @param {Object} [params] This object will simply be given to the update
      * @returns {Promise}
      */
-    reload: function (params) {
+    reload: async function (params) {
         params = params || {};
-        var self = this;
-        var def;
+        var searchPanelUpdateProm;
         var controllerState = params.controllerState || {};
         var cpState = controllerState.cpState;
         if (this._controlPanel && cpState) {
-            def = this._controlPanel.importState(cpState).then(function (searchQuery) {
+            await this._controlPanel.importState(cpState).then(function (searchQuery) {
                 params = _.extend({}, params, searchQuery);
             });
         }
-        return Promise.resolve(def).then(function () {
-            return self.update(params, {});
-        });
+        var postponeRendering = false;
+        if (this._searchPanel) {
+            this.controlPanelDomain = params.domain || this.controlPanelDomain;
+            if (controllerState.spState) {
+                this._searchPanel.importState(controllerState.spState);
+                this.searchPanelDomain = this._searchPanel.getDomain();
+            } else {
+                searchPanelUpdateProm =  this._searchPanel.update({searchDomain: this._getSearchDomain()});
+                postponeRendering = !params.noRender;
+                params.noRender = true; // wait for searchpanel to be ready to render
+            }
+            params.domain = this.controlPanelDomain.concat(this.searchPanelDomain);
+        }
+        await Promise.all([this.update(params, {}), searchPanelUpdateProm]);
+        if (postponeRendering) {
+            return this.renderer._render();
+        }
     },
     /**
      * For views that require a pager, this method will be called to allow the
@@ -266,6 +293,18 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
     // Private
     //--------------------------------------------------------------------------
 
+    /**
+     * Return the current search domain. This is the searchDomain used to update
+     * the searchpanel. It returns the domain coming from the controlpanel. This
+     * function can be overridden to add sub-domains coming from other parts of
+     * the interface.
+     *
+     * @private
+     * @returns {Array[]}
+     */
+    _getSearchDomain: function () {
+        return this.controlPanelDomain;
+    },
     /**
      * This method is the way a view can notifies the outside world that
      * something has changed.  The main use for this is to update the url, for
@@ -302,7 +341,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         if (this.bannerRoute !== undefined) {
             var self = this;
             return this.dp
-                .add(this._rpc({ route: this.bannerRoute }))
+                .add(this._rpc({route: this.bannerRoute}))
                 .then(function (response) {
                     if (!response.html) {
                         self.$el.removeClass('o_has_banner');
@@ -370,7 +409,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      */
     _renderSwitchButtons: function () {
         var self = this;
-        var views = _.filter(this.actionViews, { multiRecord: this.isMultiRecord });
+        var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});
 
         if (views.length <= 1) {
             return $();
@@ -389,7 +428,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button');
         $switchButtonsFiltered.click(_.debounce(function (event) {
             var viewType = $(event.target).data('view-type');
-            self.trigger_up('switch_view', { view_type: viewType });
+            self.trigger_up('switch_view', {view_type: viewType});
         }, 200, true));
 
         // set active view's icon as view switcher button's icon in mobile
@@ -519,8 +558,8 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      * @private
      * @param {OdooEvent} ev
      */
-    _onNavigationMove : function (ev) {
-        switch(ev.data.direction) {
+    _onNavigationMove: function (ev) {
+        switch (ev.data.direction) {
             case 'down' :
                 ev.stopPropagation();
                 this.giveFocus();
@@ -543,7 +582,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
      */
     _onOpenRecord: function (ev) {
         ev.stopPropagation();
-        var record = this.model.get(ev.data.id, { raw: true });
+        var record = this.model.get(ev.data.id, {raw: true});
         this.trigger_up('switch_view', {
             view_type: 'form',
             res_id: record.res_id,
@@ -565,6 +604,15 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
         ev.stopPropagation();
         this.reload(_.extend({offset: 0}, ev.data));
     },
+    /**
+     * @private
+     * @param {OdooEvent} ev
+     * @param {Array[]} ev.data.domain the current domain of the searchPanel
+     */
+    _onSearchPanelDomainUpdated: function (ev) {
+        this.searchPanelDomain = ev.data.domain;
+        this.reload({offset: 0});
+    },
     /**
      * Intercepts the 'switch_view' event to add the controllerID into the data,
      * and lets the event bubble up.
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 724b63bae7920ae71de3f8feb8341d7d15b2e144..afaf275ab5b8a8af42a81aa402aeee57e190f443 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -21,6 +21,7 @@ return mvc.Renderer.extend({
         this._super.apply(this, arguments);
         this.arch = params.arch;
         this.noContentHelp = params.noContentHelp;
+        this.withSearchPanel = params.withSearchPanel;
     },
     /**
      * The rendering is asynchronous. The start
@@ -30,6 +31,9 @@ return mvc.Renderer.extend({
      */
     start: function () {
         this.$el.addClass(this.arch.attrs.class);
+        if (this.withSearchPanel) {
+            this.$el.addClass('o_renderer_with_searchpanel');
+        }
         return Promise.all([this._render(), this._super()]);
     },
     /**
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index dbb1b3e557cff539b1d70d2ec6f78f7eeccd029c..fc8e73136d8dbd6a1320b2c9cb47a7951bd23b9b 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -28,6 +28,7 @@ var AbstractRenderer = require('web.AbstractRenderer');
 var AbstractController = require('web.AbstractController');
 var ControlPanelView = require('web.ControlPanelView');
 var mvc = require('web.mvc');
+var SearchPanel = require('web.SearchPanel');
 var viewUtils = require('web.viewUtils');
 
 var Factory = mvc.Factory;
@@ -50,11 +51,14 @@ var AbstractView = Factory.extend({
     searchMenuTypes: ['filter', 'groupBy', 'favorite'],
     // determines if a control panel should be instantiated
     withControlPanel: true,
+    // determines if a search panel could be instantiated
+    withSearchPanel: true,
     // determines the MVC components to use
     config: _.extend({}, Factory.prototype.config, {
         Model: AbstractModel,
         Renderer: AbstractRenderer,
         Controller: AbstractController,
+        SearchPanel: SearchPanel,
     }),
 
     /**
@@ -75,7 +79,7 @@ var AbstractView = Factory.extend({
      * @param {string} [params.controllerID]
      * @param {number} [params.count]
      * @param {number} [params.currentId]
-     * @param {string} [params.controllerState]
+     * @param {Object} [params.controllerState]
      * @param {string} [params.displayName]
      * @param {Array[]} [params.domain=[]]
      * @param {Object[]} [params.dynamicFilters] transmitted to the
@@ -88,6 +92,7 @@ var AbstractView = Factory.extend({
      * @param {string[]} [params.searchQuery.groupBy=[]]
      * @param {Object} [params.userContext={}]
      * @param {boolean} [params.withControlPanel=true]
+     * @param {boolean} [params.withSearchPanel=true]
      */
     init: function (viewInfo, params) {
         this._super.apply(this, arguments);
@@ -110,6 +115,7 @@ var AbstractView = Factory.extend({
         this.fields = this.fieldsView.viewFields;
         this.userContext = params.userContext || {};
         this.withControlPanel = this.withControlPanel && params.withControlPanel;
+        this.withSearchPanel = this.withSearchPanel && this.multi_record && params.withSearchPanel;
 
         // the boolean parameter 'isEmbedded' determines if the view should be
         // considered as a subview. For now this is only used by the graph
@@ -180,6 +186,9 @@ var AbstractView = Factory.extend({
             withBreadcrumbs: params.withBreadcrumbs,
             withSearchBar: params.withSearchBar,
         };
+        this.searchPanelParams = {
+            state: controllerState.spState,
+        };
     },
 
     //--------------------------------------------------------------------------
@@ -191,20 +200,32 @@ var AbstractView = Factory.extend({
      */
     getController: function (parent) {
         var self = this;
-        var def;
-        if (this.withControlPanel) {
-            def = this._createControlPanel(parent);
+        var cpDef = this.withControlPanel && this._createControlPanel(parent);
+        var spDef;
+        if (this.withSearchPanel) {
+            var spProto = this.config.SearchPanel.prototype;
+            var viewInfo = this.controlPanelParams.viewInfo;
+            var sections = spProto.computeSearchPanelParams(viewInfo, this.viewType);
+            if (sections) {
+                this.searchPanelParams.sections = sections;
+                this.rendererParams.withSearchPanel = true;
+                spDef = Promise.resolve(cpDef).then(this._createSearchPanel.bind(this, parent));
+            }
         }
+
         var _super = this._super.bind(this);
-        return Promise.resolve(def).then(function (controlPanel) {
+        return Promise.all([cpDef, spDef]).then(function ([controlPanel, searchPanel]) {
             // get the parent of the model if it already exists, as _super will
             // set the new controller as parent, which we don't want
             var modelParent = self.model && self.model.getParent();
-            var prom =  _super(parent);
+            var prom = _super(parent);
             prom.then(function (controller) {
                 if (controlPanel) {
                     controlPanel.setParent(controller);
                 }
+                if (searchPanel) {
+                    searchPanel.setParent(controller);
+                }
                 if (modelParent) {
                     // if we already add a model, restore its parent
                     self.model.setParent(modelParent);
@@ -243,7 +264,8 @@ var AbstractView = Factory.extend({
      *
      * @private
      * @param {Widget} parent
-     * @returns {ControlPanelController}
+     * @returns {Promise<ControlPanelController>} resolved when the controlPanel
+     *   is ready
      */
     _createControlPanel: function (parent) {
         var self = this;
@@ -256,6 +278,36 @@ var AbstractView = Factory.extend({
             });
         });
     },
+    /**
+     * @private
+     * @param {Widget} parent
+     * @returns {Promise<SearchPanel>} resolved when the searchPanel is ready
+     */
+    _createSearchPanel: async function (parent) {
+        var defaultValues = {};
+        Object.keys(this.loadParams.context).forEach((key) => {
+            let match = /^searchpanel_default_(.*)$/.exec(key);
+            if (match) {
+                defaultValues[match[1]] = this.loadParams.context[key];
+            }
+        });
+        var controlPanelDomain = this.loadParams.domain;
+        var searchPanel = new this.config.SearchPanel(parent, {
+            defaultValues: defaultValues,
+            fields: this.fields,
+            model: this.loadParams.modelName,
+            searchDomain: controlPanelDomain,
+            sections: this.searchPanelParams.sections,
+            state: this.searchPanelParams.state,
+        });
+        this.controllerParams.searchPanel = searchPanel;
+        this.controllerParams.controlPanelDomain = controlPanelDomain;
+        await searchPanel.appendTo(document.createDocumentFragment());
+
+        var searchPanelDomain = searchPanel.getDomain();
+        this.loadParams.domain = controlPanelDomain.concat(searchPanelDomain);
+        return searchPanel;
+    },
     /**
      * @private
      * @param {Object} [action]
@@ -294,6 +346,7 @@ var AbstractView = Factory.extend({
             withBreadcrumbs: 'no_breadcrumbs' in context ? !context.no_breadcrumbs : true,
             withControlPanel: this.withControlPanel,
             withSearchBar: inline ? false : this.withSearchBar,
+            withSearchPanel: this.withSearchPanel,
         };
     },
     /**
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 008bbcf87756342468d5057d76ef00f3bfa1b85d..fde334d79d0da482278e82ebeea96692f145a494 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -52,8 +52,8 @@ var BasicView = AbstractView.extend({
         this.loadParams.fieldsInfo = this.fieldsInfo;
         this.loadParams.fields = this.fields;
         this.loadParams.limit = parseInt(this.arch.attrs.limit, 10) || params.limit;
-        this.loadParams.viewType = this.viewType;
         this.loadParams.parentID = params.parentID;
+        this.loadParams.viewType = this.viewType;
         this.recordID = params.recordID;
 
         this.model = params.model;
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 90db648ffd36fa477a0320dd0f675c49ec42c95a..2ee927a4af09fe3e519d03b22dc3217db59ff494 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -23,11 +23,11 @@ var CalendarView = AbstractView.extend({
     icon: 'fa-calendar',
     jsLibs: ['/web/static/lib/fullcalendar/js/fullcalendar.js'],
     cssLibs: ['/web/static/lib/fullcalendar/css/fullcalendar.css'],
-    config: {
+    config: _.extend({}, AbstractView.prototype.config, {
         Model: CalendarModel,
         Controller: CalendarController,
         Renderer: CalendarRenderer,
-    },
+    }),
     viewType: 'calendar',
     searchMenuTypes: ['filter', 'favorite'],
 
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_view.js b/addons/web/static/src/js/views/control_panel/control_panel_view.js
index 0a140e1f90107df8b9d8f34cfed6fe1dbfb9531a..c15840714db14b4c5948deb257d39beb9fb7c106 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_view.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_view.js
@@ -201,7 +201,15 @@ var ControlPanelView = Factory.extend({
      */
     _parseSearchArch: function (arch) {
         var self = this;
-        var preFilters = _.flatten(arch.children.map(function (child) {
+        // a searchview arch may contain a 'searchpanel' node, but this isn't
+        // the concern of the ControlPanelView (the SearchPanel will handle it).
+        // Ideally, this code should whitelist the tags to take into account
+        // instead of blacklisting the others, but with the current (messy)
+        // structure of a searchview arch, it's way simpler to do it that way.
+        var children = arch.children.filter(function (child) {
+            return child.tag !== 'searchpanel';
+        });
+        var preFilters = _.flatten(children.map(function (child) {
             return child.tag !== 'group' ?
                     self._evalArchChild(child) :
                     child.children.map(self._evalArchChild);
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index 4a91bd67f19950dfa32c25421c6e8a45e39eb5ee..f8125675d4e12d2abadd0d7015ba0af547968779 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -25,11 +25,11 @@ var GraphView = AbstractView.extend({
     jsLibs: [
         '/web/static/lib/Chart/Chart.js',
     ],
-    config: {
+    config: _.extend({}, AbstractView.prototype.config, {
         Model: GraphModel,
         Controller: Controller,
         Renderer: GraphRenderer,
-    },
+    }),
     viewType: 'graph',
     searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
 
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index bf39906b8e506b8aa38f2c73ce7106a29f2b4e63..a6816b08b3921bdec3740379caf73ed6c3c78d2e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -33,7 +33,6 @@ var KanbanController = BasicController.extend({
         kanban_load_records: '_onLoadColumnRecords',
         column_toggle_fold: '_onToggleColumn',
         kanban_column_records_toggle_active: '_onToggleActiveRecords',
-        search_panel_domain_updated: '_onSearchPanelDomainUpdated',
     }),
     events: _.extend({}, BasicController.prototype.events, {
         click: '_onClick',
@@ -52,22 +51,6 @@ var KanbanController = BasicController.extend({
         this.on_create = params.on_create;
         this.hasButtons = params.hasButtons;
         this.quickCreateEnabled = params.quickCreateEnabled;
-
-        // the following attributes are used when there is a searchPanel
-        this._searchPanel = params.searchPanel;
-        this.controlPanelDomain = params.controlPanelDomain || [];
-        this.searchPanelDomain = this._searchPanel ? this._searchPanel.getDomain() : [];
-    },
-    /**
-     * @override
-     */
-    start: function () {
-        if (this._searchPanel) {
-            this.$('.o_content')
-                .addClass('o_kanban_with_searchpanel')
-                .prepend(this._searchPanel.$el);
-        }
-        return this._super.apply(this, arguments);
     },
 
     //--------------------------------------------------------------------------
@@ -91,32 +74,6 @@ var KanbanController = BasicController.extend({
         }
         return Promise.resolve();
     },
-    /**
-     * Override to add the domain coming from the searchPanel (if any) to the
-     * domain coming from the controlPanel.
-     *
-     * @override
-     */
-    update: function (params) {
-        if (!this._searchPanel) {
-            return this._super.apply(this, arguments);
-        }
-        var self = this;
-        if (params.domain) {
-            this.controlPanelDomain = params.domain;
-        }
-        // do not re-render the view as soon as records have been fetched,  but
-        // wait for the searchPanel to be ready as well, such that the view
-        // isn't re-rendered before the searchPanel
-        params.noRender = true;
-        params.domain = this.controlPanelDomain.concat(this.searchPanelDomain);
-        var superProm = this._super.apply(this, arguments);
-        var searchPanelProm = this._updateSearchPanel();
-        return Promise.all([superProm, searchPanelProm]).then(function () {
-            // searchPanel has been re-rendered, so re-render the view
-            return self.renderer.render();
-        });
-    },
 
     //--------------------------------------------------------------------------
     // Private
@@ -256,13 +213,6 @@ var KanbanController = BasicController.extend({
             this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden);
         }
     },
-    /**
-     * @private
-     * @returns {Promise}
-     */
-    _updateSearchPanel: function () {
-        return this._searchPanel.update({searchDomain: this.controlPanelDomain});
-    },
 
     //--------------------------------------------------------------------------
     // Handlers
@@ -519,15 +469,6 @@ var KanbanController = BasicController.extend({
             self._updateEnv();
         });
     },
-    /**
-     * @private
-     * @param {OdooEvent} ev
-     * @param {Array[]} ev.data.domain the current domain of the searchPanel
-     */
-    _onSearchPanelDomainUpdated: function (ev) {
-        this.searchPanelDomain = ev.data.domain;
-        this.reload({offset: 0});
-    },
     /**
      * @private
      * @param {OdooEvent} ev
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 5be98e4de52508709666c392724995c1c2c87916..ca6f8893cd8607ee00e461b234e84296dfef1d84 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -172,17 +172,6 @@ var KanbanRenderer = BasicRenderer.extend({
         this.quickCreate.toggleFold();
         this._toggleNoContentHelper();
     },
-    /**
-     * Allow the rendering to be triggered from outside. This is used for kanban
-     * views with a searchPanel, to synchronize updates (the view is updated
-     * with param 'noRender', so that it is not re-rendered before the
-     * searchPanel).
-     *
-     * @returns {$.Promise}
-     */
-    render: function () {
-        return this._render();
-    },
     /**
      * Updates a given column with its new state.
      *
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 65e6fed767b919029effd0085e9ce11a68d8f93f..e31bff931d146fe703505947f2694c58e216641b 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -8,8 +8,6 @@ var KanbanController = require('web.KanbanController');
 var kanbanExamplesRegistry = require('web.kanban_examples_registry');
 var KanbanModel = require('web.KanbanModel');
 var KanbanRenderer = require('web.KanbanRenderer');
-var pyUtils = require('web.py_utils');
-var SearchPanel = require('web.SearchPanel');
 var utils = require('web.utils');
 
 var _lt = core._lt;
@@ -19,12 +17,11 @@ var KanbanView = BasicView.extend({
     display_name: _lt("Kanban"),
     icon: 'fa-th-large',
     mobile_friendly: true,
-    config: {
+    config: _.extend({}, BasicView.prototype.config, {
         Model: KanbanModel,
         Controller: KanbanController,
         Renderer: KanbanRenderer,
-        SearchPanel: SearchPanel,
-    },
+    }),
     jsLibs: [],
     viewType: 'kanban',
 
@@ -32,8 +29,6 @@ var KanbanView = BasicView.extend({
      * @constructor
      */
     init: function (viewInfo, params) {
-        this.searchPanelSections = Object.create(null);
-
         this._super.apply(this, arguments);
 
         this.loadParams.limit = this.loadParams.limit || 40;
@@ -92,79 +87,12 @@ var KanbanView = BasicView.extend({
             this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js');
         }
 
-        this.hasSearchPanel = !_.isEmpty(this.searchPanelSections);
     },
 
     //--------------------------------------------------------------------------
     // Public
     //--------------------------------------------------------------------------
 
-    /**
-     * Override to set the controller as parent of the optional searchPanel
-     *
-     * @override
-     */
-    getController: function () {
-        var self = this;
-        return this._super.apply(this, arguments).then(function (controller) {
-            if (self.hasSearchPanel) {
-                self.controllerParams.searchPanel.setParent(controller);
-            }
-            return controller;
-        });
-    },
-
-    //--------------------------------------------------------------------------
-    // Private
-    //--------------------------------------------------------------------------
-
-    /**
-     * Override to create the searchPanel (if necessary) with the domain coming
-     * from the controlPanel
-     *
-     * @override
-     * @private
-     */
-    _createControlPanel: function (parent) {
-        if (!this.hasSearchPanel) {
-            return this._super.apply(this, arguments);
-        }
-        var self = this;
-        return this._super.apply(this, arguments).then(function (controlPanel) {
-            return self._createSearchPanel(parent).then(function () {
-                return controlPanel;
-            });
-        });
-    },
-    /**
-     * @private
-     * @param {Widget} parent
-     * @returns {Promise} resolved when the searchPanel is ready
-     */
-    _createSearchPanel: function (parent) {
-        var self = this;
-        var defaultValues = {};
-        Object.keys(this.loadParams.context).forEach(function (key) {
-            var match = /^searchpanel_default_(.*)$/.exec(key);
-            if (match) {
-                defaultValues[match[1]] = self.loadParams.context[key];
-            }
-        });
-        var controlPanelDomain = this.loadParams.domain;
-        var searchPanel = new this.config.SearchPanel(parent, {
-            defaultValues: defaultValues,
-            fields: this.fields,
-            model: this.loadParams.modelName,
-            searchDomain: controlPanelDomain,
-            sections: this.searchPanelSections,
-        });
-        this.controllerParams.searchPanel = searchPanel;
-        this.controllerParams.controlPanelDomain = controlPanelDomain;
-        return searchPanel.appendTo(document.createDocumentFragment()).then(function () {
-            var searchPanelDomain = searchPanel.getDomain();
-            self.loadParams.domain = controlPanelDomain.concat(searchPanelDomain);
-        });
-    },
     /**
      * @private
      * @param {Object} viewInfo
@@ -180,59 +108,6 @@ var KanbanView = BasicView.extend({
         }
         return true;
     },
-    /**
-     * Override to handle nodes with tagname 'searchpanel'.
-     *
-     * @override
-     * @private
-     */
-    _processNode: function (node, fv) {
-        if (node.tag === 'searchpanel') {
-            this._processSearchPanelNode(node, fv);
-            return false;
-        }
-        return this._super.apply(this, arguments);
-    },
-    /**
-     * Populate this.searchPanelSections with category/filter descriptions.
-     *
-     * @private
-     * @param {Object} node
-     * @param {Object} fv
-     */
-    _processSearchPanelNode: function (node, fv) {
-        var self = this;
-        node.children.forEach(function (childNode, index) {
-            if (childNode.tag !== 'field') {
-                return;
-            }
-            if (childNode.attrs.invisible === "1") {
-                return;
-            }
-            var fieldName = childNode.attrs.name;
-            var type = childNode.attrs.select === 'multi' ? 'filter' : 'category';
-
-            var sectionId = _.uniqueId('section_');
-            var section = {
-                color: childNode.attrs.color,
-                description: childNode.attrs.string || fv.fields[fieldName].string,
-                fieldName: fieldName,
-                icon: childNode.attrs.icon,
-                id: sectionId,
-                index: index,
-                type: type,
-            };
-            if (section.type === 'category') {
-                section.icon = section.icon || 'fa-folder';
-            } else if (section.type === 'filter') {
-                section.disableCounters = !!pyUtils.py_eval(childNode.attrs.disable_counters || '0');
-                section.domain = childNode.attrs.domain || '[]';
-                section.groupBy = childNode.attrs.groupby;
-                section.icon = section.icon || 'fa-filter';
-            }
-            self.searchPanelSections[sectionId] = section;
-        });
-    },
     /**
      * @override
      * @private
diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js
index 272ce7cb709616b1c41447aaf78c81136a7eccb5..cf58e1abf4b9bd1388b52918275c0a592bdea31e 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -22,11 +22,11 @@ var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
 var PivotView = AbstractView.extend({
     display_name: _lt('Pivot'),
     icon: 'fa-table',
-    config: {
+    config: _.extend({}, AbstractView.prototype.config,{
         Model: PivotModel,
         Controller: PivotController,
         Renderer: PivotRenderer,
-    },
+    }),
     viewType: 'pivot',
     searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
 
diff --git a/addons/web/static/src/js/views/qweb/qweb_view.js b/addons/web/static/src/js/views/qweb/qweb_view.js
index 45c7340d5c1672c37970f2ce95c568744ae00437..0c858b5eb6cc2d7a4bd2fcb45ba06499cb807648 100644
--- a/addons/web/static/src/js/views/qweb/qweb_view.js
+++ b/addons/web/static/src/js/views/qweb/qweb_view.js
@@ -178,11 +178,11 @@ var QWebView = AbstractView.extend({
     viewType: 'qweb',
     // groupable?
     enableTimeRangeMenu: true,
-    config: {
+    config: _.extend({}, AbstractView.prototype.config, {
         Model: Model,
         Renderer: Renderer,
         Controller: Controller,
-    },
+    }),
 
     /**
      * init method
diff --git a/addons/web/static/src/js/views/kanban/search_panel.js b/addons/web/static/src/js/views/search_panel.js
similarity index 81%
rename from addons/web/static/src/js/views/kanban/search_panel.js
rename to addons/web/static/src/js/views/search_panel.js
index 536409ba1997b06161bbd2e3836fa20ff6bc9153..55d2f23c0fa88ddcfda7c6f8eac9cf8228b6aaa2 100644
--- a/addons/web/static/src/js/views/kanban/search_panel.js
+++ b/addons/web/static/src/js/views/search_panel.js
@@ -8,10 +8,60 @@ odoo.define('web.SearchPanel', function (require) {
 
 var core = require('web.core');
 var Domain = require('web.Domain');
+var pyUtils = require('web.py_utils');
+var viewUtils = require('web.viewUtils');
 var Widget = require('web.Widget');
 
 var qweb = core.qweb;
 
+// defaultViewTypes is the list of view types for which the searchpanel is
+// present by default (if not explicitly stated in the 'view_types' attribute
+// in the arch)
+var defaultViewTypes = ['kanban', 'tree'];
+
+/**
+ * Given a <searchpanel> arch node, iterate over its children to generate the
+ * description of each section (being either a category or a filter).
+ *
+ * @param {Object} node a <searchpanel> arch node
+ * @param {Object} fields the fields of the model
+ * @returns {Object}
+ */
+function _processSearchPanelNode(node, fields) {
+    var sections = {};
+    node.children.forEach((childNode, index) => {
+        if (childNode.tag !== 'field') {
+            return;
+        }
+        if (childNode.attrs.invisible === "1") {
+            return;
+        }
+        var fieldName = childNode.attrs.name;
+        var type = childNode.attrs.select === 'multi' ? 'filter' : 'category';
+
+        var sectionId = _.uniqueId('section_');
+        var section = {
+            color: childNode.attrs.color,
+            description: childNode.attrs.string || fields[fieldName].string,
+            fieldName: fieldName,
+            icon: childNode.attrs.icon,
+            id: sectionId,
+            index: index,
+            type: type,
+        };
+        if (section.type === 'category') {
+            section.icon = section.icon || 'fa-folder';
+        } else if (section.type === 'filter') {
+            section.disableCounters = !!pyUtils.py_eval(childNode.attrs.disable_counters || '0');
+            section.domain = childNode.attrs.domain || '[]';
+            section.groupBy = childNode.attrs.groupby;
+            section.icon = section.icon || 'fa-filter';
+        }
+        sections[sectionId] = section;
+    });
+    return sections;
+}
+
 var SearchPanel = Widget.extend({
     className: 'o_search_panel',
     events: {
@@ -29,8 +79,10 @@ var SearchPanel = Widget.extend({
      *   default, for each filter and category
      * @param {Object} params.fields
      * @param {string} params.model
-     * @param {Object} params.sections
      * @param {Array[]} params.searchDomain domain coming from controlPanel
+     * @param {Object} params.sections
+     * @param {Object} [params.state] state exported by another searchpanel
+     *   instance
      */
     init: function (parent, params) {
         this._super.apply(this, arguments);
@@ -42,6 +94,7 @@ var SearchPanel = Widget.extend({
             return section.type === 'filter';
         });
 
+        this.initialState = params.state;
         this.defaultValues = params.defaultValues || {};
         this.fields = params.fields;
         this.model = params.model;
@@ -52,10 +105,16 @@ var SearchPanel = Widget.extend({
      */
     willStart: function () {
         var self = this;
-        var loadProm = this._fetchCategories().then(function () {
-            return self._fetchFilters().then(self._applyDefaultFilterValues.bind(self));
-        });
-        return Promise.all([loadProm, this._super.apply(this, arguments)]);
+        var loadCategoriesProm;
+        if (this.initialState) {
+            this.filters = this.initialState.filters;
+            this.categories = this.initialState.categories;
+        } else {
+            loadCategoriesProm = this._fetchCategories().then(function () {
+                return self._fetchFilters().then(self._applyDefaultFilterValues.bind(self));
+            });
+        }
+        return Promise.all([loadCategoriesProm, this._super.apply(this, arguments)]);
     },
     /**
      * @override
@@ -69,6 +128,55 @@ var SearchPanel = Widget.extend({
     // Public
     //--------------------------------------------------------------------------
 
+    /**
+     * Parse a given search view arch to extract the searchpanel information
+     * (i.e. a description of each filter/category). Note that according to the
+     * 'view_types' attribute on the <searchpanel> node, and the given viewType,
+     * it may return undefined, meaning that no searchpanel should be rendered
+     * for the current view.
+     *
+     * Note that this is static method, called by AbstractView, *before*
+     * instantiating the SearchPanel, as depending on what it returns, we may
+     * or may not instantiate a SearchPanel.
+     *
+     * @static
+     * @params {Object} viewInfo the viewInfo of a search view
+     * @params {string} viewInfo.arch
+     * @params {Object} viewInfo.fields
+     * @params {string} viewType the type of the current view (e.g. 'kanban')
+     * @returns {Object|undefined}
+     */
+    computeSearchPanelParams: function (viewInfo, viewType) {
+        var searchPanelSections;
+        if (viewInfo) {
+            var arch = viewUtils.parseArch(viewInfo.arch);
+            viewType = viewType === 'list' ? 'tree' : viewType;
+            arch.children.forEach(function (node) {
+                if (node.tag === 'searchpanel') {
+                    var attrs = node.attrs;
+                    var viewTypes = defaultViewTypes;
+                    if (attrs.view_types) {
+                        viewTypes = attrs.view_types.split(',');
+                    }
+                    if (viewTypes.indexOf(viewType) !== -1) {
+                        searchPanelSections = _processSearchPanelNode(node, viewInfo.fields);
+                    }
+                }
+            });
+        }
+        return searchPanelSections;
+    },
+    /**
+     * Export the current state (categories and filters) of the searchpanel.
+     *
+     * @returns {Object}
+     */
+    exportState: function () {
+        return {
+            categories: this.categories,
+            filters: this.filters,
+        };
+    },
     /**
      * @returns {Array[]} the current searchPanel domain based on active
      *   categories and checked filters
@@ -76,6 +184,18 @@ var SearchPanel = Widget.extend({
     getDomain: function () {
         return this._getCategoryDomain().concat(this._getFilterDomain());
     },
+    /**
+     * Import a previously exported state (see exportState).
+     *
+     * @param {Object} state
+     * @param {Object} state.filters.
+     * @param {Object} state.categories
+     */
+    importState: function (state) {
+        this.filters = state.filters || this.filters;
+        this.categories = state.categories || this.categories;
+        this._render();
+    },
     /**
      * Reload the filters and re-render. Note that we only reload the filters if
      * the controlPanel domain or searchPanel domain has changed.
@@ -145,10 +265,10 @@ var SearchPanel = Widget.extend({
             }
         });
         category.rootIds = _.filter(_.map(values, function (value) {
-                return value.id;
-            }), function (valueId) {
-                var value = category.values[valueId];
-                return value.parentId === false;
+            return value.id;
+        }), function (valueId) {
+            var value = category.values[valueId];
+            return value.parentId === false;
         });
 
         // set active value
@@ -317,6 +437,7 @@ var SearchPanel = Widget.extend({
      */
     _getCategoryDomain: function () {
         var self = this;
+
         function categoryToDomain(domain, categoryId) {
             var category = self.categories[categoryId];
             if (category.activeValueId) {
@@ -326,6 +447,7 @@ var SearchPanel = Widget.extend({
             }
             return domain;
         }
+
         return Object.keys(this.categories).reduce(categoryToDomain, []);
     },
     /**
@@ -341,6 +463,7 @@ var SearchPanel = Widget.extend({
      */
     _getFilterDomain: function () {
         var self = this;
+
         function getCheckedValueIds(values) {
             return Object.keys(values).reduce(function (checkedValues, valueId) {
                 if (values[valueId].checked) {
@@ -349,6 +472,7 @@ var SearchPanel = Widget.extend({
                 return checkedValues;
             }, []);
         }
+
         function filterToDomain(domain, filterId) {
             var filter = self.filters[filterId];
             if (filter.groups) {
@@ -367,6 +491,7 @@ var SearchPanel = Widget.extend({
             }
             return domain;
         }
+
         return Object.keys(this.filters).reduce(filterToDomain, []);
     },
     /**
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index e35f09a6cc9f9b1df1fb1620282394686b39b643..1fb202324951159ef25507dec676f0914a00e18d 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -367,6 +367,7 @@ var SelectCreateDialog = ViewDialog.extend({
             modelName: this.res_model,
             readonly: true,
             withBreadcrumbs: false,
+            withSearchPanel: false,
         }, this.options.list_view_options));
         listView.setController(SelectCreateListController);
         return listView.getController(this).then(function (controller) {
diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss
index f80d50ba576f1139a6c0a224197e261d9c396bcd..34c71ebb5515ceff7bdc355fb67e4ac9a3e2dbc1 100644
--- a/addons/web/static/src/scss/kanban_view.scss
+++ b/addons/web/static/src/scss/kanban_view.scss
@@ -582,101 +582,6 @@
     }
 }
 
-// ------- Kanban with SearchPanel -------
-$o-searchpanel-p: $o-horizontal-padding;
-$o-searchpanel-p-small: $o-horizontal-padding*0.5;
-$o-searchpanel-p-tiny: $o-searchpanel-p-small*0.5;
-
-$o-searchpanel-category-default-color: $o-brand-primary;
-$o-searchpanel-filter-default-color: #D59244;
-
-.o_kanban_with_searchpanel {
-    display: flex;
-    align-items: flex-start;
-
-    .o_kanban_view {
-        flex: 1 1 100%;
-        overflow: auto; // make the kanban renderer and search panel scroll individually
-        max-height: 100%;
-    }
-    .o_search_panel {
-        flex: 0 0 220px;
-        overflow: auto;
-        height: 100%;
-        padding: $o-searchpanel-p-small $o-searchpanel-p-small $o-searchpanel-p*2 $o-searchpanel-p;
-        border-right: 1px solid $gray-300;
-        background-color: white;
-
-        .o_search_panel_category .o_search_panel_section_icon {
-            color: $o-brand-odoo;
-        }
-        .o_search_panel_filter .o_search_panel_section_icon {
-            color: $o-searchpanel-filter-default-color;
-        }
-
-        .o_search_panel_label {
-            cursor: pointer;
-            user-select: none;
-
-            .o_toggle_fold {
-                padding: 3px;
-            }
-        }
-        .o_search_panel_section_header {
-            padding: $o-searchpanel-p-small 0;
-        }
-        .list-group-item {
-            padding: 0 0 $o-searchpanel-p-small 0;
-
-            .list-group-item {
-                padding: 0 0 0 $custom-control-gutter;
-                margin-bottom: $o-searchpanel-p-tiny*0.5;
-                &:first-child {
-                    margin-top: $o-searchpanel-p-tiny*0.5;
-                }
-            }
-            span.o_search_panel_label_title {
-                color: $headings-color;
-                @include o-text-overflow(inline-block, calc(100% - 22px));
-            }
-            header.active {
-                background-color: $list-group-action-active-bg;
-            }
-        }
-        .o_search_panel_category_value {
-            header {
-                margin-left: -$o-searchpanel-p-tiny;
-                padding-left: $o-searchpanel-p-tiny;
-            }
-            .o_search_panel_category_value {
-                position: relative;
-                padding-left: $o-searchpanel-p;
-                padding-bottom: $o-searchpanel-p-tiny;
-                margin-bottom: 0;
-
-                &:before, &:after {
-                    @include o-position-absolute(0, $left: $o-searchpanel-p-tiny);
-                    @include size(1px, 100%);
-                    background: $gray-500;
-                    content: '';
-                }
-                &:after {
-                    top: 10px;
-                    @include size(8px, 1px);
-                }
-                &:last-child {
-                    &:before {
-                        height: 11px;
-                    }
-                    &:after {
-                        top: 11px;
-                    }
-                }
-            }
-        }
-    }
-}
-
 // ----------------- Set Cover Dialog -----------------
 .modal .o_kanban_cover_container .o_kanban_cover_image {
     display: inline-block;
diff --git a/addons/web/static/src/scss/search_panel.scss b/addons/web/static/src/scss/search_panel.scss
new file mode 100644
index 0000000000000000000000000000000000000000..deac73754d123c151a0bcb6220f6a086fb836dbf
--- /dev/null
+++ b/addons/web/static/src/scss/search_panel.scss
@@ -0,0 +1,94 @@
+// ------- View with SearchPanel -------
+$o-searchpanel-p: $o-horizontal-padding;
+$o-searchpanel-p-small: $o-horizontal-padding*0.5;
+$o-searchpanel-p-tiny: $o-searchpanel-p-small*0.5;
+
+$o-searchpanel-category-default-color: $o-brand-primary;
+$o-searchpanel-filter-default-color: #D59244;
+
+.o_controller_with_searchpanel {
+    display: flex;
+    align-items: flex-start;
+
+    .o_renderer_with_searchpanel {
+        flex: 1 1 100%;
+        overflow: auto; // make the renderer and search panel scroll individually
+        max-height: 100%;
+    }
+    .o_search_panel {
+        flex: 0 0 220px;
+        overflow: auto;
+        height: 100%;
+        padding: $o-searchpanel-p-small $o-searchpanel-p-small $o-searchpanel-p*2 $o-searchpanel-p;
+        border-right: 1px solid $gray-300;
+        background-color: white;
+
+        .o_search_panel_category .o_search_panel_section_icon {
+            color: $o-brand-odoo;
+        }
+        .o_search_panel_filter .o_search_panel_section_icon {
+            color: $o-searchpanel-filter-default-color;
+        }
+
+        .o_search_panel_label {
+            cursor: pointer;
+            user-select: none;
+
+            .o_toggle_fold {
+                padding: 3px;
+            }
+        }
+        .o_search_panel_section_header {
+            padding: $o-searchpanel-p-small 0;
+        }
+        .list-group-item {
+            padding: 0 0 $o-searchpanel-p-small 0;
+
+            .list-group-item {
+                padding: 0 0 0 $custom-control-gutter;
+                margin-bottom: $o-searchpanel-p-tiny*0.5;
+                &:first-child {
+                    margin-top: $o-searchpanel-p-tiny*0.5;
+                }
+            }
+            span.o_search_panel_label_title {
+                color: $headings-color;
+                @include o-text-overflow(inline-block, calc(100% - 22px));
+            }
+            header.active {
+                background-color: $list-group-action-active-bg;
+            }
+        }
+        .o_search_panel_category_value {
+            header {
+                margin-left: -$o-searchpanel-p-tiny;
+                padding-left: $o-searchpanel-p-tiny;
+            }
+            .o_search_panel_category_value {
+                position: relative;
+                padding-left: $o-searchpanel-p;
+                padding-bottom: $o-searchpanel-p-tiny;
+                margin-bottom: 0;
+
+                &:before, &:after {
+                    @include o-position-absolute(0, $left: $o-searchpanel-p-tiny);
+                    @include size(1px, 100%);
+                    background: $gray-500;
+                    content: '';
+                }
+                &:after {
+                    top: 10px;
+                    @include size(8px, 1px);
+                }
+                &:last-child {
+                    &:before {
+                        height: 11px;
+                    }
+                    &:after {
+                        top: 11px;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 8a6675ecc584b6d9e134e9b7d1f11976ba969301..ac944425a8cafdee038b597628731c833e72c7cb 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -3442,6 +3442,9 @@ QUnit.module('ActionManager', {
                 active_id: 1,
                 active_ids: [1],
             },
+            flags: {
+                withSearchPanel: false,
+            },
         });
         var checkSessionStorage = false;
         var actionManager = await createActionManager({
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
index ef890e92e9086d4a87bc09a452aa4c0bc297a396..b8f42236c2464c4719ffe38311b012213e3f7478 100644
--- a/addons/web/static/tests/helpers/test_utils_create.js
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -154,6 +154,11 @@ async function createView(params) {
             modelName: params.model || 'foo',
         });
     } else {
+        viewOptions.controlPanelFieldsView = testUtilsMock.fieldsViewGet(mockServer, {
+            arch: params.archs && params.archs[params.model + ',false,search'] || '<search/>',
+            fields: viewInfo.fields,
+            model: params.model,
+        });
         view = new params.View(viewInfo, viewOptions);
     }
 
diff --git a/addons/web/static/tests/views/search_panel_tests.js b/addons/web/static/tests/views/search_panel_tests.js
index 9cb66724e78a78de0106d69a5a48d4dc58774f2b..b51ba9c3b3a9d986a0ec41a04626b72eadad6cab 100644
--- a/addons/web/static/tests/views/search_panel_tests.js
+++ b/addons/web/static/tests/views/search_panel_tests.js
@@ -2,10 +2,12 @@ odoo.define('web.search_panel_tests', function (require) {
 "use strict";
 
 var AbstractStorageService = require('web.AbstractStorageService');
+var FormView = require('web.FormView');
 var KanbanView = require('web.KanbanView');
 var RamStorage = require('web.RamStorage');
 var testUtils = require('web.test_utils');
 
+var createActionManager = testUtils.createActionManager;
 var createView = testUtils.createView;
 
 QUnit.module('Views', {
@@ -15,15 +17,16 @@ QUnit.module('Views', {
                 fields: {
                     foo: {string: "Foo", type: 'char'},
                     bar: {string: "Bar", type: 'boolean'},
+                    int_field: {string: "Int Field", type: 'integer'},
                     company_id: {string: "company", type: 'many2one', relation: 'company'},
                     category_id: { string: "category", type: 'many2one', relation: 'category' },
                     state: { string: "State", type: 'selection', selection: [['abc', "ABC"], ['def', "DEF"], ['ghi', "GHI"]]},
                 },
                 records: [
-                    {id: 1, bar: true, foo: "yop", company_id: 3, state: 'abc', category_id: 6},
-                    {id: 2, bar: true, foo: "blip", company_id: 5, state: 'def', category_id: 7},
-                    {id: 3, bar: true, foo: "gnap", company_id: 3, state: 'ghi', category_id: 7},
-                    {id: 4, bar: false, foo: "blip", company_id: 5, state: 'ghi', category_id: 7},
+                    {id: 1, bar: true, foo: "yop", int_field: 1, company_id: 3, state: 'abc', category_id: 6},
+                    {id: 2, bar: true, foo: "blip", int_field: 2, company_id: 5, state: 'def', category_id: 7},
+                    {id: 3, bar: true, foo: "gnap", int_field: 4, company_id: 3, state: 'ghi', category_id: 7},
+                    {id: 4, bar: false, foo: "blip", int_field: 8, company_id: 5, state: 'ghi', category_id: 7},
                 ]
             },
             company: {
@@ -48,6 +51,40 @@ QUnit.module('Views', {
             },
         };
 
+        this.actions = [{
+            id: 1,
+            name: 'Partners',
+            res_model: 'partner',
+            type: 'ir.actions.act_window',
+            views: [[false, 'kanban'], [false, 'list'], [false, 'pivot'], [false, 'form']],
+        }, {
+            id: 2,
+            name: 'Partners',
+            res_model: 'partner',
+            type: 'ir.actions.act_window',
+            views: [[false, 'form']],
+        }];
+
+        this.archs = {
+            'partner,false,list': '<tree><field name="foo"/></tree>',
+            'partner,false,kanban': '<kanban>' +
+                    '<templates><t t-name="kanban-box">' +
+                        '<div><field name="foo"/></div>' +
+                    '</t></templates>' +
+                '</kanban>',
+            'partner,false,form': '<form>' +
+                        '<button name="1" type="action" string="multi view"/>' +
+                        '<field name="foo"/>' +
+                    '</form>',
+            'partner,false,pivot': '<pivot><field name="int_field" type="measure"/></pivot>',
+            'partner,false,search': '<search>' +
+                    '<searchpanel>' +
+                        '<field name="company_id"/>' +
+                        '<field select="multi" name="category_id"/>' +
+                    '</searchpanel>' +
+                '</search>',
+        };
+
         var RamStorageService = AbstractStorageService.extend({
             storage: new RamStorage(),
         });
@@ -57,7 +94,7 @@ QUnit.module('Views', {
     },
 }, function () {
 
-    QUnit.module('SearchPanel in Kanban views');
+    QUnit.module('SearchPanel');
 
     QUnit.test('basic rendering', async function (assert) {
         assert.expect(17);
@@ -77,15 +114,19 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field select="multi" name="category_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field select="multi" name="category_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
-        assert.containsOnce(kanban, '.o_content.o_kanban_with_searchpanel > .o_search_panel');
-        assert.containsOnce(kanban, '.o_content.o_kanban_with_searchpanel > .o_kanban_view');
+        assert.containsOnce(kanban, '.o_content.o_controller_with_searchpanel > .o_search_panel');
+        assert.containsOnce(kanban, '.o_content.o_controller_with_searchpanel > .o_kanban_view');
 
         assert.containsN(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 4);
 
@@ -129,11 +170,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id" icon="fa-car" color="blue"/>' +
-                        '<field select="multi" name="state" icon="fa-star" color="#000"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id" icon="fa-car" color="blue"/>' +
+                            '<field select="multi" name="state" icon="fa-star" color="#000"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         assert.hasClass(kanban.$('.o_search_panel_section_header:first i'), 'fa-car');
@@ -160,11 +205,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field select="multi" invisible="1" name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field select="multi" invisible="1" name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 assert.step(args.method || route);
                 return this._super.apply(this, arguments);
@@ -195,12 +244,16 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field select="multi" name="category_id"/>' +
-                        '<field name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field select="multi" name="category_id"/>' +
+                            '<field name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            }
         });
 
         assert.containsN(kanban, '.o_search_panel_section', 3);
@@ -228,11 +281,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, [["state", "=", "ghi"]]);
@@ -268,10 +325,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -326,10 +387,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // select 'abc'
@@ -393,10 +458,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, [['company_id', 'child_of', expectedActiveId]]);
@@ -448,10 +517,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, []);
@@ -490,11 +563,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -557,10 +634,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // 'All' is selected by default
@@ -635,10 +716,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         assert.strictEqual(kanban.$('.o_search_panel_category_value:contains(agrolait) .o_toggle_fold').length, 1,
@@ -680,10 +765,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // unfold agrolait
@@ -724,10 +813,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -801,11 +894,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="state"/>' +
-                        '<field select="multi" name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="state"/>' +
+                            '<field select="multi" name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // 'All' should be selected by default
@@ -872,10 +969,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         assert.containsN(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 4);
@@ -928,10 +1029,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -1027,10 +1132,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -1111,11 +1220,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="state"/>' +
-                        '<field select="multi" name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="state"/>' +
+                            '<field select="multi" name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             viewOptions: {
                 limit: 2,
             },
@@ -1187,10 +1300,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id" groupby="category_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id" groupby="category_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             domain: [['bar', '=', true]],
         });
 
@@ -1305,10 +1422,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id" domain="[(\'parent_id\',\'=\',False)]"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id" domain="[(\'parent_id\',\'=\',False)]"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         assert.containsN(kanban, '.o_search_panel_filter_value', 2);
@@ -1334,10 +1455,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id" groupby="category_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id" groupby="category_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // groups are opened by default
@@ -1405,11 +1530,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="category_id"/>' +
-                        '<field select="multi" name="company_id" domain="[[\'category_id\', \'=\', category_id]]"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="category_id"/>' +
+                            '<field select="multi" name="company_id" domain="[[\'category_id\', \'=\', category_id]]"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         // select 'gold' category
@@ -1467,11 +1596,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id"/>' +
-                        '<field select="multi" name="state"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id"/>' +
+                            '<field select="multi" name="state"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, expectedDomain);
@@ -1509,10 +1642,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, [["company_id", "in", [3]]]);
@@ -1543,10 +1680,14 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field select="multi" name="company_id" groupby="category_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field select="multi" name="company_id" groupby="category_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
             mockRPC: function (route, args) {
                 if (route === '/web/dataset/search_read') {
                     assert.deepEqual(args.domain, [['company_id', 'in', [5]]]);
@@ -1581,11 +1722,15 @@ QUnit.module('Views', {
                             '<field name="foo"/>' +
                         '</div>' +
                     '</t></templates>' +
-                    '<searchpanel>' +
-                        '<field name="company_id"/>' +
-                        '<field select="multi" name="category_id"/>' +
-                    '</searchpanel>' +
                 '</kanban>',
+            archs: {
+                'partner,false,search': '<search>' +
+                        '<searchpanel>' +
+                            '<field name="company_id"/>' +
+                            '<field select="multi" name="category_id"/>' +
+                        '</searchpanel>' +
+                    '</search>',
+            },
         });
 
         var $firstSection = kanban.$('.o_search_panel_section:first');
@@ -1593,6 +1738,296 @@ QUnit.module('Views', {
             'AllasustekagrolaithighIDlowID');
         kanban.destroy();
     });
-});
 
+    QUnit.test('search panel is available on list and kanban by default', async function (assert) {
+        assert.expect(8);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+        });
+
+        await actionManager.doAction(1);
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_kanban_view');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_pivot'));
+        assert.containsOnce(actionManager, '.o_content .o_pivot');
+        assert.containsNone(actionManager, '.o_content .o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_list_view');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_data_row .o_data_cell:first'));
+        assert.containsOnce(actionManager, '.o_content .o_form_view');
+        assert.containsNone(actionManager, '.o_content .o_search_panel');
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('search panel with view_types attribute', async function (assert) {
+        assert.expect(6);
+
+        this.archs['partner,false,search'] = '<search>' +
+                '<searchpanel view_types="kanban,pivot">' +
+                    '<field name="company_id"/>' +
+                    '<field select="multi" name="category_id"/>' +
+                '</searchpanel>' +
+            '</search>';
+
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+        });
+
+        await actionManager.doAction(1);
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_kanban_view');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.containsOnce(actionManager, '.o_content .o_list_view');
+        assert.containsNone(actionManager, '.o_content .o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_pivot'));
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_pivot');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('search panel state is shared between views', async function (assert) {
+        assert.expect(16);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+            services: this.services,
+            mockRPC: function (route, args) {
+                if (route === '/web/dataset/search_read') {
+                    assert.step(JSON.stringify(args.domain));
+                }
+                return this._super.apply(this, arguments);
+            },
+        });
+
+        await actionManager.doAction(1);
+        assert.hasClass(actionManager.$('.o_search_panel_category_value:first header'), 'active');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 4);
+
+        // select 'asustek' company
+        await testUtils.dom.click(actionManager.$('.o_search_panel_category_value:nth(1) header'));
+        assert.hasClass(actionManager.$('.o_search_panel_category_value:nth(1) header'), 'active');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 2);
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.hasClass(actionManager.$('.o_search_panel_category_value:nth(1) header'), 'active');
+        assert.containsN(actionManager, '.o_data_row', 2);
+
+        // select 'agrolait' company
+        await testUtils.dom.click(actionManager.$('.o_search_panel_category_value:nth(2) header'));
+        assert.hasClass(actionManager.$('.o_search_panel_category_value:nth(2) header'), 'active');
+        assert.containsN(actionManager, '.o_data_row', 2);
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_kanban'));
+        assert.hasClass(actionManager.$('.o_search_panel_category_value:nth(2) header'), 'active');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 2);
+
+        assert.verifySteps([
+            '[]', // initial search_read
+            '[["company_id","child_of",3]]', // kanban, after selecting the first company
+            '[["company_id","child_of",3]]', // list
+            '[["company_id","child_of",5]]', // list, after selecting the other company
+            '[["company_id","child_of",5]]', // kanban
+        ]);
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('search panel filters are kept between switch views', async function (assert) {
+        assert.expect(16);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+            services: this.services,
+            mockRPC: function (route, args) {
+                if (route === '/web/dataset/search_read') {
+                    assert.step(JSON.stringify(args.domain));
+                }
+                return this._super.apply(this, arguments);
+            },
+        });
+
+        await actionManager.doAction(1);
+        assert.containsNone(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 4);
+
+        // select gold filter
+        await testUtils.dom.click(actionManager.$('.o_search_panel_filter input[type="checkbox"]:nth(0)'));
+        assert.containsOnce(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 1);
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.containsOnce(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_data_row', 1);
+
+        // select silver filter
+        await testUtils.dom.click(actionManager.$('.o_search_panel_filter input[type="checkbox"]:nth(1)'));
+        assert.containsN(actionManager, '.o_search_panel_filter_value input:checked', 2);
+        assert.containsN(actionManager, '.o_data_row', 4);
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_kanban'));
+        assert.containsN(actionManager, '.o_search_panel_filter_value input:checked', 2);
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 4);
+
+        assert.verifySteps([
+            '[]', // initial search_read
+            '[["category_id","in",[6]]]', // kanban, after selecting the gold filter
+            '[["category_id","in",[6]]]', // list
+            '[["category_id","in",[6,7]]]', // list, after selecting the silver filter
+            '[["category_id","in",[6,7]]]', // kanban
+        ]);
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('search panel filters are kept when switching to a view with no search panel', async function (assert) {
+        assert.expect(13);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+        });
+
+        await actionManager.doAction(1);
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_kanban_view');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+        assert.containsNone(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 4);
+
+        // select gold filter
+        await testUtils.dom.click(actionManager.$('.o_search_panel_filter input[type="checkbox"]:nth(0)'));
+        assert.containsOnce(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 1);
+
+        // switch to pivot
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_pivot'));
+        assert.containsOnce(actionManager, '.o_content .o_pivot');
+        assert.containsNone(actionManager, '.o_content .o_search_panel');
+        assert.strictEqual(actionManager.$('.o_pivot_cell_value').text(), '15');
+
+        // switch to list
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_list_view');
+        assert.containsOnce(actionManager, '.o_content.o_controller_with_searchpanel .o_search_panel');
+        assert.containsOnce(actionManager, '.o_search_panel_filter_value input:checked');
+        assert.containsN(actionManager, '.o_data_row', 1);
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('disable search panel onExecuteAction', async function (assert) {
+        assert.expect(6);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+            services: this.services,
+        });
+
+        await actionManager.doAction(2);
+
+        await testUtils.dom.click(actionManager.$('.o_form_view button:contains("multi view")'));
+        assert.containsOnce(actionManager, '.o_kanban_view');
+        assert.containsNone(actionManager, '.o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        assert.containsOnce(actionManager, '.o_list_view');
+        assert.containsNone(actionManager, '.o_search_panel');
+
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_kanban'));
+        assert.containsOnce(actionManager, '.o_kanban_view');
+        assert.containsNone(actionManager, '.o_search_panel');
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('categories and filters are not reloaded when switching between views', async function (assert) {
+        assert.expect(8);
+
+        var actionManager = await createActionManager({
+            actions: this.actions,
+            archs: this.archs,
+            data: this.data,
+            services: this.services,
+            mockRPC: function (route, args) {
+                assert.step(args.method || route);
+                return this._super.apply(this, arguments);
+            },
+        });
+
+        await actionManager.doAction(1);
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_list'));
+        await testUtils.dom.click(actionManager.$('.o_cp_switch_kanban'));
+
+        assert.verifySteps([
+            '/web/action/load',
+            'load_views',
+            'search_panel_select_range', // kanban: categories
+            'search_panel_select_multi_range', // kanban: filters
+            '/web/dataset/search_read', // kanban: records
+            '/web/dataset/search_read', // list: records
+            '/web/dataset/search_read', // kanban: records
+        ]);
+
+        actionManager.destroy();
+    });
+
+    QUnit.test('search panel is not instanciated in dialogs', async function (assert) {
+        assert.expect(2);
+
+        this.data.company.records = [
+            {id: 1, name: 'Company1'},
+            {id: 2, name: 'Company2'},
+            {id: 3, name: 'Company3'},
+            {id: 4, name: 'Company4'},
+            {id: 5, name: 'Company5'},
+            {id: 6, name: 'Company6'},
+            {id: 7, name: 'Company7'},
+            {id: 8, name: 'Company8'},
+        ];
+
+        var form = await createView({
+            View: FormView,
+            model: 'partner',
+            data: this.data,
+            arch: '<form><field name="company_id"/></form>',
+            archs: {
+                'company,false,list': '<tree><field name="name"/></tree>',
+                'company,false,search': '<search>' +
+                                                '<field name="name"/>' +
+                                                '<searchpanel>' +
+                                                    '<field name="category_id"/>' +
+                                                '</searchpanel>' +
+                                            '</search>',
+            },
+        });
+
+        await testUtils.fields.many2one.clickOpenDropdown('company_id');
+        await testUtils.fields.many2one.clickItem('company_id', 'Search More');
+
+        assert.containsOnce(document.body, '.modal .o_list_view');
+        assert.containsNone(document.body, '.modal .o_search_panel');
+
+        form.destroy();
+    });
+});
 });
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 6a225d7a6361defa67e5e9f828b970d02ea6c02f..f17889bf5c2a0b54077970acb84eeb21c4eeead9 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -209,6 +209,7 @@
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/web_calendar.scss"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/web_calendar_mobile.scss"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view.scss"/>
+        <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_panel.scss"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view_mobile.scss"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/dropdown_menu.scss"/>
         <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/search_view_extra.scss"/>
@@ -314,6 +315,7 @@
         <script type="text/javascript" src="/web/static/src/js/views/control_panel/search/favorites_submenus_registry.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/control_panel/search/search_filters.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/control_panel/search/search_filters_registry.js"></script>
+        <script type="text/javascript" src="/web/static/src/js/views/search_panel.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/field_manager_mixin.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/standalone_field_manager_mixin.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/view_registry.js"></script>
@@ -337,7 +339,6 @@
         <script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer_mobile.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_view.js"></script>
-        <script type="text/javascript" src="/web/static/src/js/views/kanban/search_panel.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/kanban/quick_create_form_view.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/list/list_editable_renderer.js"></script>
         <script type="text/javascript" src="/web/static/src/js/views/list/list_model.js"></script>
diff --git a/addons/web_diagram/static/src/js/diagram_view.js b/addons/web_diagram/static/src/js/diagram_view.js
index c8a46a21990c29009bf40ecd7a0d9530f7d90b98..f95a0b69564237d668ecae813c0d5da6897196ba 100644
--- a/addons/web_diagram/static/src/js/diagram_view.js
+++ b/addons/web_diagram/static/src/js/diagram_view.js
@@ -22,11 +22,11 @@ var DiagramView = BasicView.extend({
         '/web_diagram/static/lib/js/jquery.mousewheel.js',
         '/web_diagram/static/lib/js/raphael.js',
     ]],
-    config: {
+    config: _.extend({}, BasicView.prototype.config, {
         Model: DiagramModel,
         Renderer: DiagramRenderer,
         Controller: DiagramController,
-    },
+    }),
     viewType: 'diagram',
 
     /**
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index eb94a707c771411abf7356daae8651c4df70ba95..8b632bba228e7e0f2bf53724c8960dea53703289 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -1134,57 +1134,6 @@ Possible children of the view element are:
        * kanban-specific CSS
        * kanban structures/widgets (vignette, details, ...)
 
-``searchpanel``
-  allows to display a search panel on the left of the kanban view.
-  This tool allows to quickly filter data on the basis of given fields. The fields
-  are specified as direct children of the ``searchpanel`` with tag name ``field``,
-  and the following attributes:
-
-  * ``name`` (mandatory) the name of the field to filter on
-
-  * ``select`` determines the behavior and display. Possible values are
-
-      ``one`` (default) at most one value can be selected. Supported field types are
-        many2one and selection.
-
-      ``multi`` several values can be selected (checkboxes). Supported field
-        types are many2one, many2many and selection.
-
-  * ``groups``: restricts to specific users
-
-  * ``string``: determines the label to display
-
-  * ``icon``: specifies which icon is used
-
-  * ``color``: determines the icon color
-
-  Additional optional attributes are available in the ``multi`` case:
-
-  * ``domain``: determines conditions that the comodel records have to satisfy.
-
-  A domain might be used to express a dependency on another field (with select="one")
-  of the search panel. Consider
-
-  .. code-block:: xml
-
-    <searchpanel>
-      <field name="department_id"/>
-      <field name="manager_id" select="multi" domain="[('department_id', '=', department_id)]"/>
-    <searchpanel/>
-
-  In the above example, the range of values for manager_id (manager names) available at screen
-  will depend on the value currently selected for the field ``department_id``.
-
-  * ``groupby``: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.
-
-  * ``disable_counters``: default is false. If set to true the counters won't be computed.
-
-    This feature has been implemented in case performances would be too bad.
-
-    Another way to solve performance issues is to properly override the
-    ``search_panel_select_multi_range`` method.
-
-
 If you need to extend the Kanban view, see :js:class::`the JS API <KanbanRecord>`.
 
 .. _reference/views/calendar:
@@ -1951,6 +1900,62 @@ Possible children elements of the search view are:
 ``group``
     can be used to separate groups of filters, more readable than
     ``separator`` in complex search views
+``searchpanel``
+  allows to display a search panel on the left of any multi records view.
+  By default, the list and kanban views have the searchpanel enabled.
+  The search panel can be activated on other views with the attribute:
+
+  * ``view_types`` a comma separated list of view types on which to enable the search panel
+
+      default: 'tree,kanban'
+
+  This tool allows to quickly filter data on the basis of given fields. The fields
+  are specified as direct children of the ``searchpanel`` with tag name ``field``,
+  and the following attributes:
+
+  * ``name`` (mandatory) the name of the field to filter on
+
+  * ``select`` determines the behavior and display. Possible values are
+
+      ``one`` (default) at most one value can be selected. Supported field types are
+        many2one and selection.
+
+      ``multi`` several values can be selected (checkboxes). Supported field
+        types are many2one, many2many and selection.
+
+  * ``groups``: restricts to specific users
+
+  * ``string``: determines the label to display
+
+  * ``icon``: specifies which icon is used
+
+  * ``color``: determines the icon color
+
+  Additional optional attributes are available in the ``multi`` case:
+
+  * ``domain``: determines conditions that the comodel records have to satisfy.
+
+  A domain might be used to express a dependency on another field (with select="one")
+  of the search panel. Consider
+
+  .. code-block:: xml
+
+    <searchpanel>
+      <field name="department_id"/>
+      <field name="manager_id" select="multi" domain="[('department_id', '=', department_id)]"/>
+    <searchpanel/>
+
+  In the above example, the range of values for manager_id (manager names) available at screen
+  will depend on the value currently selected for the field ``department_id``.
+
+  * ``groupby``: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.
+
+  * ``disable_counters``: default is false. If set to true the counters won't be computed.
+
+    This feature has been implemented in case performances would be too bad.
+
+    Another way to solve performance issues is to properly override the
+    ``search_panel_select_multi_range`` method.
 
 .. _reference/views/search/defaults:
 
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index cff628c93497e7b157a5afc742cce9c72dd30295..a110361974a2e4bb93b63682cd3ba074a7d4816e 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -1,19 +1,16 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-import ast
 import collections
 import copy
 import datetime
 import fnmatch
 import logging
-import os
 import re
 import time
 import uuid
 
 import itertools
 from dateutil.relativedelta import relativedelta
-from functools import partial
 from difflib import HtmlDiff
 from operator import itemgetter
 
@@ -22,7 +19,7 @@ from lxml import etree
 from lxml.etree import LxmlError
 from lxml.builder import E
 
-from odoo import api, fields, models, tools, SUPERUSER_ID, _
+from odoo import api, fields, models, tools, _
 from odoo.exceptions import ValidationError
 from odoo.http import request
 from odoo.modules.module import get_resource_from_path, get_resource_path
@@ -31,7 +28,7 @@ from odoo.tools import config, graph, ConstantMapping, SKIPPED_ELEMENT_TYPES, py
 from odoo.tools.convert import _fix_multiple_roots
 from odoo.tools.json import scriptsafe as json_scriptsafe
 from odoo.tools.safe_eval import safe_eval
-from odoo.tools.view_validation import valid_view
+from odoo.tools.view_validation import valid_view, get_attrs_field_names, field_is_editable
 from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS
 from odoo.tools.image import image_data_uri
 
@@ -43,20 +40,6 @@ MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpa
 # Note: natural _order has `name`, but only because that makes list browsing easier
 INHERIT_ORDER = 'priority,id'
 
-# attributes in views that may contain references to field names
-ATTRS_WITH_FIELD_NAMES = {
-    'context',
-    'domain',
-    'decoration-bf',
-    'decoration-it',
-    'decoration-danger',
-    'decoration-info',
-    'decoration-muted',
-    'decoration-primary',
-    'decoration-success',
-    'decoration-warning',
-}
-
 
 def keep_query(*keep_params, **additional_params):
     """
@@ -181,7 +164,6 @@ xpath_utils['hasclass'] = _hasclass
 
 TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS))
 WRONGCLASS = re.compile(r"(@class\s*=|=\s*@class|contains\(@class)")
-READONLY = re.compile(r"\breadonly\b")
 
 
 class View(models.Model):
@@ -375,7 +357,7 @@ actual arch.
                     # A <data> element is a wrapper for multiple root nodes
                     view_docs = view_docs[0]
                 for view_arch in view_docs:
-                    check = valid_view(view_arch)
+                    check = valid_view(view_arch, env=self.env, model=view.model)
                     if not check:
                         raise ValidationError(_('Invalid view %s definition in %s') % (view.name, view.arch_fs))
                     if check == "Warning":
@@ -876,7 +858,7 @@ actual arch.
                 attrs = {}
                 field = Model._fields.get(node.get('name'))
                 if field:
-                    editable = self.env.context.get('view_is_editable', True) and self._field_is_editable(field, node)
+                    editable = self.env.context.get('view_is_editable', True) and field_is_editable(field, node)
                     children = False
                     views = {}
                     for f in node:
@@ -939,6 +921,15 @@ actual arch.
                 if f.tag == 'filter':
                     fields[f.get('name')] = {}
 
+        elif node.tag == 'search':
+            searchpanel = [c for c in node if c.tag == 'searchpanel']
+            if searchpanel:
+                self.with_context(
+                    base_model_name=model,
+                    check_field_names=False,  # field validation is a bit more tricky and done apart
+                    view_is_editable=False,
+                ).postprocess_and_fields(model, searchpanel[0], view_id)
+
         if not self._apply_group(model, node, modifiers, fields):
             # node must be removed, no need to proceed further with its children
             return fields
@@ -948,6 +939,9 @@ actual arch.
         orm.transfer_node_to_modifiers(node, modifiers, self._context, in_tree_view)
 
         for f in node:
+            if node.tag == 'search' and f.tag == 'searchpanel':
+                # searchpanel part has to be validated independently
+                continue
             if children or (node.tag == 'field' and f.tag in ('filter', 'separator')):
                 fields.update(self.postprocess(model, f, view_id, in_tree_view, model_fields))
 
@@ -984,113 +978,6 @@ actual arch.
 
         return arch
 
-    def _view_is_editable(self, node):
-        """ Return whether the node is an editable view. """
-        return node.tag == 'form' or node.tag == 'tree' and node.get('editable')
-
-    def _field_is_editable(self, field, node):
-        """ Return whether a field is editable (not always readonly). """
-        return (
-            (not field.readonly or READONLY.search(str(field.states or ""))) and
-            (node.get('readonly') != "1" or READONLY.search(node.get('attrs') or ""))
-        )
-
-    def get_attrs_symbols(self):
-        """ Return a set of predefined symbols for evaluating attrs. """
-        return {
-            'True', 'False', 'None',    # those are identifiers in Python 2.7
-            'self',
-            'parent',
-            'id',
-            'uid',
-            'context',
-            'context_today',
-            'active_id',
-            'active_ids',
-            'allowed_company_ids',
-            'current_company_id',
-            'active_model',
-            'time',
-            'datetime',
-            'relativedelta',
-            'current_date',
-            'abs',
-            'len',
-            'bool',
-            'float',
-            'str',
-            'unicode',
-        }
-
-    def get_attrs_field_names(self, arch, model, editable):
-        """ Retrieve the field names appearing in context, domain and attrs, and
-            return a list of triples ``(field_name, attr_name, attr_value)``.
-        """
-        VIEW_TYPES = {item[0] for item in type(self).type.selection}
-        symbols = self.get_attrs_symbols() | {None}
-        result = []
-
-        def get_name(node):
-            """ return the name from an AST node, or None """
-            if isinstance(node, ast.Name):
-                return node.id
-
-        def get_subname(get, node):
-            """ return the subfield name from an AST node, or None """
-            if isinstance(node, ast.Attribute) and get(node.value) == 'parent':
-                return node.attr
-
-        def process_expr(expr, get, key, val):
-            """ parse `expr` and collect triples """
-            for node in ast.walk(ast.parse(expr.strip(), mode='eval')):
-                name = get(node)
-                if name not in symbols:
-                    result.append((name, key, val))
-
-        def process_attrs(expr, get, key, val):
-            """ parse `expr` and collect field names in lhs of conditions. """
-            for domain in safe_eval(expr).values():
-                if not isinstance(domain, list):
-                    continue
-                for arg in domain:
-                    if isinstance(arg, (tuple, list)):
-                        process_expr(str(arg[0]), get, key, expr)
-
-        def process(node, model, editable, get=get_name):
-            """ traverse `node` and collect triples """
-            if node.tag in VIEW_TYPES:
-                # determine whether this view is editable
-                editable = editable and self._view_is_editable(node)
-            elif node.tag in ('field', 'groupby'):
-                # determine whether the field is editable
-                field = model._fields.get(node.get('name'))
-                if field:
-                    editable = editable and self._field_is_editable(field, node)
-
-            for key, val in node.items():
-                if not val:
-                    continue
-                if key in ATTRS_WITH_FIELD_NAMES:
-                    process_expr(val, get, key, val)
-                elif key == 'attrs':
-                    process_attrs(val, get, key, val)
-
-            if node.tag in ('field', 'groupby') and field and field.relational:
-                if editable and not node.get('domain'):
-                    domain = field._description_domain(self.env)
-                    # process the field's domain as if it was in the view
-                    if isinstance(domain, str):
-                        process_expr(domain, get, 'domain', domain)
-                # retrieve subfields of 'parent'
-                model = self.env[field.comodel_name]
-                get = partial(get_subname, get)
-
-            for child in node:
-                process(child, model, editable, get)
-
-        process(arch, model, editable)
-        return result
-
     @api.model
     def postprocess_and_fields(self, model, node, view_id):
         """ Return an architecture and a description of all the fields.
@@ -1124,7 +1011,7 @@ actual arch.
         attrs_fields = []
         if self.env.context.get('check_field_names'):
             editable = self.env.context.get('view_is_editable', True)
-            attrs_fields = self.get_attrs_field_names(node, Model, editable)
+            attrs_fields = get_attrs_field_names(self.env, node, Model, editable)
 
         fields_def = self.postprocess(model, node, view_id, False, fields)
         self._postprocess_access_rights(model, node)
diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng
index 66ca9a6135b879716038be7781e8cc4d78368d7c..43a46d8a0e5de044e57e667c48e9839cd20b5440 100644
--- a/odoo/addons/base/rng/common.rng
+++ b/odoo/addons/base/rng/common.rng
@@ -233,6 +233,8 @@
             <rng:optional><rng:attribute name="avg"/></rng:optional>
             <rng:optional><rng:attribute name="select"/></rng:optional>
             <rng:optional><rng:attribute name="group"/></rng:optional>
+            <rng:optional><rng:attribute name="color"/></rng:optional>
+            <rng:optional><rng:attribute name="groupby"/></rng:optional>
             <rng:optional><rng:attribute name="operator"/></rng:optional>
             <rng:optional><rng:attribute name="colspan"/></rng:optional>
             <rng:optional><rng:attribute name="nolabel"/></rng:optional>
diff --git a/odoo/addons/base/rng/search_view.rng b/odoo/addons/base/rng/search_view.rng
index 74dc4c89abd969483465fbb728d7a6bef9b22842..08776e8e2060e082fab4bd9f130e2c8bb7ca2fc5 100644
--- a/odoo/addons/base/rng/search_view.rng
+++ b/odoo/addons/base/rng/search_view.rng
@@ -6,6 +6,17 @@
          template
     -->
     <rng:include href="common.rng"/>
+
+    <rng:define name="searchpanel">
+        <rng:element name="searchpanel">
+            <rng:ref name="overload"/>
+            <rng:optional><rng:attribute name="view_types"/></rng:optional>
+            <rng:zeroOrMore>
+                <rng:ref name="field" />
+            </rng:zeroOrMore>
+        </rng:element>
+    </rng:define>
+
     <rng:define name="search">
         <rng:element name="search">
             <rng:ref name="overload"/>
@@ -17,6 +28,7 @@
                     <rng:ref name="separator"/>
                     <rng:ref name="filter"/>
                     <rng:element name="newline"><rng:empty/></rng:element>
+                    <rng:ref name="searchpanel"/>
                 </rng:choice>
             </rng:zeroOrMore>
         </rng:element>
diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py
index bde57f96a666f350097d20ed06388af1b3c4f370..2400ef117234c5d20db9201199793fc236c49723 100644
--- a/odoo/tools/view_validation.py
+++ b/odoo/tools/view_validation.py
@@ -1,11 +1,15 @@
 """ View validation code (using assertions, not the RNG schema). """
 
+import ast
 import collections
 import logging
 import os
+import re
 
+from functools import partial
 from lxml import etree
 from odoo import tools
+from odoo.tools.safe_eval import safe_eval
 
 _logger = logging.getLogger(__name__)
 
@@ -13,9 +17,140 @@ _logger = logging.getLogger(__name__)
 _validators = collections.defaultdict(list)
 _relaxng_cache = {}
 
-def valid_view(arch):
+# attributes in views that may contain references to field names
+ATTRS_WITH_FIELD_NAMES = {
+    'context',
+    'domain',
+    'decoration-bf',
+    'decoration-it',
+    'decoration-danger',
+    'decoration-info',
+    'decoration-muted',
+    'decoration-primary',
+    'decoration-success',
+    'decoration-warning',
+}
+
+READONLY = re.compile(r"\breadonly\b")
+
+
+def _get_attrs_symbols():
+    """ Return a set of predefined symbols for evaluating attrs. """
+    return {
+        'True', 'False', 'None',    # those are identifiers in Python 2.7
+        'self',
+        'parent',
+        'id',
+        'uid',
+        'context',
+        'context_today',
+        'active_id',
+        'active_ids',
+        'allowed_company_ids',
+        'current_company_id',
+        'active_model',
+        'time',
+        'datetime',
+        'relativedelta',
+        'current_date',
+        'abs',
+        'len',
+        'bool',
+        'float',
+        'str',
+        'unicode',
+    }
+
+
+def _view_is_editable(node):
+    """ Return whether the node is an editable view. """
+    return node.tag == 'form' or node.tag == 'tree' and node.get('editable')
+
+
+def field_is_editable(field, node):
+    """ Return whether a field is editable (not always readonly). """
+    return (
+        (not field.readonly or READONLY.search(str(field.states or ""))) and
+        (node.get('readonly') != "1" or READONLY.search(node.get('attrs') or ""))
+    )
+
+
+def get_attrs_field_names(env, arch, model, editable):
+    """ Retrieve the field names appearing in context, domain and attrs, and
+        return a list of triples ``(field_name, attr_name, attr_value)``.
+    """
+    VIEW_TYPES = {item[0] for item in type(env['ir.ui.view']).type.selection}
+    symbols = _get_attrs_symbols() | {None}
+    result = []
+
+    def get_name(node):
+        """ return the name from an AST node, or None """
+        if isinstance(node, ast.Name):
+            return node.id
+
+    def get_subname(get, node):
+        """ return the subfield name from an AST node, or None """
+        if isinstance(node, ast.Attribute) and get(node.value) == 'parent':
+            return node.attr
+
+    def process_expr(expr, get, key, val):
+        """ parse `expr` and collect triples """
+        for node in ast.walk(ast.parse(expr.strip(), mode='eval')):
+            name = get(node)
+            if name not in symbols:
+                result.append((name, key, val))
+
+    def process_attrs(expr, get, key, val):
+        """ parse `expr` and collect field names in lhs of conditions. """
+        for domain in safe_eval(expr).values():
+            if not isinstance(domain, list):
+                continue
+            for arg in domain:
+                if isinstance(arg, (tuple, list)):
+                    process_expr(str(arg[0]), get, key, expr)
+
+    def process(node, model, editable, get=get_name):
+        """ traverse `node` and collect triples """
+        if node.tag in VIEW_TYPES:
+            # determine whether this view is editable
+            editable = editable and _view_is_editable(node)
+        elif node.tag in ('field', 'groupby'):
+            # determine whether the field is editable
+            field = model._fields.get(node.get('name'))
+            if field:
+                editable = editable and field_is_editable(field, node)
+
+        for key, val in node.items():
+            if not val:
+                continue
+            if key in ATTRS_WITH_FIELD_NAMES:
+                process_expr(val, get, key, val)
+            elif key == 'attrs':
+                process_attrs(val, get, key, val)
+
+        if node.tag in ('field', 'groupby') and field and field.relational:
+            if editable and not node.get('domain'):
+                domain = field._description_domain(env)
+                # process the field's domain as if it was in the view
+                if isinstance(domain, str):
+                    process_expr(domain, get, 'domain', domain)
+            # retrieve subfields of 'parent'
+            model = env[field.comodel_name]
+            get = partial(get_subname, get)
+
+        for child in node:
+            if node.tag == 'search' and child.tag == 'searchpanel':
+                # searchpanel part has to be validated independently
+                continue
+            process(child, model, editable, get)
+
+    process(arch, model, editable)
+    return result
+
+
+def valid_view(arch, **kwargs):
     for pred in _validators[arch.tag]:
-        check = pred(arch)
+        check = pred(arch, **kwargs)
         if not check:
             _logger.error("Invalid XML: %s", pred.__doc__)
             return False
@@ -49,7 +184,7 @@ def relaxng(view_type):
 
 
 @validate('calendar', 'diagram', 'gantt', 'graph', 'pivot', 'search', 'tree', 'activity')
-def schema_valid(arch):
+def schema_valid(arch, **kwargs):
     """ Get RNG validator and validate RNG file."""
     validator = relaxng(arch.tag)
     if validator and not validator.validate(arch):
@@ -60,14 +195,48 @@ def schema_valid(arch):
         return result
     return True
 
+
+@validate('search')
+def valid_searchpanel(arch, **kwargs):
+    """ There must be at most one ``searchpanel`` node in search view archs. """
+    return len(arch.xpath('/search/searchpanel')) <= 1
+
+
+@validate('search')
+def valid_searchpanel_domain_select(arch, **kwargs):
+    """ In the searchpanel, the attribute ``domain`` can only be used on ``field`` nodes with
+        ``select`` attribute set to ``multi``. """
+    for child in arch.xpath('/search/searchpanel/field'):
+        if child.get('domain') and child.get('select') != 'multi':
+            return False
+    return True
+
+
+@validate('search')
+def valid_searchpanel_domain_fields(arch, **kwargs):
+    """ In the searchpanel, fields used in the ``domain`` attribute must be present inside the
+        ``searchpanel`` node with ``select`` attribute not set to ``multi``. """
+    searchpanel = arch.xpath('/search/searchpanel')
+    if searchpanel:
+        env = kwargs['env']
+        model = kwargs['model']
+        attrs_fields = [r[0] for r in get_attrs_field_names(env, searchpanel[0], env[model], False)]
+        non_multi_fields = [
+            c.get('name') for c in arch.xpath('/search/searchpanel/field')
+            if c.get('select') != 'multi'
+        ]
+        return len(set(attrs_fields) - set(non_multi_fields)) == 0
+    return True
+
+
 @validate('form')
-def valid_page_in_book(arch):
+def valid_page_in_book(arch, **kwargs):
     """A `page` node must be below a `notebook` node."""
     return not arch.xpath('//page[not(ancestor::notebook)]')
 
 
 @validate('graph')
-def valid_field_in_graph(arch):
+def valid_field_in_graph(arch, **kwargs):
     """ Children of ``graph`` can only be ``field`` """
     return all(
         child.tag == 'field'
@@ -76,7 +245,7 @@ def valid_field_in_graph(arch):
 
 
 @validate('tree')
-def valid_field_in_tree(arch):
+def valid_field_in_tree(arch, **kwargs):
     """ Children of ``tree`` view must be ``field`` or ``button`` or ``control`` or ``groupby``."""
     return all(
         child.tag in ('field', 'button', 'control', 'groupby')
@@ -85,24 +254,24 @@ def valid_field_in_tree(arch):
 
 
 @validate('form', 'graph', 'tree', 'activity')
-def valid_att_in_field(arch):
+def valid_att_in_field(arch, **kwargs):
     """ ``field`` nodes must all have a ``@name`` """
     return not arch.xpath('//field[not(@name)]')
 
 
 @validate('form')
-def valid_att_in_label(arch):
+def valid_att_in_label(arch, **kwargs):
     """ ``label`` nodes must have a ``@for`` """
     return not arch.xpath('//label[not(@for) and not(descendant::input)]')
 
 
 @validate('form')
-def valid_att_in_form(arch):
+def valid_att_in_form(arch, **kwargs):
     return True
 
 
 @validate('form')
-def valid_type_in_colspan(arch):
+def valid_type_in_colspan(arch, **kwargs):
     """A `colspan` attribute must be an `integer` type."""
     return all(
         attrib.isdigit()
@@ -111,22 +280,24 @@ def valid_type_in_colspan(arch):
 
 
 @validate('form')
-def valid_type_in_col(arch):
+def valid_type_in_col(arch, **kwargs):
     """A `col` attribute must be an `integer` type."""
     return all(
         attrib.isdigit()
         for attrib in arch.xpath('//@col')
     )
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_alternative_image_text(arch):
+def valid_alternative_image_text(arch, **kwargs):
     """An `img` tag must have an alt value."""
     if arch.xpath('//img[not(@alt or @t-att-alt or @t-attf-alt)]'):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_alternative_icon_text(arch):
+def valid_alternative_icon_text(arch, **kwargs):
     """An icon with fa- class or in a button must have aria-label in its tag, parents, descendants or have text."""
     valid_aria_attrs = {
         'aria-label', 'aria-labelledby', 't-att-aria-label', 't-attf-aria-label',
@@ -166,8 +337,9 @@ def valid_alternative_icon_text(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_title_icon(arch):
+def valid_title_icon(arch, **kwargs):
     """An icon with fa- class or in a button must have title in its tag, parents, descendants or have text."""
     valid_title_attrs = {'title', 't-att-title', 't-attf-title'}
     valid_t_attrs = {'t-value', 't-raw', 't-field', 't-esc'}
@@ -205,8 +377,9 @@ def valid_title_icon(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_button(arch):
+def valid_simili_button(arch, **kwargs):
     """A simili button must be tagged with "role='button'"."""
     # Select elements with class 'btn'
     xpath = '//a[contains(concat(" ", @class), " btn")'
@@ -217,8 +390,9 @@ def valid_simili_button(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_dropdown(arch):
+def valid_simili_dropdown(arch, **kwargs):
     """A simili dropdown must be tagged with "role='menu'"."""
     xpath = '//*[contains(concat(" ", @class, " "), " dropdown-menu ")'
     xpath += ' or contains(concat(" ", @t-att-class, " "), " dropdown-menu ")'
@@ -228,8 +402,9 @@ def valid_simili_dropdown(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_progressbar(arch):
+def valid_simili_progressbar(arch, **kwargs):
     """A simili progressbar must be tagged with "role='progressbar'" and have
     aria-valuenow, aria-valuemin and aria-valuemax attributes."""
     # Select elements with class 'btn'
@@ -245,8 +420,9 @@ def valid_simili_progressbar(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_dialog(arch):
+def valid_dialog(arch, **kwargs):
     """A dialog must use role="dialog" and its header, body and footer contents must use <header/>, <main/> and <footer/>."""
     # Select elements with class 'btn'
     xpath = '//*[contains(concat(" ", @class, " "), " modal ")'
@@ -278,8 +454,9 @@ def valid_dialog(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_tabpanel(arch):
+def valid_simili_tabpanel(arch, **kwargs):
     """A tab panel with tab-pane class must have role="tabpanel"."""
     # Select elements with class 'btn'
     xpath = '//*[contains(concat(" ", @class, " "), " tab-pane ")'
@@ -290,8 +467,9 @@ def valid_simili_tabpanel(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_tab(arch):
+def valid_simili_tab(arch, **kwargs):
     """A tab link must have role="tab", a link to an id (without #) by aria-controls."""
     # Select elements with class 'btn'
     xpath = '//*[@data-toggle="tab"]'
@@ -302,8 +480,9 @@ def valid_simili_tab(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_simili_tablist(arch):
+def valid_simili_tablist(arch, **kwargs):
     """A tab list with class nav-tabs must have role="tablist"."""
     # Select elements with class 'btn'
     xpath = '//*[contains(concat(" ", @class, " "), " nav-tabs ")'
@@ -314,8 +493,9 @@ def valid_simili_tablist(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_focusable_button(arch):
+def valid_focusable_button(arch, **kwargs):
     """A simili button must be with a `button`, an `input` (with type `button`, `submit` or `reset`) or a `a` tag."""
     xpath = '//*[contains(concat(" ", @class), " btn")'
     xpath += ' or contains(concat(" ", @t-att-class), " btn")'
@@ -339,16 +519,18 @@ def valid_focusable_button(arch):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_prohibited_none_role(arch):
+def valid_prohibited_none_role(arch, **kwargs):
     """A role can't be `none` or `presentation`. All your elements must be accessible with screen readers, describe it."""
     xpath = '//*[@role="none" or @role="presentation"]'
     if arch.xpath(xpath):
         return "Warning"
     return True
 
+
 @validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
-def valid_alerts(arch):
+def valid_alerts(arch, **kwargs):
     """An alert (class alert-*) must have an alert, alertdialog or status role. Please use alert and alertdialog only for what expects to stop any activity to be read immediatly."""
     xpath = '//*[contains(concat(" ", @class), " alert-")'
     xpath += ' or contains(concat(" ", @t-att-class), " alert-")'