Merge PR #1635 into 12.0

Signed-off-by hbrunn
pull/2010/head
OCA-git-bot 2021-08-04 17:14:38 +00:00
commit eadef8e7b8
25 changed files with 2142 additions and 0 deletions

View File

@ -0,0 +1,101 @@
================
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>
* Jan Verbeek <jverbeek@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,195 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * web_advanced_filters
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-29 10:55+0000\n"
"PO-Revision-Date: 2014-07-29 10:55+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \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
#: view:ir.filters:0
msgid "Add the result of following filters"
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
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:94
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:114
#, python-format
msgid "Remove from existing selection"
msgstr ""
#. module: web_advanced_filters
#: code:addons/web_advanced_filters/model/ir_filters.py:131
#, 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
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:86
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:106
#, python-format
msgid "To existing selection"
msgstr ""
#. module: web_advanced_filters
#: view:ir.filters.combine.with.existing:0
msgid "Combine with existing filter"
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 ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:74
#, python-format
msgid "Marked records"
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:78
#, python-format
msgid "To new selection"
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
#: 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
#: 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 ""
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:102
#, python-format
msgid "Whole selection"
msgstr ""
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Complement"
msgstr ""

View File

@ -0,0 +1,203 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * web_advanced_filters
#
# Rudolf Schnapka <schnapkar@golive-saar.de>, 2015.
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-29 10:55+0000\n"
"PO-Revision-Date: 2015-01-04 14:05+0100\n"
"Last-Translator: Rudolf Schnapka <schnapkar@golive-saar.de>\n"
"Language-Team: German <kde-i18n-de@kde.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
"Language: de\n"
"X-Generator: Lokalize 1.5\n"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Save filter"
msgstr "Filter sichern"
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Union"
msgstr "Verbund"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Add the result of following filters"
msgstr "Die Ergebnisse der folgenden Filter hinzufügen"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "Save"
msgstr "Speichern"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Freeze filter"
msgstr "Filter einfrieren"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:94
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:114
#, python-format
msgid "Remove from existing selection"
msgstr "Aus aktueller Auswahl entfernen"
#. module: web_advanced_filters
#: code:addons/web_advanced_filters/model/ir_filters.py:131
#, python-format
msgid "Testing %s"
msgstr "Teste %s"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "or"
msgstr "oder"
#. module: web_advanced_filters
#: field:ir.filters,complement_filter_ids:0
msgid "Remove result of filters"
msgstr "Ergebnis der Filter entfernen"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:86
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:106
#, python-format
msgid "To existing selection"
msgstr "Zu bestehender Auswahl"
#. module: web_advanced_filters
#: view:ir.filters.combine.with.existing:0
msgid "Combine with existing filter"
msgstr "Mit bestehendem Filter kombinieren"
#. 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 ""
"Diesem Filter genau die aktuellen Datensätze, künftig unveränderlich, "
"zuordnen. Vorsicht, dieser Vorgang kann nicht rückgängig gemacht werden!"
#. 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 "Fortgeschrittene Filter"
#. module: web_advanced_filters
#: field:ir.filters,active:0
msgid "Active"
msgstr "Aktiv"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:74
#, python-format
msgid "Marked records"
msgstr "Markierte Datensätze"
#. module: web_advanced_filters
#: field:ir.filters,is_frozen:0
msgid "Frozen"
msgstr "Eingefroren"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,filter_id:0
msgid "Filter"
msgstr "Filter"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Test filter"
msgstr "Filtertest"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:78
#, python-format
msgid "To new selection"
msgstr "Zu neuer Auswahl"
#. module: web_advanced_filters
#: field:ir.filters,domain_this:0
msgid "This filter's own domain"
msgstr "Dieses Filters eigene Domäne (Abgrenzung)"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Remove the result of following filters"
msgstr "Das Ergebnis des folgenden Filters entfernen"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,context:0
msgid "Context"
msgstr "Kontext"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,action:0
msgid "Action"
msgstr "Aktion"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,model:0
msgid "Model"
msgstr "Modell"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,domain:0
msgid "Domain"
msgstr "Domäne"
#. module: web_advanced_filters
#: field:ir.filters,union_filter_ids:0
msgid "Add result of filters"
msgstr "Ergebnis von Filtern hinzufügen"
#. 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 "Eine Auswahl mit bestehendem Filter kombinieren"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Are you sure? You can't undo this operation!"
msgstr "Sind Sie sicher? Sie können diesen Vorgang nicht mehr rückgängig machen!"
#. module: web_advanced_filters
#: model:ir.model,name:web_advanced_filters.model_ir_filters
msgid "Filters"
msgstr "Filter"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "Cancel"
msgstr "Abbrechen"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:102
#, python-format
msgid "Whole selection"
msgstr "Gesamte Auswahl"
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Complement"
msgstr "Ergänzung"

