Merge PR #1629 into 12.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2020-07-06 09:34:35 +00:00
19 changed files with 2037 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
============
Search Panel
============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/12.0/web_view_searchpanel
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_view_searchpanel
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/162/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
With Odoo version 13 a new feature is added which allows kanban views to be
extended by a search panel. This can be defined via XML and is then automatically
added to the view. With this module the function is ported back to version 12.
**Table of contents**
.. contents::
:local:
Usage
=====
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.
* ``groups``: restricts to specific users
* ``string``: determines the label to display
* ``icon``: specifies which icon is used
* ``color``: determines the icon color
Possible values for the ``select`` attribute 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.
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.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_view_searchpanel%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* MuK IT
Contributors
~~~~~~~~~~~~
* Mathias Markl <mathias.markl@mukit.at>
* Enric Tobella <etobella@creublanca.es>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/12.0/web_view_searchpanel>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,27 @@
# Copyright 2017-2019 MuK IT GmbH.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Search Panel',
'summary': 'Kanban Search Panel',
'version': '12.0.1.0.0',
'category': 'Extra Tools',
'license': 'LGPL-3',
'author': 'MuK IT, Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/web',
'depends': [
'web',
],
'data': [
'template/assets.xml',
],
'qweb': [
'static/src/xml/kanban.xml',
],
'images': [
'static/description/banner.png'
],
'demo': [
'demo/res_users_views.xml',
]
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017-2019 MuK IT GmbH.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<record id="view_res_users_kanban" model="ir.ui.view">
<field name="name">res.users.kanban</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_res_users_kanban"/>
<field name="arch" type="xml">
<xpath expr="//templates" position="after">
<searchpanel>
<field name="lang" select="multi"/>
<field name="tz" select="multi"/>
</searchpanel>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
from . import base

View File

