3
0
Fork 0

[MIG] web_advanced_filter

12.0
Holger Brunn 2020-04-21 23:47:25 +02:00 committed by Jan Verbeek
parent 969446594c
commit bbeddba6e1
35 changed files with 1481 additions and 854 deletions

View File

@ -0,0 +1,100 @@
================
Advanced filters
================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! 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-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-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_advanced_filter
: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_advanced_filter
: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|
This addon allows users to apply set operations on filters: Remove or add
certain ids from/to a selection, but also to remove or add another filter's
outcome from/to a filter. This can be stacked, so the filter domain can be
arbitrarily complicated.
The math is hidden from the user as far as possible, in the hope it's still
user friendly.
**Table of contents**
.. contents::
:local:
Usage
=====
After this addon is installed, every search view shows a new menu `Advanced
filters`. Here the set operations can be applied as necessary.
Note the menu is only visible when there's search criteria or some record
selected.
Known issues / Roadmap
======================
* client side tests
* consider renaming all instances of `Favorites` in core to `Filters` for consistency
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_advanced_filter%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
~~~~~~~
* Therp BV
Contributors
~~~~~~~~~~~~
* Holger Brunn <hbrunn@therp.nl>
Other credits
~~~~~~~~~~~~~
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
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_advanced_filter>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,18 @@
from . import models
from . import wizards
def pre_init_hook(cr):
"""take care to move the domain field to domain_this to keep the values"""
cr.execute(
'SELECT count(attname) FROM pg_attribute '
'WHERE attrelid = '
'(SELECT oid FROM pg_class WHERE relname = %s) '
'AND attname = %s', ('ir_filters', 'domain_this'))
if not cr.fetchone()[0]:
cr.execute('ALTER table ir_filters RENAME domain TO domain_this')
def uninstall_hook(cr, registry):
"""move domain_this back to domain"""
cr.execute('ALTER TABLE ir_filters RENAME domain_this TO domain')

View File

@ -0,0 +1,24 @@
# Copyright 2014 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Advanced filters",
"version": "12.0.1.0.0",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"complexity": "normal",
"summary": "Set operations on filter results",
"category": "Tools",
"depends": [
'web',
],
"qweb": [
"static/src/xml/web_advanced_filter.xml",
],
"data": [
"wizards/ir_filters_combine_with_existing.xml",
"views/ir_filters.xml",
"views/templates.xml",
],
"pre_init_hook": "pre_init_hook",
"uninstall_hook": "uninstall_hook",
}

View File