View File

@ -0,0 +1,199 @@
# This file contains the translation of the following modules:
# * web_advanced_filters
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-29 09:54+0000\n"
"PO-Revision-Date: 2014-07-29 09:54+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Save filter"
msgstr "Opslaan filter"
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Union"
msgstr "Union"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Add the result of following filters"
msgstr "Voeg resultaat van onderstaande filteringen toe aan filter"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "Save"
msgstr "Opslaan"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Freeze filter"
msgstr "Bevries filter"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:94
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:114
#, python-format
msgid "Remove from existing filter"
msgstr "Verwijder uit bestaand filter"
#. module: web_advanced_filters
#: code:addons/web_advanced_filters/model/ir_filters.py:131
#, python-format
msgid "Testing %s"
msgstr "Testen %s"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "or"
msgstr "or"
#. module: web_advanced_filters
#: field:ir.filters,save_as_public:0
msgid "Share with all users"
msgstr "Deel met alle gebruikers"
#. module: web_advanced_filters
#: field:ir.filters,complement_filter_ids:0
msgid "Remove result of filters"
msgstr "Verwijder resultaat van filters"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:86
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:106
#, python-format
msgid "To existing filter"
msgstr "Voeg toe aan bestaand filter"
#. module: web_advanced_filters
#: view:ir.filters.combine.with.existing:0
msgid "Combine with existing filter"
msgstr "Combineer met bestaande filter"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Have this filter contain exactly the records it currently contains, with no changes in the future. Be careful, you can't undo this operation!"
msgstr "Laat deze filter precies deze records bevatten, zonder veranderingen in de toekomst. Wees voorzichtig, je kan dit niet meer veranderen!"
#. 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 "Geavanceerde filters"
#. 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:74
#, python-format
msgid "Marked records"
msgstr "Aangevinkte records (verzameling)"
#. module: web_advanced_filters
#: field:ir.filters,is_frozen:0
msgid "Frozen"
msgstr "Bevroren"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,filter_id:0
msgid "Filter"
msgstr "Filter"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Test filter"
msgstr "Test filter"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:78
#, python-format
msgid "To new filter"
msgstr "Opslaan als nieuw filter"
#. module: web_advanced_filters
#: field:ir.filters,domain_this:0
msgid "This filter's own domain"
msgstr "Deze filters eigen domein"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Remove the result of following filters"
msgstr "Verwijder resultaat van onderstaande filteringen uit filter"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,context:0
msgid "Context"
msgstr "Context"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,action:0
msgid "Action"
msgstr "Actie"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,model:0
msgid "Model"
msgstr "Model"
#. module: web_advanced_filters
#. openerp-web
#: code:addons/web_advanced_filters/static/src/js/web_advanced_filters.js:102
#, python-format
msgid "Whole selection (criteria)"
msgstr "Alle records (als criteria)"
#. module: web_advanced_filters
#: field:ir.filters.combine.with.existing,domain:0
msgid "Domain"
msgstr "Domein"
#. module: web_advanced_filters
#: field:ir.filters,union_filter_ids:0
msgid "Add result of filters"
msgstr "Voeg resultaat toe aan filters"
#. 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 "Combineer een selectie met een bestaande filter"
#. module: web_advanced_filters
#: view:ir.filters:0
msgid "Are you sure? You can't undo this operation!"
msgstr "Weet u het zeker? U kan deze operatie niet meer ongedaan maken"
#. module: web_advanced_filters
#: model:ir.model,name:web_advanced_filters.model_ir_filters
msgid "Filters"
msgstr "Filters"
#. module: web_advanced_filters
#: view:ir.filters:0
#: view:ir.filters.combine.with.existing:0
msgid "Cancel"
msgstr "Annuleren"
#. module: web_advanced_filters
#: selection:ir.filters.combine.with.existing,action:0
msgid "Complement"
msgstr "Aanvullen"