@@ -0,0 +1,198 @@
# Copyright 2017-2019 MuK IT GmbH.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.osv import expression
_logger = logging.getLogger(__name__)
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""
Return possible values of the field field_name (case select="one")
and the parent field (if any) used to hierarchize them.
:param field_name: the name of a many2one category field
:return: {
'parent_field': parent field on the comodel of field, or False
'values': array of dictionaries containing some info on the
records available on the comodel of the field 'field_name'.
The display name (and possibly parent_field) are fetched.
}
"""
field = self._fields[field_name]
supported_types = ['many2one']
if field.type not in supported_types:
raise UserError(_(
'Only types %(supported_types)s are supported for category'
'(found type %(field_type)s)'
) % ({
'supported_types': supported_types,
'field_type': field.type
}))
Comodel = self.env[field.comodel_name]
fields = ['display_name']
parent_name = (
Comodel._parent_name if Comodel._parent_name
in Comodel._fields else False
)
if parent_name:
fields.append(parent_name)
model_domain = expression.AND([
kwargs.get('search_domain', []),
kwargs.get('category_domain', []),
kwargs.get('filter_domain', []),
])
return {
'parent_field': parent_name,
'values': Comodel.with_context(
hierarchical_naming=False).search_read(model_domain, fields),
}
@api.model
def search_panel_select_multi_range(self, field_name, **kwargs):
"""
Return possible values of the field field_name (case select="multi"),
possibly with counters and groups.
:param field_name: the name of a filter field;
possible types are many2one, many2many, selection.
:param search_domain: base domain of search
:param category_domain: domain generated by categories
:param filter_domain: domain generated by filters
:param comodel_domain: domain of field values (if relational)
:param group_by: extra field to read on comodel, to group comodel
records
:param disable_counters: whether to count records by value
:return: a list of possible values, each being a dict with keys
'id' (value),
'name' (value label),
'count' (how many records with that value),
'group_id' (value of group),
'group_name' (label of group).
"""
field = self._fields[field_name]
supported_types = ['many2one', 'many2many', 'selection']
if field.type not in supported_types:
raise UserError(_(
'Only types %(supported_types)s are supported for '
'filter (found type %(field_type)s)'
) % ({
'supported_types': supported_types, 'field_type': field.type}))
Comodel = self.env.get(field.comodel_name)
model_domain = expression.AND([
kwargs.get('search_domain', []),
kwargs.get('category_domain', []),
kwargs.get('filter_domain', []),
[(field_name, '!=', False)],
])
comodel_domain = kwargs.get('comodel_domain', [])
disable_counters = kwargs.get('disable_counters', False)
group_by = kwargs.get('group_by', False)
if group_by:
# determine the labeling of values returned by the group_by field
group_by_field = Comodel._fields[group_by]
if group_by_field.type == 'many2one':
def group_id_name(value):
return value or (False, _("Not Set"))
elif group_by_field.type == 'selection':
desc = Comodel.fields_get([group_by])[group_by]
group_by_selection = dict(desc['selection'])
group_by_selection[False] = _("Not Set")
def group_id_name(value):
return value, group_by_selection[value]
else:
def group_id_name(value):
return (value, value) if value else (False, _("Not Set"))
# get filter_values
filter_values = []
if field.type == 'many2one':
counters = {}
if not disable_counters:
groups = self.read_group(
model_domain, [field_name], [field_name])
counters = {
group[field_name][0]: group[field_name + '_count']
for group in groups
}
# retrieve all possible values, and return them with their label
# and counter
field_names = ['display_name']
if group_by:
field_names.append(group_by)
records = Comodel.search_read(comodel_domain, field_names)
for record in records:
record_id = record['id']
values = {
'id': record_id,
'name': record['display_name'],
'count': counters.get(record_id, 0),
}
if group_by:
values['group_id'], values['group_name'] = group_id_name(
record[group_by])
filter_values.append(values)
elif field.type == 'many2many':
# retrieve all possible values, and return them with their label
# and counter
field_names = ['display_name']
if group_by:
field_names.append(group_by)
records = Comodel.search_read(comodel_domain, field_names)
for record in records:
record_id = record['id']
values = {
'id': record_id,
'name': record['display_name'],
'count': 0,
}
if not disable_counters:
count_domain = expression.AND([
model_domain, [(field_name, 'in', record_id)]])
values['count'] = self.search_count(count_domain)
if group_by:
values['group_id'], values['group_name'] = group_id_name(
record[group_by])
filter_values.append(values)
elif field.type == 'selection':
counters = {}
if not disable_counters:
groups = self.read_group(
model_domain, [field_name], [field_name])
counters = {
group[field_name]: group[field_name + '_count']
for group in groups
}
# retrieve all possible values, and return them with their label
# and counter
selection = self.fields_get([field_name])[field_name]['selection']
for value, label in selection:
filter_values.append({
'id': value,
'name': label,
'count': counters.get(value, 0),
})
return filter_values

View File

@@ -0,0 +1,2 @@
* Mathias Markl <mathias.markl@mukit.at>
* Enric Tobella <etobella@creublanca.es>

View File

@@ -0,0 +1,3 @@
With Odoo version 13 a new feature is added which allows kanban views to be
extended by a search panel. This can be defined via XML and is then automatically
added to the view. With this module the function is ported back to version 12.

View File

@@ -0,0 +1,40 @@
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.
* ``groups``: restricts to specific users
* ``string``: determines the label to display
* ``icon``: specifies which icon is used
* ``color``: determines the icon color
Possible values for the ``select`` attribute 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.
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,462 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Search Panel</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="search-panel">
<h1 class="title">Search Panel</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_view_searchpanel"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_view_searchpanel"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/162/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>With Odoo version 13 a new feature is added which allows kanban views to be
extended by a search panel. This can be defined via XML and is then automatically
added to the view. With this module the function is ported back to version 12.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>This tool allows to quickly filter data on the basis of given fields. The fields
are specified as direct children of the <tt class="docutils literal">searchpanel</tt> with tag name <tt class="docutils literal">field</tt>,
and the following attributes:</p>
<ul class="simple">
<li><tt class="docutils literal">name</tt> (mandatory) the name of the field to filter on</li>
<li><tt class="docutils literal">select</tt> determines the behavior and display.</li>
<li><tt class="docutils literal">groups</tt>: restricts to specific users</li>
<li><tt class="docutils literal">string</tt>: determines the label to display</li>
<li><tt class="docutils literal">icon</tt>: specifies which icon is used</li>
<li><tt class="docutils literal">color</tt>: determines the icon color</li>
</ul>
<p>Possible values for the <tt class="docutils literal">select</tt> attribute are</p>
<ul class="simple">
<li><tt class="docutils literal">one</tt> (default) at most one value can be selected. Supported field types are many2one and selection.</li>
<li><tt class="docutils literal">multi</tt> several values can be selected (checkboxes). Supported field types are many2one, many2many and selection.</li>
</ul>
<p>Additional optional attributes are available in the <tt class="docutils literal">multi</tt> case:</p>
<ul class="simple">
<li><tt class="docutils literal">domain</tt>: determines conditions that the comodel records have to satisfy.</li>
</ul>
<p>A domain might be used to express a dependency on another field (with select=”one”)
of the search panel. Consider</p>
<pre class="code xml literal-block">
<span class="nt">&lt;searchpanel&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;department_id&quot;</span><span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;manager_id&quot;</span> <span class="na">select=</span><span class="s">&quot;multi&quot;</span> <span class="na">domain=</span><span class="s">&quot;[('department_id', '=', department_id)]&quot;</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/searchpanel&gt;</span>
</pre>
<p>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 <tt class="docutils literal">department_id</tt>.</p>
<ul class="simple">
<li><tt class="docutils literal">groupby</tt>: field name of the comodel (only available for many2one and many2many fields). Values will be grouped by that field.</li>
<li><tt class="docutils literal">disable_counters</tt>: default is false. If set to true the counters wont be computed.</li>
</ul>
<p>This feature has been implemented in case performances would be too bad.</p>
<p>Another way to solve performance issues is to properly override the <tt class="docutils literal">search_panel_select_multi_range</tt> method.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_view_searchpanel%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>MuK IT</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<ul class="simple">
<li>Mathias Markl &lt;<a class="reference external" href="mailto:mathias.markl&#64;mukit.at">mathias.markl&#64;mukit.at</a>&gt;</li>
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_view_searchpanel">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
/**
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**/
odoo.define('web_view_searchpanel.KanbanController', function (require) {
"use strict";
var KanbanController = require('web.KanbanController');
KanbanController.include({
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
search_panel_domain_updated: '_onSearchPanelDomainUpdated',
}),
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this._searchPanel = params.searchPanel;
this.controlPanelDomain = params.controlPanelDomain || [];
this.searchPanelDomain = this._searchPanel ?
this._searchPanel.getDomain() : [];
},
start: function () {
if (this._searchPanel) {
this.$el.addClass('o_kanban_with_searchpanel');
this.$el.prepend(this._searchPanel.$el);
}
return this._super.apply(this, arguments);
},
update: function (params) {
if (!this._searchPanel) {
return this._super.apply(this, arguments);
}
var self = this;
if (params.domain) {
this.controlPanelDomain = params.domain;
}
params.noRender = true;
params.domain = this.controlPanelDomain.concat(
this.searchPanelDomain);
var superProm = this._super.apply(this, arguments);
var searchPanelProm = this._updateSearchPanel();
return $.when(superProm, searchPanelProm).then(function () {
return self.renderer.render();
});
},
_updateSearchPanel: function () {
return this._searchPanel.update({
searchDomain: this.controlPanelDomain,
});
},
_onSearchPanelDomainUpdated: function (ev) {
this.searchPanelDomain = ev.data.domain;
this.reload({offset: 0});
},
});
});

View File

@@ -0,0 +1,19 @@
/**
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**/
odoo.define('web_view_searchpanel.KanbanRenderer', function (require) {
"use strict";
var KanbanRenderer = require('web.KanbanRenderer');
KanbanRenderer.include({
render: function () {
return this._render();
},
});
});

View File

@@ -0,0 +1,631 @@
/**********************************************************************************
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**********************************************************************************/
odoo.define('web_view_searchpanel.SearchPanel', function (require) {
"use strict";
var core = require('web.core');
var config = require('web.config');
var Domain = require('web.Domain');
var Widget = require('web.Widget');
var qweb = core.qweb;
var SearchPanel = Widget.extend({
className: 'o_search_panel',
events: {
'click .o_search_panel_category_value header': '_onCategoryValueClicked',
'click .o_search_panel_category_value .o_toggle_fold': '_onToggleFoldCategory',
'click .o_search_panel_filter_group .o_toggle_fold': '_onToggleFoldFilterGroup',
'change .o_search_panel_filter_value > div > input': '_onFilterValueChanged',
'change .o_search_panel_filter_group > div > input': '_onFilterGroupChanged',
},
/**
* @override
* @param {Object} params
* @param {Object} [params.defaultCategoryValues={}] the category value to
* activate by default, for each category
* @param {Object} params.fields
* @param {string} params.model
* @param {Object} params.sections
* @param {Array[]} params.searchDomain domain coming from controlPanel
*/
init: function (parent, params) {
this._super.apply(this, arguments);
this.categories = _.pick(params.sections, function (section) {
return section.type === 'category';
});
this.filters = _.pick(params.sections, function (section) {
return section.type === 'filter';
});
this.defaultCategoryValues = params.defaultCategoryValues || {};
this.fields = params.fields;
this.model = params.model;
this.searchDomain = params.searchDomain;
this.loadProm = $.Deferred();
this.loadPromLazy = true;
},
willStart: function () {
var self = this;
var loading = $.Deferred();
var loadPromTimer = setTimeout(function () {
if(loading.state() !== 'resolved') {
loading.resolve();
}
}, this.loadPromMaxTime || 1000);
this._fetchCategories().then(function () {
self._fetchFilters().then(function () {
if(loading.state() !== 'resolved') {
clearTimeout(loadPromTimer);
self.loadPromLazy = false;
loading.resolve();
}
self.loadProm.resolve();
});
});
return $.when(loading, this._super.apply(this, arguments));
},
start: function () {
var self = this;
if(this.loadProm.state() !== 'resolved') {
this.$el.html($("<div/>", {
'class': "o_search_panel_loading",
'html': "<i class='fa fa-spinner fa-pulse' />"
}));
}
this.loadProm.then(function() {
self._render();
});
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Array[]} the current searchPanel domain based on active
* categories and checked filters
*/
getDomain: function () {
return this._getCategoryDomain().concat(this._getFilterDomain());
},
/**
* Reload the filters and re-render. Note that we only reload the filters if
* the controlPanel domain or searchPanel domain has changed.
*
* @param {Object} params
* @param {Array[]} params.searchDomain domain coming from controlPanel
* @returns {$.Promise}
*/
update: function (params) {
if(this.loadProm.state() === 'resolved') {
var newSearchDomainStr = JSON.stringify(params.searchDomain);
var currentSearchDomainStr = JSON.stringify(this.searchDomain);
if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) {
this.needReload = false;
this.searchDomain = params.searchDomain;
this._fetchFilters().then(this._render.bind(this));
} else {
this._render();
}
}
return $.when();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} categoryId
* @param {Object[]} values
*/
_createCategoryTree: function (categoryId, values) {
var category = this.categories[categoryId];
var parentField = category.parentField;
category.values = {};
values.forEach(function (value) {
category.values[value.id] = _.extend({}, value, {
childrenIds: [],
folded: true,
parentId: value[parentField] && value[parentField][0] || false,
});
});
Object.keys(category.values).forEach(function (valueId) {
var value = category.values[valueId];
if (value.parentId && category.values[value.parentId]) {
category.values[value.parentId].childrenIds.push(value.id);
} else {
value.parentId = false;
value[parentField] = false;
}
});
category.rootIds = Object.keys(category.values).filter(function (valueId) {
var value = category.values[valueId];
return value.parentId === false;
});
category.activeValueId = false;
if(!this.loadPromLazy) {
// set active value
var validValues = _.pluck(category.values, 'id').concat([false]);
// set active value from context
var value = this.defaultCategoryValues[category.fieldName];
// if not set in context, or set to an unknown value, set active value
// from localStorage
if (!_.contains(validValues, value)) {
var storageKey = this._getLocalStorageKey(category);
value = this.call('local_storage', 'getItem', storageKey);
}
// if not set in localStorage either, select 'All'
category.activeValueId = _.contains(validValues, value) ? value : false;
// unfold ancestor values of active value to make it is visible
if (category.activeValueId) {
var parentValueIds = this._getAncestorValueIds(category, category.activeValueId);
parentValueIds.forEach(function (parentValue) {
category.values[parentValue].folded = false;
});
}
}
},
/**
* @private
* @param {string} filterId
* @param {Object[]} values
*/
_createFilterTree: function (filterId, values) {
var filter = this.filters[filterId];
// restore checked property
values.forEach(function (value) {
var oldValue = filter.values && filter.values[value.id];
value.checked = oldValue && oldValue.checked || false;
});
filter.values = {};
var groupIds = [];
if (filter.groupBy) {
var groups = {};
values.forEach(function (value) {
var groupId = value.group_id;
if (!groups[groupId]) {
if (groupId) {
groupIds.push(groupId);
}
groups[groupId] = {
folded: false,
id: groupId,
name: value.group_name,
values: {},
tooltip: value.group_tooltip,
sequence: value.group_sequence,
sortedValueIds: [],
};
// restore former checked and folded state
var oldGroup = filter.groups && filter.groups[groupId];
groups[groupId].state = oldGroup && oldGroup.state || false;
groups[groupId].folded = oldGroup && oldGroup.folded || false;
}
groups[groupId].values[value.id] = value;
groups[groupId].sortedValueIds.push(value.id);
});
filter.groups = groups;
filter.sortedGroupIds = _.sortBy(groupIds, function (groupId) {
return groups[groupId].sequence || groups[groupId].name;
});
Object.keys(filter.groups).forEach(function (groupId) {
filter.values = _.extend(filter.values, filter.groups[groupId].values);
});
} else {
values.forEach(function (value) {
filter.values[value.id] = value;
});
filter.sortedValueIds = values.map(function (value) {
return value.id;
});
}
},
/**
* Fetch values for each category. This is done only once, at startup.
*
* @private
* @returns {$.Promise} resolved when all categories have been fetched
*/
_fetchCategories: function () {
var self = this;
var defs = Object.keys(this.categories).map(function (categoryId) {
var category = self.categories[categoryId];
var field = self.fields[category.fieldName];
var def;
if (field.type === 'selection') {
var values = field.selection.map(function (value) {
return {id: value[0], display_name: value[1]};
});
def = $.when(values);
} else {
var categoryDomain = self._getCategoryDomain();
var filterDomain = self._getFilterDomain();
def = self._rpc({
method: 'search_panel_select_range',
model: self.model,
args: [category.fieldName],
kwargs: {
category_domain: categoryDomain,
filter_domain: filterDomain,
search_domain: self.searchDomain,
},
}, {
shadow: true,
}).then(function (result) {
category.parentField = result.parent_field;
return result.values;
});
}
return def.then(function (values) {
self._createCategoryTree(categoryId, values);
});
});
return $.when.apply($, defs);
},
/**
* Fetch values for each filter. This is done at startup, and at each reload
* (when the controlPanel or searchPanel domain changes).
*
* @private
* @returns {$.Promise} resolved when all filters have been fetched
*/
_fetchFilters: function () {
var self = this;
var evalContext = {};
Object.keys(this.categories).forEach(function (categoryId) {
var category = self.categories[categoryId];
evalContext[category.fieldName] = category.activeValueId;
});
var categoryDomain = this._getCategoryDomain();
var filterDomain = this._getFilterDomain();
var defs = Object.keys(this.filters).map(function (filterId) {
var filter = self.filters[filterId];
return self._rpc({
method: 'search_panel_select_multi_range',
model: self.model,
args: [filter.fieldName],
kwargs: {
category_domain: categoryDomain,
comodel_domain: Domain.prototype.stringToArray(filter.domain, evalContext),
disable_counters: filter.disableCounters,
filter_domain: filterDomain,
group_by: filter.groupBy || false,
search_domain: self.searchDomain,
},
}, {
shadow: true,
}).then(function (values) {
self._createFilterTree(filterId, values);
});
});
return $.when.apply($, defs);
},
/**
* Compute and return the domain based on the current active categories.
*
* @private
* @returns {Array[]}
*/
_getCategoryDomain: function () {
var self = this;
function categoryToDomain(domain, categoryId) {
var category = self.categories[categoryId];
if (category.activeValueId) {
domain.push([category.fieldName, '=', category.activeValueId]);
} else if(self.loadPromLazy && self.loadProm.state() !== 'resolved') {
var value = self.defaultCategoryValues[category.fieldName];
if (value) {
domain.push([category.fieldName, '=', value]);
}
}
return domain;
}
return Object.keys(this.categories).reduce(categoryToDomain, []);
},
/**
* Compute and return the domain based on the current checked filters.
* The values of a single filter are combined using a simple rule: checked values within
* a same group are combined with an 'OR' (this is expressed as single condition using a list)
* and groups are combined with an 'AND' (expressed by concatenation of conditions).
* If a filter has no groups, its checked values are implicitely considered as forming
* a group (and grouped using an 'OR').
*
* @private
* @returns {Array[]}
*/
_getFilterDomain: function () {
var self = this;
function getCheckedValueIds(values) {
return Object.keys(values).reduce(function (checkedValues, valueId) {
if (values[valueId].checked) {
checkedValues.push(values[valueId].id);
}
return checkedValues;
}, []);
}
function filterToDomain(domain, filterId) {
var filter = self.filters[filterId];
if (filter.groups) {
Object.keys(filter.groups).forEach(function (groupId) {
var group = filter.groups[groupId];
var checkedValues = getCheckedValueIds(group.values);
if (checkedValues.length) {
domain.push([filter.fieldName, 'in', checkedValues]);
}
});
} else if (filter.values) {
var checkedValues = getCheckedValueIds(filter.values);
if (checkedValues.length) {
domain.push([filter.fieldName, 'in', checkedValues]);
}
}
return domain;
}
return Object.keys(this.filters).reduce(filterToDomain, []);
},
/**
* The active id of each category is stored in the localStorage, s.t. it
* can be restored afterwards (when the action is reloaded, for instance).
* This function returns the key in the sessionStorage for a given category.
*
* @param {Object} category
* @returns {string}
*/
_getLocalStorageKey: function (category) {
return 'searchpanel_' + this.model + '_' + category.fieldName;
},
/**
* @private
* @param {Object} category
* @param {integer} categoryValueId
* @returns {integer[]} list of ids of the ancestors of the given value in
* the given category
*/
_getAncestorValueIds: function (category, categoryValueId) {
var categoryValue = category.values[categoryValueId];
var parentId = categoryValue.parentId;
if (parentId) {
return [parentId].concat(this._getAncestorValueIds(category, parentId));
}
return [];
},
/**
* @private
*/
_render: function () {
var self = this;
this.$el.empty();
// sort categories and filters according to their index
var categories = Object.keys(this.categories).map(function (categoryId) {
return self.categories[categoryId];
});
var filters = Object.keys(this.filters).map(function (filterId) {
return self.filters[filterId];
});
var sections = categories.concat(filters).sort(function (s1, s2) {
return s1.index - s2.index;
});
sections.forEach(function (section) {
if (Object.keys(section.values).length) {
if (section.type === 'category') {
self.$el.append(self._renderCategory(section));
} else {
self.$el.append(self._renderFilter(section));
}
}
});
},
/**
* @private
* @param {Object} category
* @returns {string}
*/
_renderCategory: function (category) {
return qweb.render('SearchPanel.Category', {category: category});
},
/**
* @private
* @param {Object} filter
* @returns {jQuery}
*/
_renderFilter: function (filter) {
var $filter = $(qweb.render('SearchPanel.Filter', {filter: filter}));
// set group inputs in indeterminate state when necessary
Object.keys(filter.groups || {}).forEach(function (groupId) {
var state = filter.groups[groupId].state;
// group 'false' is not displayed
if (groupId !== 'false' && state === 'indeterminate') {
$filter
.find('.o_search_panel_filter_group[data-group-id=' + groupId + '] input')
.get(0)
.indeterminate = true;
}
});
return $filter;
},
/**
* Compute the current searchPanel domain based on categories and filters,
* and notify environment of the domain change.
*
* Note that this assumes that the environment will update the searchPanel.
* This is done as such to ensure the coordination between the reloading of
* the searchPanel and the reloading of the data.
*
* @private
*/
_notifyDomainUpdated: function () {
this.needReload = true;
this.trigger_up('search_panel_domain_updated', {
domain: this.getDomain(),
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onCategoryValueClicked: function (ev) {
ev.stopPropagation();
var $item = $(ev.currentTarget).closest('.o_search_panel_category_value');
var category = this.categories[$item.data('categoryId')];
var valueId = $item.data('id') || false;
category.activeValueId = valueId;
var storageKey = this._getLocalStorageKey(category);
this.call('local_storage', 'setItem', storageKey, valueId);
this._notifyDomainUpdated();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onFilterGroupChanged: function (ev) {
ev.stopPropagation();
var $item = $(ev.target).closest('.o_search_panel_filter_group');
var filter = this.filters[$item.data('filterId')];
var groupId = $item.data('groupId');
var group = filter.groups[groupId];
group.state = group.state === 'checked' ? 'unchecked' : 'checked';
Object.keys(group.values).forEach(function (valueId) {
group.values[valueId].checked = group.state === 'checked';
});
this._notifyDomainUpdated();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onFilterValueChanged: function (ev) {
ev.stopPropagation();
var $item = $(ev.target).closest('.o_search_panel_filter_value');
var valueId = $item.data('valueId');
var filter = this.filters[$item.data('filterId')];
var value = filter.values[valueId];
value.checked = !value.checked;
var group = filter.groups && filter.groups[value.group_id];
if (group) {
var valuePartition = _.partition(Object.keys(group.values), function (valueId) {
return group.values[valueId].checked;
});
if (valuePartition[0].length && valuePartition[1].length) {
group.state = 'indeterminate';
} else if (valuePartition[0].length) {
group.state = 'checked';
} else {
group.state = 'unchecked';
}
}
this._notifyDomainUpdated();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onToggleFoldCategory: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $item = $(ev.currentTarget).closest('.o_search_panel_category_value');
var category = this.categories[$item.data('categoryId')];
var valueId = $item.data('id');
category.values[valueId].folded = !category.values[valueId].folded;
this._render();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onToggleFoldFilterGroup: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $item = $(ev.currentTarget).closest('.o_search_panel_filter_group');
var filter = this.filters[$item.data('filterId')];
var groupId = $item.data('groupId');
filter.groups[groupId].folded = !filter.groups[groupId].folded;
this._render();
},
});
if (config.device.isMobile) {
SearchPanel.include({
tagName: 'details',
_getCategorySelection: function () {
var self = this;
return Object.keys(this.categories).reduce(function (selection, categoryId) {
var category = self.categories[categoryId];
if (category.activeValueId) {
var ancestorIds = [category.activeValueId].concat(self._getAncestorValueIds(category, category.activeValueId));
var breadcrumb = ancestorIds.map(function (valueId) {
return category.values[valueId].display_name;
});
selection.push({ breadcrumb: breadcrumb, icon: category.icon, color: category.color});
}
return selection;
}, []);
},
_getFilterSelection: function () {
var self = this;
return Object.keys(this.filters).reduce(function (selection, filterId) {
var filter = self.filters[filterId];
if (filter.groups) {
Object.keys(filter.groups).forEach(function (groupId) {
var group = filter.groups[groupId];
Object.keys(group.values).forEach(function (valueId) {
var value = group.values[valueId];
if (value.checked) {
selection.push({name: value.name, icon: filter.icon, color: filter.color});
}
});
});
} else if (filter.values) {
Object.keys(filter.values).forEach(function (valueId) {
var value = filter.values[valueId];
if (value.checked) {
selection.push({name: value.name, icon: filter.icon, color: filter.color});
}
});
}
return selection;
}, []);
},
_render: function () {
this._super.apply(this, arguments);
this.$el.prepend(qweb.render('SearchPanel.MobileSummary', {
categories: this._getCategorySelection(),
filterValues: this._getFilterSelection(),
separator: ' / ',
}));
},
});
}
return SearchPanel;
});

View File

@@ -0,0 +1,118 @@
/**
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**/
odoo.define('web_view_searchpanel.KanbanView', function (require) {
"use strict";
var pyUtils = require('web.py_utils');
var KanbanView = require('web.KanbanView');
var SearchPanel = require('web.SearchPanel');
KanbanView.include({
config: _.extend({}, KanbanView.prototype.config, {
SearchPanel: SearchPanel,
}),
init: function (viewInfo, params) {
this.searchPanelSections = Object.create(null);
this._super.apply(this, arguments);
this.hasSearchPanel = !_.isEmpty(this.searchPanelSections);
},
getController: function (parent) {
var self = this;
var def = undefined;
if (this.hasSearchPanel) {
def = this._createSearchPanel(parent);
}
var _super = this._super.bind(this);
return $.when(def).then(function (searchPanel) {
return _super(parent).done(function (controller) {
if (self.hasSearchPanel) {
self.controllerParams.searchPanel.setParent(
controller);
}
return controller
});
});
},
_createSearchPanel: function (parent) {
var self = this;
var defaultCategoryValues = {};
Object.keys(this.loadParams.context).forEach(function (key) {
var match = /^searchpanel_default_(.*)$/.exec(key);
if (match) {
defaultCategoryValues[
match[1]
] = self.loadParams.context[key];
}
});
var controlPanelDomain = this.loadParams.domain;
var searchPanel = new this.config.SearchPanel(parent, {
defaultCategoryValues: defaultCategoryValues,
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);
});
},
_processNode: function (node, fv) {
if (node.tag === 'searchpanel') {
this._processSearchPanelNode(node, fv);
return false;
}
return this._super.apply(this, arguments);
},
_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;
});
},
});
});

View File

@@ -0,0 +1,158 @@
/**********************************************************************************
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**********************************************************************************/
.o_kanban_with_searchpanel {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
.o_onboarding_container {
flex: 1 1 100%;
}
.o_kanban_view {
flex: 1 1 calc(100% - #{$o-searchpanel-w});
overflow: auto;
max-height: 100%;
}
.o_search_panel {
flex: 0 0 $o-searchpanel-w;
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;
position: relative;
.o_search_panel_loading {
position: absolute;
margin-top: -30px;
margin-left: -30px;
margin-bottom: 0;
margin-right: 0;
font-size: 60px;
height: 60px;
width: 60px;
left: 50%;
top: 50%;
}
.o_search_panel_category .o_search_panel_section_icon {
color: $o-searchpanel-category-default-color;
}
.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;
}
}
}
}
}
}
@include media-breakpoint-down(sm) {
.o_kanban_with_searchpanel {
flex-direction: column;
.o_onboarding_container {
display: none;
}
details.o_search_panel {
flex-basis: auto;
height: auto;
width: 100%;
padding: 0;
> summary {
padding: $o-searchpanel-p-small;
// Hide the caret. For details see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary
list-style-type: none;
&::-webkit-details-marker {
display: none
}
.badge {
font-size: 100%;
background-color: white;
}
}
> .o_search_panel_section {
margin: 0 $o-searchpanel-p-small 0 $o-searchpanel-p;
}
&[open] {
z-index: $zindex-dropdown;
> summary {
background-color: $list-group-action-active-bg;
}
.fa-chevron-left:before {
content: "\f078";
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
/**********************************************************************************
*
* Copyright 2017-2019 MuK IT GmbH.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
*
**********************************************************************************/
$o-searchpanel-w: 220px;
$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: $o-brand-odoo;

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017-2019 MuK IT GmbH.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="template" xml:space="preserve">
<t t-name="SearchPanel.SectionHeader">
<header class="o_search_panel_section_header text-uppercase">
<i t-attf-class="mr-1 fa #{section.icon} o_search_panel_section_icon" t-attf-style="#{section.color ? ('color: ' + section.color) : ''}"/>
<b><t t-esc="section.description"/></b>
</header>
</t>
<t t-name="SearchPanel.Category">
<section class="o_search_panel_section o_search_panel_category">
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="category"/>
</t>
<ul class="list-group d-block">
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-category-id="category.id">
<header t-att-class="'list-group-item-action' + (!category.activeValueId ? ' active' : '')">
<label class="o_search_panel_label mb0 d-block">
<span class="o_search_panel_label_title"><b>All</b></span>
</label>
</header>
</li>
<t t-call="SearchPanel.CategoryValues">
<t t-set="values" t-value="category.rootIds"/>
</t>
</ul>
</section>
</t>
<t t-name="SearchPanel.CategoryValues">
<t t-foreach="values" t-as="valueId">
<t t-set="value" t-value="category.values[valueId]"/>
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-id="value.id" t-att-data-category-id="category.id">
<header t-att-class="'list-group-item-action' + (value.id === category.activeValueId ? ' active' : '')">
<label t-att-for="value.display_name" class="o_search_panel_label mb0 d-block">
<i t-if="value.childrenIds.length" t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (value.folded ? 'fa-caret-left' : 'fa-caret-down')"/>
<span class="o_search_panel_label_title"><t t-esc="value.display_name"/></span>
</label>
</header>
<ul t-if="!value.folded" class="list-group d-block">
<t t-call="SearchPanel.CategoryValues">
<t t-set="values" t-value="value.childrenIds"/>
</t>
</ul>
</li>
</t>
</t>
<t t-name="SearchPanel.Filter">
<section class="o_search_panel_section o_search_panel_filter">
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="filter"/>
</t>
<ul class="list-group d-block">
<t t-if="filter.groups" t-call="SearchPanel.FilterGroups">
<t t-set="groups" t-value="filter.groups"/>
</t>
<t t-else="" t-call="SearchPanel.FilterValues">
<t t-set="sortedValueIds" t-value="filter.sortedValueIds"/>
<t t-set="values" t-value="filter.values"/>
</t>
</ul>
</section>
</t>
<t t-name="SearchPanel.FilterGroups">
<li t-foreach="filter.sortedGroupIds" t-as="groupId" t-att-data-group-id="groupId" t-att-data-filter-id="filter.id"
class="o_search_panel_filter_group list-group-item border-0">
<t t-set="group" t-value="groups[groupId]"/>
<div class="custom-control custom-checkbox">
<t t-set="inputId" t-value="_.uniqueId('input_')"/>
<input type="checkbox" t-att-id="inputId" t-att-checked="group.state === 'checked' ? 'checked' : undefined" class="custom-control-input"/>
<label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group.tooltip">
<i t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (group.folded ? 'fa-caret-left' : 'fa-caret-down')"/>
<span class="o_search_panel_label_title"><t t-esc="group.name"/></span>
</label>
</div>
<ul t-if="!group.folded" class="list-group d-block">
<t t-call="SearchPanel.FilterValues">
<t t-set="sortedValueIds" t-value="group.sortedValueIds"/>
<t t-set="values" t-value="group.values"/>
</t>
</ul>
</li>
<ul t-if="groups['false']" class="list-group d-block">
<t t-call="SearchPanel.FilterValues">
<t t-set="group" t-value="groups['false']"/>
<t t-set="sortedValueIds" t-value="group.sortedValueIds"/>
<t t-set="values" t-value="group.values"/>
</t>
</ul>
</t>
<t t-name="SearchPanel.FilterValues">
<li t-foreach="sortedValueIds" t-as="valueId" t-att-data-value-id="valueId" t-att-data-filter-id="filter.id"
class="o_search_panel_filter_value list-group-item border-0">
<t t-set="value" t-value="values[valueId]"/>
<div class="custom-control custom-checkbox">
<t t-set="inputId" t-value="_.uniqueId('input_')"/>
<input type="checkbox" t-att-id="inputId" t-att-checked="value.checked ? 'checked' : undefined" class="custom-control-input"/>
<label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group &amp;&amp; group.tooltip">
<span class="o_search_panel_label_title"><t t-esc="value.name"/></span>
<span t-if="value.count > 0" class="pull-right text-muted mr-2 mt-1 small"><t t-esc="value.count"/></span>
<span t-if="filter.disableCounters" class="pull-right text-muted mr-2 mt-1 small">?</span>
</label>
</div>
</li>
</t>
<t t-name="SearchPanel.MobileSummary">
<t t-set="emptySelection" t-value="!categories.length &amp; !filterValues.length"/>
<summary class="d-flex align-items-center">
<div class="text-truncate font-italic ml-2 mr-auto">
<t t-if="emptySelection">Filters...</t>
<span t-foreach="categories" t-as="category" class="o_search_panel_category mr-1">
<i t-if="category.icon" t-attf-class="o_search_panel_section_icon fa fa-w #{category.icon}" t-attf-style="#{category.color ? ('color: ' + category.color) : undefined}"/>
<t t-esc="category.breadcrumb.join(separator)"/>
</span>
<span t-foreach="filterValues" t-as="filterValue" class="o_search_panel_filter mr-1">
<i t-if="filterValue.icon" t-attf-class="o_search_panel_section_icon fa fa-w #{filterValue.icon}" t-attf-style="#{filterValue.color ? ('color: ' + filterValue.color) : undefined}"/>
<t t-esc="filterValue.name"/>
</span>
</div>
<i class="fa fa-fw fa-chevron-left"/>
</summary>
</t>
</templates>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017-2019 MuK IT GmbH.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template id="assets_backend" name="Backend Assets" inherit_id="web.assets_backend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/css" href="/web_view_searchpanel/static/src/scss/variables.scss" />
<link rel="stylesheet" type="text/css" href="/web_view_searchpanel/static/src/scss/kanban_view.scss" />
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_searchpanel.js" />
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_controller.js" />
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_renderer.js" />
<script type="text/javascript" src="/web_view_searchpanel/static/src/js/kanban_view.js" />
</xpath>
</template>
</odoo>