@ -0,0 +1,231 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * web_advanced_filters
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: web (7.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-10 11:28+0000\n"
"PO-Revision-Date: 2015-10-07 17:50+0000\n"
"Last-Translator: <>\n"
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/oca/OCA-web-7-0/language/nl_NL/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: nl_NL\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Save filter"
msgstr ""
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Union"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,evaluate_always:0
msgid "Always evaluate this filter before using it"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Add the result of following filters"
msgstr ""
#. module: web_advanced_filters
#: help:ir.filters,evaluate_before_negate:0
msgid ""
"This is necessary if this filter contains positive operatorson x2many fields"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0 view:ir.filters.combine.with.existing:0
msgid "Save"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Freeze filter"
msgstr ""
#. module: web_advanced_filters
#: help:ir.filters,evaluate_always:0
msgid ""
"This is necessary if this filter contains x2many fields with_auto_join "
"activated"
msgstr ""
#. module: web_advanced_filters
#: code:addons/web_advanced_filters/model/ir_filters.py:218
#, python-format
msgid "Testing %s"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0 view:ir.filters.combine.with.existing:0
msgid "or"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,complement_filter_ids:0
msgid "Remove result of filters"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid ""
"{'readonly': ['|', ('union_filter_ids', '!=', [[6, False, []]]), "
"('complement_filter_ids', '!=', [[6, False, []]])]}"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters.combine.with.existing:0
msgid "Combine with existing filter"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,evaluate_before_negate:0
msgid "Evaluate this filter before negating"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid ""
"Have this filter contain extly the records it currently contains, with no "
"changes in the future. Be careful, you can't undo this operation!"
msgstr ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:32
#, python-format
msgid "Advanced filters"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,active:0
msgid "Active"
msgstr "Actief"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:86
#, python-format
msgid "Marked records"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,save_as_public:0
msgid "Share with all users"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,is_frozen:0
msgid "Frozen"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,filter_id:0
msgid "Filter"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Test filter"
msgstr ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:58
#, python-format
msgid "Whole selection (criteria)"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,domain_this:0
msgid "This filter's own domain"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Remove the result of following filters"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,context:0
msgid "Context"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,action:0
msgid "Action"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,model:0
msgid "Model"
msgstr ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:62
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:90
#, python-format
msgid "To new filter"
msgstr ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:78
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:106
#, python-format
msgid "Remove from existing filter"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,domain:0
msgid "Domain"
msgstr ""
#. module: web_advanced_filters
#: field:ir.filters,union_filter_ids:0
msgid "Add result of filters"
msgstr ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:70
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:98
#, python-format
msgid "To existing filter"
msgstr ""
#. module: web_advanced_filters
#: model:ir.model,name:web_advanced_filters.model_ir_filters_combine_with_existing
msgid "Combine a selection with an existing filter"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Are you sure? You can't undo this operation!"
msgstr ""
#. module: web_advanced_filters
#: model:ir.model,name:web_advanced_filters.model_ir_filters
msgid "Filters"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters:0 view:ir.filters.combine.with.existing:0
msgid "Cancel"
msgstr "Verwijderen"
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Complement"
msgstr ""

View File

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

View File

@ -0,0 +1,215 @@
# Copyright 2014-2020 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging
from odoo import _, api, fields, models
from odoo.tools.safe_eval import safe_eval
from odoo.osv import expression
_logger = logging.getLogger(__name__)
class IrFilters(models.Model):
_inherit = 'ir.filters'
_evaluate_before_negate = ['one2many', 'many2many']
is_frozen = fields.Boolean('Frozen', compute='_compute_is_frozen')
union_filter_ids = fields.Many2many(
'ir.filters', 'ir_filters_union_rel', 'left_filter_id',
'right_filter_id', 'Add result of filters',
domain=['|', ('active', '=', False), ('active', '=', True)],
)
complement_filter_ids = fields.Many2many(
'ir.filters', 'ir_filters_complement_rel', 'left_filter_id',
'right_filter_id', 'Remove result of filters',
domain=['|', ('active', '=', False), ('active', '=', True)],
)
domain = fields.Text(compute='_compute_domain', inverse='_inverse_domain')
domain_this = fields.Text('This filter\'s own domain', oldname='domain')
evaluate_before_negate = fields.Boolean(
'Evaluate this filter before negating',
compute='_compute_flags',
help='This is necessary if this filter contains positive operators'
'on x2many fields',
)
evaluate_always = fields.Boolean(
'Always evaluate this filter before using it',
compute='_compute_flags',
help='This is necessary if this filter contains x2many fields with'
'_auto_join activated',
)
save_as_public = fields.Boolean(
'Share with all users', default=False,
compute=lambda self: None, inverse=lambda self: None,
)
@api.multi
@api.depends('domain')
def _compute_is_frozen(self):
'''determine if this is fixed list of ids'''
for this in self:
try:
domain = safe_eval(this.domain)
except (SyntaxError, TypeError, ValueError, NameError):
domain = [expression.FALSE_LEAF]
this.is_frozen = (
len(domain) == 1 and
expression.is_leaf(domain[0]) and
domain[0][0] == 'id'
)
@api.multi
@api.depends(
'domain_this', 'union_filter_ids.domain',
'complement_filter_ids.domain',
)
def _compute_domain(self):
'''combine our domain with all domains to union/complement,
this works recursively'''
def eval_n(domain):
'''parse a domain and normalize it'''
try:
domain = safe_eval(domain)
except (SyntaxError, TypeError, ValueError, NameError):
domain = [expression.FALSE_LEAF]
return expression.normalize_domain(
domain or [expression.FALSE_LEAF])
for this in self:
if this.model_id not in self.env:
this.domain = '[]'
_logger.error('Unknown model %s used in filter', this.model_id)
continue
domain = eval_n(this.domain_this)
for u in this.union_filter_ids:
if u.evaluate_always:
matching_ids = self.env[u.model_id].search(
eval_n(u.domain),
).ids
domain = expression.OR([
domain,
[('id', 'in', matching_ids)],
])
else:
domain = expression.OR([domain, eval_n(u.domain)])
for c in this.complement_filter_ids:
if c.evaluate_before_negate:
matching_ids = self.env[c.model_id].search(
eval_n(c.domain),
).ids
domain = expression.AND([
domain,
[('id', 'not in', matching_ids)],
])
else:
domain = expression.AND([
domain,
['!'] + eval_n(c['domain']),
])
this.domain = str(expression.normalize_domain(domain))
@api.multi
def _inverse_domain(self):
for this in self:
this.domain_this = this.domain
@api.multi
@api.depends('domain')
def _compute_flags(self):
"""check if this filter contains references to x2many fields. If so,
then negation goes wrong in nearly all cases, so we evaluate the
filter and remove its resulting ids"""
for this in self:
this.evaluate_before_negate = False
this.evaluate_always = False
domain = expression.normalize_domain(
safe_eval(this.domain or 'False') or [expression.FALSE_LEAF]
)
for arg in domain:
current_model = self.env.get(this.model_id)
if current_model is None:
_logger.error(
'Unknown model %s used in filter', this.model_id)
continue
if not expression.is_leaf(arg) or not isinstance(arg[0], str):
continue
has_x2many = False
has_auto_join = False
for field_name in arg[0].split('.'):
if field_name in models.MAGIC_COLUMNS:
continue
field = current_model._fields[field_name]
has_x2many |= field.type in self._evaluate_before_negate
has_x2many |= bool(field.compute)
has_auto_join |= getattr(field, 'auto_join', False)
has_auto_join |= bool(field.compute)
if field.comodel_name:
current_model = self.env.get(field.comodel_name)
if current_model is None or has_x2many and has_auto_join:
break
this.evaluate_before_negate |= has_x2many
this.evaluate_always |= has_auto_join
if this.evaluate_before_negate and this.evaluate_always:
break
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
values.setdefault(
'user_id',
False if values.get('save_as_public') else self.env.user.id,
)
return super(IrFilters, self).create(vals_list)
@api.multi
def _evaluate(self):
self.ensure_one()
return self.env[self.model_id].with_context(
safe_eval(self.context)
).search(safe_eval(self.domain))
@api.multi
def button_save(self):
return {'type': 'ir.actions.act_window_close'}
@api.multi
def button_freeze(self):
'''evaluate the filter and write a fixed [('id', 'in', [])] domain'''
for this in self:
ids = this._evaluate().ids
removed_filters = \
this.union_filter_ids + this.complement_filter_ids
this.write({
'domain': str([('id', 'in', ids)]),
'union_filter_ids': [(6, 0, [])],
'complement_filter_ids': [(6, 0, [])],
})
# if we removed inactive filters which are orphaned now, delete
# them
if removed_filters:
self.env.cr.execute(
'''delete from ir_filters
where
not active and id in %s
and not exists (select right_filter_id
from ir_filters_union_rel where left_filter_id=id)
and not exists (select right_filter_id
from ir_filters_complement_rel where
left_filter_id=id)''',
(tuple(removed_filters.ids),)
)
@api.multi
def button_test(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Testing %s') % self.name,
'res_model': self.model_id,
'domain': self.domain,
'view_type': 'form',
'view_mode': 'tree,form',
'context': {
'default_filter_id': self.id,
},
}

View File

@ -0,0 +1 @@
* Holger Brunn <hbrunn@therp.nl>

View File

@ -0,0 +1 @@
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.

View File

@ -0,0 +1,8 @@
This addon allows users to apply set operations on filters: Remove or add
certain ids from/to a selection, but also to remove or add another filter's
outcome from/to a filter. This can be stacked, so the filter domain can be
arbitrarily complicated.
The math is hidden from the user as far as possible, in the hope it's still
user friendly.

View File

@ -0,0 +1,2 @@
* client side tests
* consider renaming all instances of `Favorites` in core to `Filters` for consistency

View File

@ -0,0 +1,5 @@
After this addon is installed, every search view shows a new menu `Advanced
filters`. Here the set operations can be applied as necessary.
Note the menu is only visible when there's search criteria or some record
selected.

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,447 @@
<?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.14: http://docutils.sourceforge.net/" />
<title>Advanced filters</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="advanced-filters">
<h1 class="title">Advanced filters</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/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_advanced_filter"><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_advanced_filter"><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>This addon allows users to apply set operations on filters: Remove or add
certain ids from/to a selection, but also to remove or add another filters
outcome from/to a filter. This can be stacked, so the filter domain can be
arbitrarily complicated.</p>
<p>The math is hidden from the user as far as possible, in the hope its still
user friendly.</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="#known-issues-roadmap" id="id2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="id7">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>After this addon is installed, every search view shows a new menu <cite>Advanced
filters</cite>. Here the set operations can be applied as necessary.</p>
<p>Note the menu is only visible when theres search criteria or some record
selected.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id2">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>client side tests</li>
<li>consider renaming all instances of <cite>Favorites</cite> in core to <cite>Filters</cite> for consistency</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">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_advanced_filter%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="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Therp BV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Holger Brunn &lt;<a class="reference external" href="mailto:hbrunn&#64;therp.nl">hbrunn&#64;therp.nl</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#id7">Other credits</a></h2>
<ul class="simple">
<li>Odoo Community Association: <a class="reference external" href="https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg">Icon</a>.</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">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_advanced_filter">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,7 @@
.o_advanced_filter_menu .o_menu_item[data-id$='-header'] a {
font-weight: bold;
cursor: default;
}
.o_advanced_filter_menu .o_menu_item[data-id$='-header'] a:hover {
background-color: inherit;
}

View File

@ -0,0 +1,256 @@
// Copyright 2014-2020 Therp BV <https://therp.nl>
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
odoo.define('web_advanced_filter', function(require) {
var core = require('web.core'),
_t = core._t,
QWeb = core.qweb,
data_manager = require('web.data_manager'),
DropdownMenu = require('web.DropdownMenu'),
FavoriteMenu = require('web.FavoriteMenu'),
pyUtils = require('web.py_utils'),
SearchView = require('web.SearchView'),
AdvancedFiltersMenu = DropdownMenu.extend({
init: function (parent) {
this._super(
parent, this._advanced_filter_header(),
this._advanced_filter_items()
);
this.search_view = parent;
this.action_manager = this.search_view.getParent();
this.action_manager
.on('switch_view', this, this._advanced_filter_update);
this.action_manager
.on('selection_changed', this, this._advanced_filter_update);
this.action_manager.on('search', this, this._advanced_filter_update);
},
start: function () {
this._super.apply(this, arguments);
this.$menu.addClass('o_advanced_filter_menu');
this._advanced_filter_update();
},
_onItemClick: function (event) {
event.preventDefault();
event.stopPropagation();
var itemId = $(event.currentTarget).data('id'),
item = _.findWhere(this.items, {itemId: itemId});
if (item && item.callback) {
item.callback.apply(this, [item]);
}
},
_advanced_filter_header: function () {
return {
category: 'advanced_filters',
title: _t('Advanced filters'),
icon: 'fa fa-cubes',
symbol:
this.isMobile ? 'fa fa-chevron-right float-right mt4' : false,
};
},
_advanced_filter_items: function () {
return [
{
itemId: 'domain-header',
description: _t('Whole selection (criteria)'),
},
{
itemId: 'domain-new',
description: _t('To new filter'),
callback: this._advanced_filters_save_criteria,
},
{
itemId: 'domain-union',
description: _t('To existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'union', 'domain'
),
},
{
itemId: 'domain-complement',
description: _t('Remove from existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'complement', 'domain'
),
},
{
itemId: 'ids-header',
description: _t('Marked records'),
},
{
itemId: 'ids-new',
description: _t('To new filter'),
callback: this._advanced_filters_save_selection,
},
{
itemId: 'ids-union',
description: _t('To existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'union', 'ids'
),
},
{
itemId: 'ids-complement',
description: _t('Remove from existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'complement', 'ids'
),
},
]
},
_advanced_filters_save_criteria: function(item)
{
var search_data = this.search_view.build_search_data(),
search = pyUtils.eval_domains_and_contexts({
domains: search_data.domains,
contexts: search_data.contexts,
group_by_seq: search_data.groupbys || []
}),
ctx = search.context;
return this.do_action({
name: item.description,
type: 'ir.actions.act_window',
res_model: 'ir.filters',
views: [[false, 'form']],
target: 'new',
context: {
default_model_id: this.search_view.dataset._model.name,
default_domain: JSON.stringify(search.domain),
default_context: JSON.stringify(ctx),
default_user_id: false,
form_view_ref: 'web_advanced_filter.form_ir_filters_save_new',
},
},
{
on_close: this.search_view._reloadFilters.bind(this.search_view),
});
},
_advanced_filters_save_selection: function(item)
{
var controller = this.action_manager.getCurrentController(),
widget = controller.widget,
ids = widget.getSelectedIds && widget.getSelectedIds() || [];
this.do_action({
name: item.description,
type: 'ir.actions.act_window',
res_model: 'ir.filters',
views: [[false, 'form']],
target: 'new',
context: {
default_model_id: this.search_view.dataset._model.name,
default_domain: JSON.stringify([['id', 'in', ids]]),
default_context: JSON.stringify({}),
default_user_id: false,
form_view_ref: 'web_advanced_filter.form_ir_filters_save_new',
},
},
{
on_close: this.search_view._reloadFilters.bind(this.search_view),
});
},
_advanced_filters_combine_with_existing: function(action, type, item)
{
var search_data = this.search_view.build_search_data(),
search = pyUtils.eval_domains_and_contexts({
domains: search_data.domains,
contexts: search_data.contexts,
group_by_seq: search_data.groupbys || []
}),
domain = [], ctx = {};
switch(type)
{
case 'domain':
domain = search.domain;
ctx = search.context;
break;
case 'ids':
var controller = this.action_manager.getCurrentController(),
widget = controller.widget;
domain = [[
'id', 'in',
widget.getSelectedIds && widget.getSelectedIds() || [],
]]
ctx = {};
break;
}
this.do_action({
name: item.description,
type: 'ir.actions.act_window',
res_model: 'ir.filters.combine.with.existing',
views: [[false, 'form']],
target: 'new',
context: {
default_model: this.search_view.dataset._model.name,
default_domain: JSON.stringify(domain),
default_action: action,
default_context: JSON.stringify(ctx),
default_filter_id:
this.search_view.dataset.context.default_filter_id || false,
},
},
{
on_close: this.search_view._reloadFilters.bind(this.search_view),
});
},
_advanced_filter_update: function(event) {
var show_ids = false,
show_domain = this.search_view.query.length > 0;
switch ((event || {}).name) {
case 'selection_changed':
show_ids = event.data.selection.length > 0;
break;
case 'search':
show_domain = event.data.domains.length > 0;
break;
}
this.$menu.children('.o_menu_item[data-id^="ids-"]')
.toggle(show_ids);
this.$menu.children('.o_menu_item[data-id^="domain-"]')
.toggle(show_domain);
this.$dropdownReference.toggle(show_ids || show_domain);
},
});
SearchView.include({
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.advanced_filters_menu = self._createAdvancedFiltersMenu();
return self.advanced_filters_menu.insertBefore(
self.favorite_menu.$el
);
});
},
_createAdvancedFiltersMenu: function () {
return new AdvancedFiltersMenu(this);
},
_reloadFilters: function () {
if (this.options.disable_favorites) {
return;
}
var self = this;
data_manager._invalidate(
data_manager._cache.filters,
data_manager._gen_key(this.dataset.model, this.action.id)
);
this.loadFilters(this.dataset, this.action.id)
.then(function (filters) {
self.favorite_filters = filters;
var favorite_menu = new FavoriteMenu(
self, self.query, self.dataset.model, self.action,
self.favorite_filters
);
favorite_menu.replace(self.favorite_menu.$el);
self.favorite_menu = favorite_menu;
});
},
});
return {
AdvancedFiltersMenu: AdvancedFiltersMenu,
}
});

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="AdvancedFilter">
<div role="separator" class="dropdown-divider"/>
<button type="button" class="dropdown-item o_advanced_filter o_closed_menu" aria-expanded="false">Whole selection (criteria)</button>
<div class="dropdown-item-text o_advanced_filter-domain">
<button type="button" class="dropdown-item o_advanced_filter-add-new">To new filter</button>
<button type="button" class="dropdown-item o_advanced_filter-add-existing">To existing filter</button>
<button type="button" class="dropdown-item o_advanced_filter-remove">Remove from existing filter</button>
</div>
<button type="button" class="dropdown-item o_advanced_filter o_closed_menu" aria-expanded="false">Marked records</button>
<div class="dropdown-item-text o_advanced_filter-ids">
<button type="button" class="dropdown-item o_advanced_filter-add-new">To new filter</button>
<button type="button" class="dropdown-item o_advanced_filter-add-existing">To existing filter</button>
<button type="button" class="dropdown-item o_advanced_filter-remove">Remove from existing filter</button>
</div>
</t>
</templates>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="ir_filters_view_form" model="ir.ui.view">
<field name="model">ir.filters</field>
<field name="inherit_id" ref="base.ir_filters_view_form" />
<field name="arch" type="xml">
<sheet position="before">
<header>
<button type="object" string="Test filter" name="button_test" class="btn-primary" />
<button type="object" string="Freeze filter" name="button_freeze" attrs="{'invisible': [('is_frozen', '=', True)]}" help="Have this filter contain extly the records it currently contains, with no changes in the future. Be careful, you can&apos;t undo this operation!" confirm="Are you sure? You can&apos;t undo this operation!" />
</header>
</sheet>
<group position="after">
<field name="is_frozen" invisible="True" />
<field name="id" invisible="True" />
<group string="Add the result of following filters">
<field name="union_filter_ids" nolabel="1" domain="[('user_id', 'in', [False, uid]), ('id', '!=', id), ('model_id', '=', model_id)]" />
</group>
<group string="Remove the result of following filters">
<field name="complement_filter_ids" nolabel="1" domain="[('user_id', 'in', [False, uid]),('id', '!=', id), ('model_id', '=', model_id)]" />
</group>
</group>
<field name="domain" position="attributes">
<attribute name="attrs">{'readonly': ['|', ('union_filter_ids', '!=', [[6, False, []]]), ('complement_filter_ids', '!=', [[6, False, []]])]}</attribute>
</field>
<field name="domain" position="after">
<field name="domain_this" attrs="{'invisible': [('union_filter_ids', '=', [[6, False, []]]), ('complement_filter_ids', '=', [[6, False, []]])]}" />
</field>
</field>
</record>
<record id="form_ir_filters_save_new" model="ir.ui.view">
<field name="model">ir.filters</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<form string="Save filter" version="7.0">
<group>
<field name="name" />
<field name="is_default"/>
<field name="save_as_public" />
</group>
<footer>
<button class="oe_highlight" type="object" name="button_save" string="Save" />
or
<button class="oe_link" special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<template id="assets_backend" name="web_advanced_filter assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_advanced_filter/static/src/js/web_advanced_filter.js"></script>
<link rel="stylesheet" href="/web_advanced_filter/static/src/css/web_advanced_filter.css"/>
</xpath>
</template>
</data>
</odoo>

