From dd7022eccd99b5ba9688f63b517d23c405f23797 Mon Sep 17 00:00:00 2001
From: "Lucas Perais (lpe)" <lpe@odoo.com>
Date: Thu, 4 Jul 2019 07:40:18 +0000
Subject: [PATCH] [IMP] base,web*: searchpanel in all multi record views

Before this rev. the searchpanel could only be activated in kanban
views (and it was specified in the kanban arch). From now on, the
searchpanel can be activated in any multi record view. It's arch
is moved to the search view arch (inside the <searchpanel> tag).

Task 1985921

Co-authored-by: Aaron Bohy <aab@odoo.com>
---
 .../js/chrome/action_manager_act_window.js    |   5 +-
 .../src/js/views/abstract_controller.js       |  76 ++-
 .../static/src/js/views/abstract_renderer.js  |   4 +
 .../web/static/src/js/views/abstract_view.js  |  67 +-
 .../static/src/js/views/basic/basic_view.js   |   2 +-
 .../src/js/views/calendar/calendar_view.js    |   4 +-
 .../views/control_panel/control_panel_view.js |  10 +-
 .../static/src/js/views/graph/graph_view.js   |   4 +-
 .../src/js/views/kanban/kanban_controller.js  |  59 --
 .../src/js/views/kanban/kanban_renderer.js    |  11 -
 .../static/src/js/views/kanban/kanban_view.js | 129 +---
 .../static/src/js/views/pivot/pivot_view.js   |   4 +-
 .../web/static/src/js/views/qweb/qweb_view.js |   4 +-
 .../src/js/views/{kanban => }/search_panel.js | 143 +++-
 .../web/static/src/js/views/view_dialogs.js   |   1 +
 addons/web/static/src/scss/kanban_view.scss   |  95 ---
 addons/web/static/src/scss/search_panel.scss  |  94 +++
 .../tests/chrome/action_manager_tests.js      |   3 +
 .../static/tests/helpers/test_utils_create.js |   5 +
 .../static/tests/views/search_panel_tests.js  | 637 +++++++++++++++---
 addons/web/views/webclient_templates.xml      |   3 +-
 .../web_diagram/static/src/js/diagram_view.js |   4 +-
 doc/reference/views.rst                       | 107 +--
 odoo/addons/base/models/ir_ui_view.py         | 147 +---
 odoo/addons/base/rng/common.rng               |   2 +
 odoo/addons/base/rng/search_view.rng          |  12 +
 odoo/tools/view_validation.py                 | 230 ++++++-
 27 files changed, 1220 insertions(+), 642 deletions(-)
 rename addons/web/static/src/js/views/{kanban => }/search_panel.js (81%)
 create mode 100644 addons/web/static/src/scss/search_panel.scss

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 c19364fa7a99..ef9273bb4849 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 d8cc6baa8f5a..58a011599d19 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 724b63bae792..afaf275ab5b8 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 dbb1b3e557cf..fc8e73136d8d 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 008bbcf87756..fde334d79d0d 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 90db648ffd36..2ee927a4af09 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 0a140e1f9010..c15840714db1 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 4a91bd67f199..f8125675d4e1 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 bf39906b8e50..a6816b08b392 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 5be98e4de525..ca6f8893cd86 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 65e6fed767b9..e31bff931d14 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 272ce7cb7096..cf58e1abf4b9 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 45c7340d5c16..0c858b5eb6cc 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 536409ba1997..55d2f23c0fa8 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 e35f09a6cc9f..1fb202324951 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 f80d50ba576f..34c71ebb5515 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 000000000000..deac73754d12
--- /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 8a6675ecc584..ac944425a8ca 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 ef890e92e908..b8f42236c246 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 9cb66724e78a..b51ba9c3b3a9 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 6a225d7a6361..f17889bf5c2a 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 c8a46a21990c..f95a0b695642 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 eb94a707c771..8b632bba228e 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 cff628c93497..a110361974a2 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 66ca9a6135b8..43a46d8a0e5d 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 74dc4c89abd9..08776e8e2060 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 bde57f96a666..2400ef117234 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-")'
-- 
GitLab