View File

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

View File

@ -0,0 +1,301 @@
# 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',
context={"active_test": False},
)
complement_filter_ids = fields.Many2many(
'ir.filters', 'ir_filters_complement_rel', 'left_filter_id',
'right_filter_id', 'Remove result of filters',
context={"active_test": False},
)
domain = fields.Text(compute='_compute_domain', inverse='_inverse_domain')
domain_this = fields.Text("Domain without helpers", oldname='domain')
evaluate_before_negate = fields.Boolean(
'Evaluate this filter before negating',
compute='_compute_pre_evaluate',
help='This is necessary if this filter contains positive operators'
'on x2many fields',
)
evaluate_before_join = fields.Boolean(
'Evaluate this filter before joining',
compute='_compute_pre_evaluate',
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, store=False
)
@api.model
def _eval_domain(self, domain):
"""Parse a domain and normalize it, with a default if it's invalid."""
try:
domain = safe_eval(domain) or [expression.FALSE_LEAF]
except (SyntaxError, TypeError, ValueError, NameError):
domain = [expression.FALSE_LEAF]
return expression.normalize_domain(domain)
@api.multi
@api.depends('domain')
def _compute_is_frozen(self):
'''determine if this is fixed list of ids'''
for this in self:
domain = self._eval_domain(this.domain)
this.is_frozen = (
len(domain) == 1
and expression.is_leaf(domain[0])
and domain[0][0] == "id"
and domain[0][1] == "in"
)
@api.multi
@api.depends(
# Morally, this should have union_filter_ids.domain and
# complement_filter_ids.domain. But that causes the domain to be
# invalidated much too often, sometimes giving an enormous slowdown.
# This comment was added in version 12. It should be reinvestigated
# when migrating to a higher version.
'domain_this', 'union_filter_ids', 'complement_filter_ids',
)
def _compute_domain(self):
'''combine our domain with all domains to union/complement,
this works recursively'''
for this in self:
if this.model_id not in self.env:
this.domain = '[]'
_logger.error(
"Unknown model %s used in filter %d",
this.model_id,
this.id,
)
continue
domain = self._eval_domain(this.domain_this)
for u in this.union_filter_ids:
if u.model_id != this.model_id:
_logger.warning(
"Model mismatch in helper %d on filter %d!",
u.model_id,
this.model_id,
)
continue
if u.evaluate_before_join:
matching_ids = (
self.env[this.model_id]
.search(self._eval_domain(u.domain))
.ids
)
domain = expression.OR([
domain,
[('id', 'in', matching_ids)],
])
else:
domain = expression.OR(
[domain, self._eval_domain(u.domain)]
)
for c in this.complement_filter_ids:
if c.model_id != this.model_id:
_logger.warning(
"Model mismatch in helper %d on filter %d!",
c.model_id,
this.model_id,
)
continue
if c.evaluate_before_negate:
matching_ids = (
self.env[this.model_id]
.search(self._eval_domain(c.domain))
.ids
)
domain = expression.AND([
domain,
[('id', 'not in', matching_ids)],
])
else:
domain = expression.AND([
domain,
["!"] + self._eval_domain(c["domain"])
])
this.domain = repr(expression.normalize_domain(domain))
@api.multi
def _inverse_domain(self):
for this in self:
this.domain_this = this.domain
@api.multi
@api.depends('domain', 'model_id')
def _compute_pre_evaluate(self):
"""Check if this filter contains problematic fields.
Domains with certain fields can't be negated or joined properly.
They have to be evaluated in advance. In particular:
- Negating a query on a 2many field doesn't invert the matched records.
``foo.bar != 3`` will yield all records with a ``foo.bar`` that
isn't ``3``, not all records without a ``foo.bar`` that is ``3``.
So just putting a ``!`` in front of the domain doesn't do what we
want.
- Querying an autojoined field constrains the entire search, even if
it's joined with ``|``. If ``('user_ids.login', '=', 'admin')`` is
used anywhere in the domain then only records that satisfy that leaf
are found.
- Fields with custom search logic (``search=...``) might do one of the
above, or some other strange thing.
Examples can be found in the tests for this module.
"""
for this in self:
evaluate_before_negate = False
evaluate_before_join = False
domain = self._eval_domain(this.domain)
for arg in domain:
if not expression.is_leaf(arg) or not isinstance(arg[0], str):
continue
current_model = self.env.get(this.model_id)
if current_model is None:
_logger.error(
"Unknown model %s used in filter %d",
this.model_id,
this.id,
)
continue
for field_name in arg[0].split('.'):
if field_name in models.MAGIC_COLUMNS:
continue
field = current_model._fields[field_name]
evaluate_before_negate = (
evaluate_before_negate
or field.search
or field.type in self._evaluate_before_negate
or getattr(field, "auto_join", False)
)
evaluate_before_join = (
evaluate_before_join
or field.search
or getattr(field, "auto_join", False)
)
if field.comodel_name:
current_model = self.env.get(field.comodel_name)
if current_model is None or (
evaluate_before_negate and evaluate_before_join
):
break
if evaluate_before_negate and evaluate_before_join:
break
this.evaluate_before_negate = evaluate_before_negate
this.evaluate_before_join = evaluate_before_join
@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
this.write({
'domain': repr([('id', 'in', ids)]),
'union_filter_ids': [(6, 0, [])],
'complement_filter_ids': [(6, 0, [])],
})
@api.multi
def write(self, vals):
if not ("union_filter_ids" in vals or "complement_filter_ids" in vals):
return super().write(vals)
old = self.mapped("union_filter_ids") | self.mapped(
"complement_filter_ids"
)
res = super().write(vals)
new = self.mapped("union_filter_ids") | self.mapped(
"complement_filter_ids"
)
(old - new)._garbage_collect_helpers()
return res
@api.multi
def unlink(self):
helpers = self.mapped("union_filter_ids") | self.mapped(
"complement_filter_ids"
)
res = super().unlink()
helpers._garbage_collect_helpers()
return res
@api.multi
def _garbage_collect_helpers(self):
"""Remove filters that have been made obsolete.
This method should only be called on filters that are/were in some
other filter's union_filter_ids or complement_filter_ids.
Filters are removed if they're not active and no longer used.
"""
if not self:
return
self.env.cr.execute(
"""DELETE FROM ir_filters
WHERE NOT active AND id IN %s
AND NOT EXISTS (
SELECT left_filter_id FROM ir_filters_union_rel
WHERE right_filter_id = id
)
AND NOT EXISTS (
SELECT left_filter_id FROM ir_filters_complement_rel
WHERE right_filter_id = id
)""",
(tuple(self.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,2 @@
* Holger Brunn <hbrunn@therp.nl>
* Jan Verbeek <jverbeek@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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,448 @@
<?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>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>
<li>Jan Verbeek &lt;<a class="reference external" href="mailto:jverbeek&#64;therp.nl">jverbeek&#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('Search query'),
},
{
itemId: 'domain-new',
description: _t('To new filter'),
callback: this._advanced_filters_save_criteria,
},
{
itemId: 'domain-union',
description: _t('Add to existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'union', 'domain'
),
},
{
itemId: 'domain-complement',
description: _t('Subtract 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('Add to existing filter'),
callback: this._advanced_filters_combine_with_existing.bind(
this, 'union', 'ids'
),
},
{
itemId: 'ids-complement',
description: _t('Subtract 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">Search query</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">Add to existing filter</button>
<button type="button" class="dropdown-item o_advanced_filter-remove">Subtract 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">Add to existing filter</button>
<button type="button" class="dropdown-item o_advanced_filter-remove">Subtract from existing filter</button>
</div>
</t>
</templates>

View File

@ -0,0 +1,3 @@
# Copyright 2020 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import test_web_advanced_filter

View File

@ -0,0 +1,196 @@
# Copyright 2020 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import json
import uuid
from odoo.osv import expression
from odoo.tests.common import TransactionCase
class TestWebAdvancedFilter(TransactionCase):
def setUp(self):
super().setUp()
self.domain = [
'|',
('id', '=', self.env.ref('base.res_partner_1').id),
('id', '=', self.env.ref('base.res_partner_2').id),
]
self.domain_add = [
'|',
('id', '=', self.env.ref('base.res_partner_3').id),
('id', '=', self.env.ref('base.res_partner_4').id),
]
self.domain_remove = [
'|',
('id', '=', self.env.ref('base.res_partner_3').id),
('id', '=', self.env.ref('base.res_partner_10').id),
]
self.extra_partner = self.env.ref('base.res_partner_12')
self.test_filter = self.env['ir.filters'].create({
'name': 'testfilter',
'domain': json.dumps(self.domain),
'model_id': 'res.partner',
})
def test_union_existing(self):
"""test adding selections/domains to a filter"""
self.assertEqual(
self.env['res.partner'].search(self.domain),
self.test_filter._evaluate(),
)
self._combine('union', self.domain_add)
self.assertItemsEqual(
self.env['res.partner'].search(self.domain) +
self.env['res.partner'].search(self.domain_add),
self.test_filter._evaluate(),
)
self.assertFalse(self.test_filter.is_frozen)
self.test_filter.button_freeze()
self.assertTrue(self.test_filter.is_frozen)
# adding a set of ids to a frozen filter must result in a frozen filter
# itself (=just a list of ids)
self._combine('union', [('id', 'in', self.extra_partner.ids)])
self.assertItemsEqual(
self.extra_partner +
self.env['res.partner'].search(self.domain) +
self.env['res.partner'].search(self.domain_add),
self.test_filter._evaluate(),
)
self.assertTrue(self.test_filter.is_frozen)
def test_complement_existing(self):
"""test removing selections/domains from a filter"""
self._combine('union', self.domain_add)
self._combine('complement', self.domain_remove)
self.assertItemsEqual(
self.env['res.partner'].search(self.domain) +
self.env['res.partner'].search(self.domain_add) -
self.env['res.partner'].search(self.domain_remove),
self.test_filter._evaluate(),
)
self.test_filter.button_freeze()
remove_partner = self.env.ref('base.res_partner_1')
self._combine('complement', [('id', 'in', remove_partner.ids)])
self.assertItemsEqual(
self.env['res.partner'].search(self.domain) +
self.env['res.partner'].search(self.domain_add) -
self.env['res.partner'].search(self.domain_remove) -
remove_partner,
self.test_filter._evaluate(),
)
self.assertTrue(self.test_filter.is_frozen)
def test_x2many(self):
"""test if combined filters behave correctly for x2many fields"""
self._combine(
'complement', [(
'category_id', 'in',
self.env.ref('base.res_partner_category_12').ids,
)],
)
self.assertTrue(
self.test_filter.complement_filter_ids.evaluate_before_negate
)
self.assertFalse(
self.test_filter.complement_filter_ids.evaluate_before_join
)
def test_computed_field(self):
"""computed fields always need evaluation"""
self.assertTrue(
self.env["res.groups"]._fields["full_name"].search,
"res.groups.full_name doesn't have custom search, outdated test",
)
filt = self._create_filter(
model_id="res.groups", domain=[("full_name", "=", "test")]
)
self.assertTrue(filt.evaluate_before_negate)
self.assertTrue(filt.evaluate_before_join)
def test_garbage_collection(self):
helper1 = self._create_filter(active=False)
helper2 = self._create_filter(active=False)
helper3 = self._create_filter()
filter1 = self._create_filter(
unions=helper1, complements=helper2 | helper3
)
filter2 = self._create_filter(unions=helper1)
self.assertTrue(helper1.exists())
self.assertTrue(helper2.exists())
filter1.unlink()
# Still used by others → kept
self.assertTrue(helper1.exists())
# Not used by others → deleted
self.assertFalse(helper2.exists())
# Not a "hidden" filter → kept
self.assertTrue(helper3.exists())
filter2.write({"union_filter_ids": [(5, 0, 0)]})
self.assertFalse(helper1.exists())
self.assertFalse(helper2.exists())
self.assertTrue(helper3.exists())
def test_autojoin_eval_necessity(self):
self.assertTrue(
self.env["res.partner"]._fields["user_ids"].auto_join,
"res.partner.user_ids is no longer autojoin, change the test",
)
normal_domain = [("id", "=", self.env.ref("base.main_partner").id)]
autojoin_domain = [("user_ids.login", "=", "admin")]
merged_domain = expression.OR([normal_domain, autojoin_domain])
search = self.env["res.partner"].search
self.assertNotEqual(
search(normal_domain) | search(autojoin_domain),
search(merged_domain),
"Searching autojoined fields is not buggy, is the workaround "
"still needed?",
)
helper = self._create_filter(domain=autojoin_domain)
main = self._create_filter(domain=normal_domain, unions=helper)
self.assertEqual(
main._evaluate(), search(normal_domain) | search(autojoin_domain)
)
def test_2many_eval_necessity(self):
category = self.env.ref("base.res_partner_category_14")
domain = [("category_id.name", "=", category.name)]
search = self.env["res.partner"].search
self.assertTrue(
search(domain) & search(["!"] + domain),
"Negating across 2many relations is not buggy, is the workaround "
"still needed?",
)
helper = self._create_filter(domain=domain)
main = self._create_filter(domain=domain, complements=helper)
self.assertFalse(main._evaluate())
def _create_filter(self, unions=(), complements=(), **vals):
vals.setdefault("model_id", "res.partner")
vals.setdefault("name", str(uuid.uuid4()))
vals.setdefault("domain", "[(1, '=', 1)]")
vals.setdefault(
"union_filter_ids", [(6, 0, [rec.id for rec in unions])]
)
vals.setdefault(
"complement_filter_ids", [(6, 0, [rec.id for rec in complements])]
)
return self.env["ir.filters"].create(vals)
def _combine(self, action, domain, *, model="res.partner"):
"""run the combination wizard with some default values"""
self.env['ir.filters.combine.with.existing'].create({
'action': action,
'domain': json.dumps(domain),
'model': model,
'filter_id': self.test_filter.id
}).button_save()

View File

@ -0,0 +1,54 @@
<?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 exactly 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', '!=', []), ('complement_filter_ids', '!=', [])]}
</attribute>
</field>
<field name="domain" position="after">
<field name="domain_this"
widget="domain"
options="{'model': 'model_id'}"
attrs="{'invisible': [('union_filter_ids', '=', []), ('complement_filter_ids', '=', [])]}" />
</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,70 @@
# 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()
domain = json.loads(self.domain)
is_frozen = (
len(domain) == 1
and expression.is_leaf(domain[0])
and domain[0][0] == "id"
and domain[0][1] == "in"
)
if self.action == 'union':
if is_frozen and self.filter_id.is_frozen:
domain[0][2] = list(set(domain[0][2]).union(
set(safe_eval(self.filter_id.domain)[0][2])))
self.filter_id.write({'domain': repr(domain)})
else:
self.filter_id.write({
'union_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
self.filter_id.name, 'add', time.time()),
'active': False,
'domain': repr(domain),
'context': repr(json.loads(self.context)),
'model_id': self.model,
'user_id': self.filter_id.user_id.id or False,
})],
})
elif self.action == 'complement':
if is_frozen and self.filter_id.is_frozen:
complement_set = set(safe_eval(self.filter_id.domain)[0][2])
domain[0][2] = list(
complement_set.difference(set(domain[0][2])))
self.filter_id.write({'domain': repr(domain)})
else:
self.filter_id.write({
'complement_filter_ids': [(0, 0, {
'name': '%s_%s_%d' % (
self.filter_id.name, 'remove', time.time()),
'active': False,
'domain': repr(domain),
'context': repr(json.loads(self.context)),
'model_id': self.model,
'user_id': self.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>