View File

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

View File

@ -0,0 +1,68 @@
# Copyright 2014 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import time
import json
from odoo import api, fields, models
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval
class IrFiltersCombineWithExisting(models.TransientModel):
_name = 'ir.filters.combine.with.existing'
_description = 'Combine a selection with an existing filter'
action = fields.Selection(
[('union', 'Union'), ('complement', 'Complement')],
'Action', required=True,
)
domain = fields.Char('Domain', required=True)
context = fields.Char('Context', required=True, default='{}')
model = fields.Char('Model', required=True)
filter_id = fields.Many2one('ir.filters', 'Filter', required=True)
@api.multi
def button_save(self):
self.ensure_one()
this = self
domain = json.loads(this.domain)
is_frozen = (len(domain) == 1 and
expression.is_leaf(domain[0]) and
domain[0][0] == 'id')
if this.action == 'union':
if is_frozen and this.filter_id.is_frozen:
domain[0][2] = list(set(domain[0][2]).union(
set(safe_eval(this.filter_id.domain)[0][2])))
this.filter_id.write({'domain': str(domain)})
else:
this.filter_id.write({
'union_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
this.filter_id.name, 'add', time.time()),
'active': False,
'domain': str(domain),
'context': this.context,
'model_id': this.model,
'user_id': this.filter_id.user_id.id or False,
})],
})
elif this.action == 'complement':
if is_frozen and this.filter_id.is_frozen:
complement_set = set(safe_eval(this.filter_id.domain)[0][2])
domain[0][2] = list(
complement_set.difference(set(domain[0][2])))
this.filter_id.write({'domain': str(domain)})
else:
this.filter_id.write({
'complement_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
this.filter_id.name, 'remove', time.time()),
'active': False,
'domain': str(domain),
'context': this.context,
'model_id': this.model,
'user_id': this.filter_id.user_id.id or False,
})],
})
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="form_ir_filters_combine_with_existing" model="ir.ui.view">
<field name="model">ir.filters.combine.with.existing</field>
<field name="arch" type="xml">
<form string="Combine with existing filter" version="7.0">
<group>
<field name="model" invisible="1" />
<field name="filter_id" domain="[('model_id', '=', model)]" context="{'default_model_id': model}" />
</group>
<footer>
<button class="btn-primary" type="object" name="button_save" string="Save" />
<button class="btn-secondary" special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import model
from . import wizard

