From bbeddba6e151e9475abd9808b6717a5f1ddb0807 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Tue, 21 Apr 2020 23:47:25 +0200 Subject: [PATCH] [MIG] web_advanced_filter --- web_advanced_filter/README.rst | 100 ++++ web_advanced_filter/__init__.py | 18 + web_advanced_filter/__manifest__.py | 24 + .../i18n/advanced_filter.pot | 0 .../i18n/de.po | 0 .../i18n/nl.po | 0 web_advanced_filter/i18n/nl_NL.po | 231 +++++++++ web_advanced_filter/models/__init__.py | 1 + web_advanced_filter/models/ir_filters.py | 215 +++++++++ web_advanced_filter/readme/CONTRIBUTORS.rst | 1 + web_advanced_filter/readme/CREDITS.rst | 1 + web_advanced_filter/readme/DESCRIPTION.rst | 8 + web_advanced_filter/readme/ROADMAP.rst | 2 + web_advanced_filter/readme/USAGE.rst | 5 + .../static/description}/icon.png | Bin .../static/description/index.html | 447 ++++++++++++++++++ .../static/src/css/web_advanced_filter.css | 7 + .../static/src/js/web_advanced_filter.js | 256 ++++++++++ .../static/src/xml/web_advanced_filter.xml | 18 + web_advanced_filter/views/ir_filters.xml | 49 ++ web_advanced_filter/views/templates.xml | 11 + web_advanced_filter/wizards/__init__.py | 1 + .../ir_filters_combine_with_existing.py | 68 +++ .../ir_filters_combine_with_existing.xml | 18 + web_advanced_filters/__init__.py | 22 - web_advanced_filters/__openerp__.py | 79 ---- web_advanced_filters/data/migration.xml | 6 - web_advanced_filters/model/__init__.py | 21 - web_advanced_filters/model/ir_filters.py | 259 ---------- .../static/src/css/web_advanced_filters.css | 14 - .../static/src/js/web_advanced_filters.js | 272 ----------- web_advanced_filters/view/ir_filters.xml | 51 -- web_advanced_filters/wizard/__init__.py | 21 - .../ir_filters_combine_with_existing.py | 88 ---- .../ir_filters_combine_with_existing.xml | 21 - 35 files changed, 1481 insertions(+), 854 deletions(-) create mode 100644 web_advanced_filter/README.rst create mode 100644 web_advanced_filter/__init__.py create mode 100644 web_advanced_filter/__manifest__.py rename {web_advanced_filters => web_advanced_filter}/i18n/advanced_filter.pot (100%) rename {web_advanced_filters => web_advanced_filter}/i18n/de.po (100%) rename {web_advanced_filters => web_advanced_filter}/i18n/nl.po (100%) create mode 100644 web_advanced_filter/i18n/nl_NL.po create mode 100644 web_advanced_filter/models/__init__.py create mode 100644 web_advanced_filter/models/ir_filters.py create mode 100644 web_advanced_filter/readme/CONTRIBUTORS.rst create mode 100644 web_advanced_filter/readme/CREDITS.rst create mode 100644 web_advanced_filter/readme/DESCRIPTION.rst create mode 100644 web_advanced_filter/readme/ROADMAP.rst create mode 100644 web_advanced_filter/readme/USAGE.rst rename {web_advanced_filters/static/src/img => web_advanced_filter/static/description}/icon.png (100%) create mode 100644 web_advanced_filter/static/description/index.html create mode 100644 web_advanced_filter/static/src/css/web_advanced_filter.css create mode 100644 web_advanced_filter/static/src/js/web_advanced_filter.js create mode 100644 web_advanced_filter/static/src/xml/web_advanced_filter.xml create mode 100644 web_advanced_filter/views/ir_filters.xml create mode 100644 web_advanced_filter/views/templates.xml create mode 100644 web_advanced_filter/wizards/__init__.py create mode 100644 web_advanced_filter/wizards/ir_filters_combine_with_existing.py create mode 100644 web_advanced_filter/wizards/ir_filters_combine_with_existing.xml delete mode 100644 web_advanced_filters/__init__.py delete mode 100644 web_advanced_filters/__openerp__.py delete mode 100644 web_advanced_filters/data/migration.xml delete mode 100644 web_advanced_filters/model/__init__.py delete mode 100644 web_advanced_filters/model/ir_filters.py delete mode 100644 web_advanced_filters/static/src/css/web_advanced_filters.css delete mode 100644 web_advanced_filters/static/src/js/web_advanced_filters.js delete mode 100644 web_advanced_filters/view/ir_filters.xml delete mode 100644 web_advanced_filters/wizard/__init__.py delete mode 100644 web_advanced_filters/wizard/ir_filters_combine_with_existing.py delete mode 100644 web_advanced_filters/wizard/ir_filters_combine_with_existing.xml diff --git a/web_advanced_filter/README.rst b/web_advanced_filter/README.rst new file mode 100644 index 000000000..1d41f41f9 --- /dev/null +++ b/web_advanced_filter/README.rst @@ -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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Therp BV + +Contributors +~~~~~~~~~~~~ + +* Holger Brunn + +Other credits +~~~~~~~~~~~~~ + +* Odoo Community Association: `Icon `_. + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_advanced_filter/__init__.py b/web_advanced_filter/__init__.py new file mode 100644 index 000000000..a0e7f5247 --- /dev/null +++ b/web_advanced_filter/__init__.py @@ -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') diff --git a/web_advanced_filter/__manifest__.py b/web_advanced_filter/__manifest__.py new file mode 100644 index 000000000..17aa25d9f --- /dev/null +++ b/web_advanced_filter/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2014 Therp BV +# 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", +} diff --git a/web_advanced_filters/i18n/advanced_filter.pot b/web_advanced_filter/i18n/advanced_filter.pot similarity index 100% rename from web_advanced_filters/i18n/advanced_filter.pot rename to web_advanced_filter/i18n/advanced_filter.pot diff --git a/web_advanced_filters/i18n/de.po b/web_advanced_filter/i18n/de.po similarity index 100% rename from web_advanced_filters/i18n/de.po rename to web_advanced_filter/i18n/de.po diff --git a/web_advanced_filters/i18n/nl.po b/web_advanced_filter/i18n/nl.po similarity index 100% rename from web_advanced_filters/i18n/nl.po rename to web_advanced_filter/i18n/nl.po diff --git a/web_advanced_filter/i18n/nl_NL.po b/web_advanced_filter/i18n/nl_NL.po new file mode 100644 index 000000000..bf6229551 --- /dev/null +++ b/web_advanced_filter/i18n/nl_NL.po @@ -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 "" diff --git a/web_advanced_filter/models/__init__.py b/web_advanced_filter/models/__init__.py new file mode 100644 index 000000000..4c520abbe --- /dev/null +++ b/web_advanced_filter/models/__init__.py @@ -0,0 +1 @@ +from . import ir_filters diff --git a/web_advanced_filter/models/ir_filters.py b/web_advanced_filter/models/ir_filters.py new file mode 100644 index 000000000..e2553fe63 --- /dev/null +++ b/web_advanced_filter/models/ir_filters.py @@ -0,0 +1,215 @@ +# Copyright 2014-2020 Therp BV +# 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, + }, + } diff --git a/web_advanced_filter/readme/CONTRIBUTORS.rst b/web_advanced_filter/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b120a956f --- /dev/null +++ b/web_advanced_filter/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Holger Brunn diff --git a/web_advanced_filter/readme/CREDITS.rst b/web_advanced_filter/readme/CREDITS.rst new file mode 100644 index 000000000..cc056a80d --- /dev/null +++ b/web_advanced_filter/readme/CREDITS.rst @@ -0,0 +1 @@ +* Odoo Community Association: `Icon `_. diff --git a/web_advanced_filter/readme/DESCRIPTION.rst b/web_advanced_filter/readme/DESCRIPTION.rst new file mode 100644 index 000000000..172cd82b8 --- /dev/null +++ b/web_advanced_filter/readme/DESCRIPTION.rst @@ -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. + diff --git a/web_advanced_filter/readme/ROADMAP.rst b/web_advanced_filter/readme/ROADMAP.rst new file mode 100644 index 000000000..fff5a1c56 --- /dev/null +++ b/web_advanced_filter/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* client side tests +* consider renaming all instances of `Favorites` in core to `Filters` for consistency diff --git a/web_advanced_filter/readme/USAGE.rst b/web_advanced_filter/readme/USAGE.rst new file mode 100644 index 000000000..c6295e900 --- /dev/null +++ b/web_advanced_filter/readme/USAGE.rst @@ -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. diff --git a/web_advanced_filters/static/src/img/icon.png b/web_advanced_filter/static/description/icon.png similarity index 100% rename from web_advanced_filters/static/src/img/icon.png rename to web_advanced_filter/static/description/icon.png diff --git a/web_advanced_filter/static/description/index.html b/web_advanced_filter/static/description/index.html new file mode 100644 index 000000000..09e67ab6d --- /dev/null +++ b/web_advanced_filter/static/description/index.html @@ -0,0 +1,447 @@ + + + + + + +Advanced filters + + + +
+

Advanced filters

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runbot

+

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

+ +
+

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. +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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+
    +
  • Odoo Community Association: Icon.
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_advanced_filter/static/src/css/web_advanced_filter.css b/web_advanced_filter/static/src/css/web_advanced_filter.css new file mode 100644 index 000000000..725745197 --- /dev/null +++ b/web_advanced_filter/static/src/css/web_advanced_filter.css @@ -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; +} diff --git a/web_advanced_filter/static/src/js/web_advanced_filter.js b/web_advanced_filter/static/src/js/web_advanced_filter.js new file mode 100644 index 000000000..11ae16ec8 --- /dev/null +++ b/web_advanced_filter/static/src/js/web_advanced_filter.js @@ -0,0 +1,256 @@ +// Copyright 2014-2020 Therp BV +// 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, +} +}); diff --git a/web_advanced_filter/static/src/xml/web_advanced_filter.xml b/web_advanced_filter/static/src/xml/web_advanced_filter.xml new file mode 100644 index 000000000..ba37d544e --- /dev/null +++ b/web_advanced_filter/static/src/xml/web_advanced_filter.xml @@ -0,0 +1,18 @@ + + + +