View File

@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
"name": "Advanced filters",
"version": "1.0",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"complexity": "normal",
"description": """
Introduction
------------
This addon allows users to apply set operations on filters: Remove or add
certain ids from/to a selection, but also to remove or add another filter's
outcome from/to a filter. This can be stacked, so the filter domain can be
arbitrarily complicated.
The math is hidden from the user as far as possible, in the hope it's still
user friendly.
Usage
-----
After this addon is installed, every list view shows a new menu 'Advanced
filters'. Here the set operations can be applied as necessary.
Caution
-------
Deinstalling this module will leave you with filters with empty domains. Use
this query before uninstalling to avoid that:
``alter table ir_filters rename domain_this to domain``
""",
"category": "Tools",
"depends": [
'base',
'web',
],
"data": [
"data/migration.xml",
"wizard/ir_filters_combine_with_existing.xml",
"view/ir_filters.xml",
],
"js": [
'static/src/js/web_advanced_filters.js',
],
"css": [
'static/src/css/web_advanced_filters.css',
],
"qweb": [
],
"test": [
],
"auto_install": False,
"installable": True,
"application": False,
"external_dependencies": {
'python': [],
},
}

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data noupdate="1">
<function name="_migrate_name_change" model="ir.filters" />
</data>
</openerp>

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import ir_filters

View File

@ -1,259 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import itertools
from openerp.osv.orm import Model, MAGIC_COLUMNS
from openerp.osv import fields, expression
from openerp.tools.safe_eval import safe_eval
from openerp.tools.translate import _
class IrFilters(Model):
_inherit = 'ir.filters'
_evaluate_before_negate = ['one2many', 'many2many']
def _is_frozen_get(self, cr, uid, ids, field_name, args, context=None):
'''determine if this is fixed list of ids'''
result = {}
for this in self.browse(cr, uid, ids, context=context):
try:
domain = safe_eval(this.domain)
except:
domain = [expression.FALSE_LEAF]
result[this.id] = (len(domain) == 1 and
expression.is_leaf(domain[0]) and
domain[0][0] == 'id')
return result
def _domain_get(self, cr, uid, ids, field_name, args, context=None):
'''combine our domain with all domains to union/complement,
this works recursively'''
def eval_n(domain):
'''parse a domain and normalize it'''
try:
domain = safe_eval(domain)
except:
domain = [expression.FALSE_LEAF]
return expression.normalize_domain(
domain or [expression.FALSE_LEAF])
result = {}
for this in self.read(
cr, uid, ids,
['domain_this', 'union_filter_ids', 'complement_filter_ids'],
context=context):
domain = eval_n(this['domain_this'])
for u in self.read(cr, uid, this['union_filter_ids'],
['domain', 'evaluate_always', 'model_id'],
context=context):
if u['evaluate_always']:
matching_ids = self.pool[u['model_id']].search(
cr, uid, eval_n(u['domain']),
context=context)
domain = expression.OR([
domain,
[('id', 'in', matching_ids)],
])
else:
domain = expression.OR([domain, eval_n(u['domain'])])
for c in self.read(cr, uid, this['complement_filter_ids'],
['domain', 'evaluate_before_negate',
'model_id'],
context=context):
if c['evaluate_before_negate']:
matching_ids = self.pool[c['model_id']].search(
cr, uid, eval_n(c['domain']),
context=context)
domain = expression.AND([
domain,
[('id', 'not in', matching_ids)],
])
else:
domain = expression.AND([
domain,
['!'] + eval_n(c['domain'])])
result[this['id']] = str(expression.normalize_domain(domain))
return result
def _domain_set(self, cr, uid, ids, field_name, field_value, args,
context=None):
self.write(cr, uid, ids, {'domain_this': field_value})
def _evaluate_get(self, cr, uid, ids, field_name, args, context=None):
"""check if this filter contains references to x2many fields. If so,
then negation goes wrong in nearly all cases, so we evaluate the
filter and remove its resulting ids"""
result = {}
for this in self.read(cr, uid, ids, ['model_id', 'domain'],
context=context):
result[this['id']] = {
'evaluate_before_negate': False,
'evaluate_always': False,
}
domain = expression.normalize_domain(
safe_eval(this['domain'] or 'False') or
[expression.FALSE_LEAF])
for arg in domain:
if not expression.is_leaf(arg) or not isinstance(
arg[0], basestring):
continue
current_model = self.pool.get(this['model_id'])
if not current_model:
continue
has_x2many = False
has_auto_join = False
for field_name in arg[0].split('.'):
if field_name in MAGIC_COLUMNS:
continue
field = current_model._all_columns[field_name].column
has_x2many |= field._type in self._evaluate_before_negate
has_x2many |= isinstance(field, fields.function)
has_auto_join |= field._auto_join
has_auto_join |= isinstance(field, fields.function)
if hasattr(field, '_obj'):
current_model = self.pool.get(field._obj)
if not current_model or has_x2many and has_auto_join:
break
result[this['id']]['evaluate_before_negate'] |= has_x2many
result[this['id']]['evaluate_always'] |= has_auto_join
if result[this['id']]['evaluate_before_negate'] and\
result[this['id']]['evaluate_always']:
break
return result
_columns = {
'is_frozen': fields.function(
_is_frozen_get, type='boolean', string='Frozen'),
'union_filter_ids': fields.many2many(
'ir.filters', 'ir_filters_union_rel', 'left_filter_id',
'right_filter_id', 'Add result of filters',
domain=['|', ('active', '=', False), ('active', '=', True)]),
'complement_filter_ids': fields.many2many(
'ir.filters', 'ir_filters_complement_rel', 'left_filter_id',
'right_filter_id', 'Remove result of filters',
domain=['|', ('active', '=', False), ('active', '=', True)]),
'active': fields.boolean('Active'),
'domain': fields.function(
_domain_get, type='text', string='Domain',
fnct_inv=_domain_set),
'domain_this': fields.text(
'This filter\'s own domain', oldname='domain'),
'evaluate_before_negate': fields.function(
_evaluate_get, type='boolean', multi='evaluate',
string='Evaluate this filter before negating',
help='This is necessary if this filter contains positive operators'
'on x2many fields'),
'evaluate_always': fields.function(
_evaluate_get, type='boolean', multi='evaluate',
string='Always evaluate this filter before using it',
help='This is necessary if this filter contains x2many fields with'
'_auto_join activated'),
'save_as_public': fields.function(
lambda self, cr, uid, ids, *args, **kwargs:
dict((i, False) for i in ids),
fnct_inv=lambda *args, **kwargs: None,
type='boolean',
string='Share with all users'),
}
_defaults = {
'active': True,
'save_as_public': False,
}
def _evaluate(self, cr, uid, ids, context=None):
assert len(ids) == 1
this = self.browse(cr, uid, ids[0], context=context)
return self.pool[this.model_id].search(
cr, uid, safe_eval(this.domain), context=safe_eval(this.context))
def button_save(self, cr, uid, ids, context=None):
return {'type': 'ir.actions.act_window.close'}
def button_freeze(self, cr, uid, ids, context=None):
'''evaluate the filter and write a fixed [('id', 'in', [])] domain'''
for this in self.browse(cr, uid, ids, context=context):
ids = this._evaluate()
removed_filter_ids = [f.id for f in itertools.chain(
this.union_filter_ids, this.complement_filter_ids)]
this.write({
'domain': str([('id', 'in', ids)]),
'union_filter_ids': [(6, 0, [])],
'complement_filter_ids': [(6, 0, [])],
})
# if we removed inactive filters which are orphaned now, delete
# them
cr.execute('''delete from ir_filters
where
not active and id in %s
and not exists (select right_filter_id
from ir_filters_union_rel where left_filter_id=id)
and not exists (select right_filter_id
from ir_filters_complement_rel where
left_filter_id=id)
''',
(tuple(removed_filter_ids),))
def button_test(self, cr, uid, ids, context=None):
for this in self.browse(cr, uid, ids, context=context):
return {
'type': 'ir.actions.act_window',
'name': _('Testing %s') % this.name,
'res_model': this.model_id,
'domain': this.domain,
'view_type': 'form',
'view_mode': 'tree,form',
'context': {
'default_filter_id': this.id,
},
}
def _auto_init(self, cr, context=None):
cr.execute(
'SELECT count(attname) FROM pg_attribute '
'WHERE attrelid = '
'( SELECT oid FROM pg_class WHERE relname = %s) '
'AND attname = %s', (self._table, 'domain_this'))
if not cr.fetchone()[0]:
cr.execute(
'ALTER table %s RENAME domain TO domain_this' % self._table)
return super(IrFilters, self)._auto_init(cr, context=context)
def _migrate_name_change(self, cr, uid, context=None):
cr.execute(
"select id from ir_module_module where name='advanced_filters' "
"and author='Therp BV'")
old_name_installed = cr.fetchall()
if not old_name_installed:
return
cr.execute(
"delete from ir_model_data where module='web_advanced_filters'")
cr.execute(
"update ir_model_data set module='web_advanced_filters' "
"where module='advanced_filters'")
cr.execute(
"update ir_module_module set state='to remove' where "
"name='advanced_filters' and state not in "
"('uninstalled', 'to remove')")
def create(self, cr, uid, values, context=None):
values.setdefault(
'user_id', False if values.get('save_as_public') else uid)
return super(IrFilters, self).create(cr, uid, values, context=context)

View File

@ -1,14 +0,0 @@
li.oe_advanced_filters_header
{
font-weight: bold;
}
.openerp .oe_dropdown_menu > li.oe_advanced_filters_header:hover
{
background-color: inherit;
background-image: inherit;
box-shadow: none;
}
.openerp .oe_dropdown_menu > li.oe_advanced_filters_header a:hover
{
cursor: default !important;
}

View File

@ -1,272 +0,0 @@
//-*- coding: utf-8 -*-
//############################################################################
//
// OpenERP, Open Source Management Solution
// This module copyright (C) 2014 Therp BV (<http://therp.nl>).
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
//############################################################################
openerp.web_advanced_filters = function(instance)
{
var _t = instance.web._t;
instance.web.Sidebar.include({
init: function()
{
var result = this._super.apply(this, arguments);
this.sections.push({
'name': 'advanced_filters',
'label': _t('Advanced filters'),
});
this.items.advanced_filters = [];
return result;
},
});
instance.web.ListView.include({
do_select: function (ids, records)
{
var result = this._super(this, arguments);
this.advanced_filters_show(ids);
return result;
},
load_list: function(data)
{
var result = this._super.apply(this, arguments),
self = this;
if(!this.sidebar || this.sidebar.items.advanced_filters.length)
{
this.advanced_filters_show([]);
return result;
}
this.sidebar.add_items(
'advanced_filters',
[
{
label: _t('Whole selection (criteria)'),
classname: 'oe_advanced_filters_header',
},
{
label: _t('To new filter'),
callback: function ()
{
self.advanced_filters_save_criteria.apply(
self, arguments);
},
},
{
label: _t('To existing filter'),
callback: function (item)
{
self.advanced_filters_combine_with_existing.apply(
self, ['union', 'domain', item]);
},
},
{
label: _t('Remove from existing filter'),
callback: function (item)
{
self.advanced_filters_combine_with_existing.apply(
self, ['complement', 'domain', item]);
},
},
{
label: _t('Marked records'),
classname: 'oe_advanced_filters_header',
},
{
label: _t('To new filter'),
callback: function ()
{
self.advanced_filters_save_selection.apply(
self, arguments);
},
},
{
label: _t('To existing filter'),
callback: function (item)
{
self.advanced_filters_combine_with_existing.apply(
self, ['union', 'ids', item]);
},
},
{
label: _t('Remove from existing filter'),
callback: function (item)
{
self.advanced_filters_combine_with_existing.apply(
self, ['complement', 'ids', item]);
},
},
]
);
this.do_select([], []);
return result;
},
advanced_filters_show: function(ids)
{
var self = this;
if(this.sidebar)
{
this.sidebar.$el.show();
this.sidebar.$el.children().children().each(function(i, e)
{
$e = jQuery(e)
if($e.find('li.oe_advanced_filters_header').length)
{
var search = self.ViewManager.searchview
.build_search_data();
$e.find('a[data-index="0"],a[data-index="1"],' +
'a[data-index="2"],a[data-index="3"]')
.parent().toggle(
search.contexts.length > 0 ||
search.domains.length > 0);
$e.find('a[data-index="4"],a[data-index="5"],' +
'a[data-index="6"],a[data-index="7"]')
.parent().toggle(ids.length > 0);
$e.toggle(
search.contexts.length > 0 ||
search.domains.length > 0 ||
ids.length > 0);
}
else
{
$e.toggle(ids.length > 0);
}
});
}
},
advanced_filters_save_criteria: function(item)
{
var search = this.ViewManager.searchview.build_search_data(),
self = this;
instance.web.pyeval.eval_domains_and_contexts({
domains: search.domains,
contexts: search.contexts,
group_by_seq: search.groupbys || []
}).done(function(search)
{
var ctx = search.context;
_(_.keys(instance.session.user_context)).each(
function (key) {delete ctx[key]});
self.do_action({
name: item.label,
type: 'ir.actions.act_window',
res_model: 'ir.filters',
views: [[false, 'form']],
target: 'new',
context: {
default_model_id: self.dataset._model.name,
default_domain: JSON.stringify(search.domain),
default_context: JSON.stringify(ctx),
default_user_id: JSON.stringify(false),
form_view_ref: 'web_advanced_filters.form_ir_filters_save_new',
},
},
{
on_close: function()
{
self.ViewManager.setup_search_view(
self.ViewManager.searchview.view_id,
self.ViewManager.searchview.defaults);
},
});
});
},
advanced_filters_save_selection: function(item)
{
var self = this;
this.do_action({
name: item.label,
type: 'ir.actions.act_window',
res_model: 'ir.filters',
views: [[false, 'form']],
target: 'new',
context: {
default_model_id: this.dataset._model.name,
default_domain: JSON.stringify(
[
['id', 'in', this.groups.get_selection().ids],
]
),
default_context: JSON.stringify({}),
default_user_id: JSON.stringify(false),
form_view_ref: 'web_advanced_filters.form_ir_filters_save_new',
},
},
{
on_close: function()
{
self.ViewManager.setup_search_view(
self.ViewManager.searchview.view_id,
self.ViewManager.searchview.defaults);
},
});
},
advanced_filters_combine_with_existing: function(action, type, item)
{
var search = this.ViewManager.searchview.build_search_data(),
self = this;
instance.web.pyeval.eval_domains_and_contexts({
domains: search.domains,
contexts: search.contexts,
group_by_seq: search.groupbys || []
}).done(function(search)
{
var domain = [], ctx = {};
switch(type)
{
case 'domain':
domain = search.domain;
ctx = search.context;
_(_.keys(instance.session.user_context)).each(
function (key) {delete ctx[key]});
break;
case 'ids':
domain = [
['id', 'in', self.groups.get_selection().ids],
]
ctx = {};
break;
}
self.do_action({
name: item.label,
type: 'ir.actions.act_window',
res_model: 'ir.filters.combine.with.existing',
views: [[false, 'form']],
target: 'new',
context: _.extend({
default_model: self.dataset._model.name,
default_domain: JSON.stringify(domain),
default_action: action,
default_context: JSON.stringify(ctx),
},
self.dataset.context.default_filter_id ? {
default_filter_id:
self.dataset.context.default_filter_id,
} : {}),
},
{
on_close: function()
{
self.ViewManager.setup_search_view(
self.ViewManager.searchview.view_id,
self.ViewManager.searchview.defaults);
},
});
});
},
});
}

View File

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="ir_filters_view_form" model="ir.ui.view">
<field name="model">ir.filters</field>
<field name="inherit_id" ref="base.ir_filters_view_form" />
<field name="arch" type="xml">
<sheet position="before">
<header>
<button type="object" string="Test filter" name="button_test" class="oe_highlight" />
<button type="object" string="Freeze filter" name="button_freeze" attrs="{'invisible': [('is_frozen', '=', True)]}" help="Have this filter contain extly the records it currently contains, with no changes in the future. Be careful, you can&apos;t undo this operation!" confirm="Are you sure? You can&apos;t undo this operation!" />
</header>
</sheet>
<group position="after">
<field name="is_frozen" invisible="True" />
<field name="id" invisible="True" />
<group string="Add the result of following filters">
<field name="union_filter_ids" nolabel="1" domain="[('user_id', 'in', [False, uid]), ('id', '!=', id), ('model_id', '=', model_id)]" />
</group>
<group string="Remove the result of following filters">
<field name="complement_filter_ids" nolabel="1" domain="[('user_id', 'in', [False, uid]),('id', '!=', id), ('model_id', '=', model_id)]" />
</group>
</group>
<field name="domain" position="attributes">
<attribute name="attrs">{'readonly': ['|', ('union_filter_ids', '!=', [[6, False, []]]), ('complement_filter_ids', '!=', [[6, False, []]])]}</attribute>
</field>
<field name="domain" position="after">
<field name="domain_this" attrs="{'invisible': [('union_filter_ids', '=', [[6, False, []]]), ('complement_filter_ids', '=', [[6, False, []]])]}" />
</field>
</field>
</record>
<record id="form_ir_filters_save_new" model="ir.ui.view">
<field name="model">ir.filters</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<form string="Save filter" version="7.0">
<group>
<field name="name" />
<field name="is_default"/>
<field name="save_as_public" />
</group>
<footer>
<button class="oe_highlight" type="object" name="button_save" string="Save" />
or
<button class="oe_link" special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</data>
</openerp>

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import ir_filters_combine_with_existing

View File

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
import json
from openerp.osv.orm import TransientModel
from openerp.osv import fields, expression
from openerp.tools.safe_eval import safe_eval
class IrFiltersCombineWithExisting(TransientModel):
_name = 'ir.filters.combine.with.existing'
_description = 'Combine a selection with an existing filter'
_columns = {
'action': fields.selection(
[('union', 'Union'), ('complement', 'Complement')],
'Action', required=True),
'domain': fields.char('Domain', required=True),
'context': fields.char('Context', required=True),
'model': fields.char('Model', required=True),
'filter_id': fields.many2one('ir.filters', 'Filter', required=True),
}
def button_save(self, cr, uid, ids, context=None):
assert len(ids) == 1
this = self.browse(cr, uid, ids[0], context=context)
domain = json.loads(this.domain)
is_frozen = (len(domain) == 1 and
expression.is_leaf(domain[0]) and
domain[0][0] == 'id')
if this.action == 'union':
if is_frozen and this.filter_id.is_frozen:
domain[0][2] = list(set(domain[0][2]).union(
set(safe_eval(this.filter_id.domain)[0][2])))
this.filter_id.write({'domain': str(domain)})
else:
this.filter_id.write(
{
'union_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
this.filter_id.name, 'add', time.time()),
'active': False,
'domain': str(domain),
'context': this.context,
'model_id': this.model,
'user_id': this.filter_id.user_id.id or False,
})],
})
elif this.action == 'complement':
if is_frozen and this.filter_id.is_frozen:
complement_set = set(safe_eval(this.filter_id.domain)[0][2])
domain[0][2] = list(
complement_set.difference(set(domain[0][2])))
this.filter_id.write({'domain': str(domain)})
else:
this.filter_id.write(
{
'complement_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
this.filter_id.name, 'remove', time.time()),
'active': False,
'domain': str(domain),
'context': this.context,
'model_id': this.model,
'user_id': this.filter_id.user_id.id or False,
})],
})
return {'type': 'ir.actions.act_window.close'}

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="form_ir_filters_combine_with_existing" model="ir.ui.view">
<field name="model">ir.filters.combine.with.existing</field>
<field name="arch" type="xml">
<form string="Combine with existing filter" version="7.0">
<group>
<field name="model" invisible="1" />
<field name="filter_id" domain="[('model_id', '=', model)]" />
</group>
<footer>
<button class="oe_highlight" type="object" name="button_save" string="Save" />
or
<button class="oe_link" special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</data>
</openerp>