diff --git a/advanced_filters/__init__.py b/advanced_filters/__init__.py new file mode 100644 index 00000000..500e771d --- /dev/null +++ b/advanced_filters/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import model +from . import wizard diff --git a/advanced_filters/__openerp__.py b/advanced_filters/__openerp__.py new file mode 100644 index 00000000..3cd69411 --- /dev/null +++ b/advanced_filters/__openerp__.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Advanced filters", + "version": "1.0", + "author": "Therp BV", + "license": "AGPL-3", + "complexity": "normal", + "description": """ +Introduction +------------ + +This addon allows users to apply set operations on filters: Remove or add +certain ids from/to a selection, but also to remove or add another filter's +outcome from/to a filter. This can be stacked, so the filter domain can be +arbitrarily complicated. + +The math is hidden from the user as far as possible, in the hope it's still +user friendly. + +Usage +----- + +After this addon is installed, every list view shows a new menu 'Advanced +filters'. Here the set operations can be applied as necessary. + +Caution +------- + +Deinstalling this module will leave you with filters with empty domains. Use +this query before uninstalling to avoid that: + +``alter table ir_filters rename domain_this to domain`` + """, + "category": "Tools", + "depends": [ + 'base', + 'web', + ], + "data": [ + "wizard/ir_filters_combine_with_existing.xml", + "view/ir_filters.xml", + ], + "js": [ + 'static/src/js/advanced_filters.js', + ], + "css": [ + 'static/src/css/advanced_filters.css', + ], + "qweb": [ + ], + "test": [ + ], + "auto_install": False, + "installable": True, + "application": False, + "external_dependencies": { + 'python': [], + }, +} diff --git a/advanced_filters/model/__init__.py b/advanced_filters/model/__init__.py new file mode 100644 index 00000000..bbae0587 --- /dev/null +++ b/advanced_filters/model/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import ir_filters diff --git a/advanced_filters/model/ir_filters.py b/advanced_filters/model/ir_filters.py new file mode 100644 index 00000000..a709a52a --- /dev/null +++ b/advanced_filters/model/ir_filters.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import itertools +from openerp.osv.orm import Model +from openerp.osv import fields, expression +from openerp.tools.safe_eval import const_eval +from openerp.tools.translate import _ + + +class IrFilters(Model): + _inherit = 'ir.filters' + + def _is_frozen_get(self, cr, uid, ids, field_name, args, context=None): + '''determine if this is fixed list of ids''' + result = {} + for this in self.browse(cr, uid, ids, context=context): + domain = const_eval(this.domain) + result[this.id] = (len(domain) == 1 and + expression.is_leaf(domain[0]) and + domain[0][0] == 'id') + return result + + def _domain_get(self, cr, uid, ids, field_name, args, context=None): + '''combine our domain with all domains to union/complement, + this works recursively''' + def eval_n(domain): + '''parse a domain and normalize it''' + return expression.normalize_domain( + const_eval(domain) or [expression.FALSE_LEAF]) + result = {} + for this in self.read( + cr, uid, ids, + ['domain_this', 'union_filter_ids', 'complement_filter_ids'], + context=context): + domain = eval_n(this['domain_this']) + domain = expression.OR( + [domain] + + [eval_n(u['domain']) for u in self.read( + cr, uid, this['union_filter_ids'], ['domain'], + context=context)]) + for c in self.read(cr, uid, this['complement_filter_ids'], + ['domain'], context=context): + domain = expression.AND([ + domain, + ['!'] + eval_n(c['domain'])]) + result[this['id']] = str(domain) + return result + + def _domain_set(self, cr, uid, ids, field_name, field_value, args, + context=None): + self.write(cr, uid, ids, {'domain_this': field_value}) + + _columns = { + 'is_frozen': fields.function( + _is_frozen_get, type='boolean', string='Frozen'), + 'union_filter_ids': fields.many2many( + 'ir.filters', 'ir_filters_union_rel', 'left_filter_id', + 'right_filter_id', 'Add result of filters', + domain=['|', ('active', '=', False), ('active', '=', True)]), + 'complement_filter_ids': fields.many2many( + 'ir.filters', 'ir_filters_complement_rel', 'left_filter_id', + 'right_filter_id', 'Remove result of filters', + domain=['|', ('active', '=', False), ('active', '=', True)]), + 'active': fields.boolean('Active'), + 'domain': fields.function( + _domain_get, type='text', string='Domain', + fnct_inv=_domain_set), + 'domain_this': fields.text( + 'This filter\'s own domain', oldname='domain'), + } + + _defaults = { + 'active': True, + } + + def _evaluate(self, cr, uid, ids, context=None): + assert len(ids) == 1 + this = self.browse(cr, uid, ids[0], context=context) + return self.pool[this.model_id].search( + cr, uid, const_eval(this.domain), context=const_eval(this.context)) + + def button_save(self, cr, uid, ids, context=None): + return {'type': 'ir.actions.act_window.close'} + + def button_freeze(self, cr, uid, ids, context=None): + '''evaluate the filter and write a fixed [('ids', 'in', [])] domain''' + for this in self.browse(cr, uid, ids, context=context): + ids = this._evaluate() + removed_filter_ids = [f.id for f in itertools.chain( + this.union_filter_ids, this.complement_filter_ids)] + this.write({ + 'domain': str([('id', 'in', ids)]), + 'union_filter_ids': [(6, 0, [])], + 'complement_filter_ids': [(6, 0, [])], + }) + #if we removed inactive filters which are orphaned now, delete them + cr.execute('''delete from ir_filters + where + not active and id in %s + and not exists (select right_filter_id + from ir_filters_union_rel where left_filter_id=id) + and not exists (select right_filter_id + from ir_filters_complement_rel where + left_filter_id=id) + ''', + (tuple(removed_filter_ids),)) + + def button_test(self, cr, uid, ids, context=None): + for this in self.browse(cr, uid, ids, context=None): + return { + 'type': 'ir.actions.act_window', + 'name': _('Testing %s') % this.name, + 'res_model': this.model_id, + 'domain': this.domain, + 'view_type': 'form', + 'view_mode': 'tree', + 'context': { + 'default_filter_id': this.id, + }, + } + + def _auto_init(self, cr, context=None): + cr.execute( + 'SELECT count(attname) FROM pg_attribute ' + 'WHERE attrelid = ' + '( SELECT oid FROM pg_class WHERE relname = %s) ' + 'AND attname = %s', (self._table, 'domain_this')) + if not cr.fetchone()[0]: + cr.execute( + 'ALTER table %s RENAME domain TO domain_this' % self._table) + return super(IrFilters, self)._auto_init(cr, context=context) diff --git a/advanced_filters/static/src/css/advanced_filters.css b/advanced_filters/static/src/css/advanced_filters.css new file mode 100644 index 00000000..06a3c8ec --- /dev/null +++ b/advanced_filters/static/src/css/advanced_filters.css @@ -0,0 +1,14 @@ +li.oe_advanced_filters_header +{ + font-weight: bold; +} +.openerp .oe_dropdown_menu > li.oe_advanced_filters_header:hover +{ + background-color: inherit; + background-image: inherit; + box-shadow: none; +} +.openerp .oe_dropdown_menu > li.oe_advanced_filters_header a:hover +{ + cursor: default !important; +} diff --git a/advanced_filters/static/src/img/icon.png b/advanced_filters/static/src/img/icon.png new file mode 100644 index 00000000..29372604 Binary files /dev/null and b/advanced_filters/static/src/img/icon.png differ diff --git a/advanced_filters/static/src/js/advanced_filters.js b/advanced_filters/static/src/js/advanced_filters.js new file mode 100644 index 00000000..63c59d04 --- /dev/null +++ b/advanced_filters/static/src/js/advanced_filters.js @@ -0,0 +1,209 @@ +//-*- coding: utf-8 -*- +//############################################################################ +// +// OpenERP, Open Source Management Solution +// This module copyright (C) 2014 Therp BV (). +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +//############################################################################ + +openerp.advanced_filters = function(instance) +{ + var _t = instance.web._t; + + instance.web.Sidebar.include({ + init: function() + { + var result = this._super.apply(this, arguments); + this.sections.push({ + 'name': 'advanced_filters', + 'label': _t('Advanced filters'), + }); + this.items.advanced_filters = []; + return result; + }, + }); + instance.web.ListView.include({ + do_select: function (ids, records) + { + var result = this._super(this, arguments); + if(this.sidebar) + { + this.sidebar.$el.show(); + this.sidebar.$el.children().children().each(function(i, e) + { + $e = jQuery(e) + if($e.find('li.oe_advanced_filters_header').length) + { + $e.find('a[data-index="0"],a[data-index="1"],' + + 'a[data-index="2"],a[data-index="3"]') + .parent().toggle(ids.length); + } + else + { + $e.toggle(ids.length); + } + }); + } + return result; + }, + load_list: function(data) + { + var result = this._super.apply(this, arguments), + self = this; + if(!this.sidebar || this.sidebar.items.advanced_filters.length) + { + return result; + } + this.sidebar.add_items( + 'advanced_filters', + [ + { + label: _t('Marked records'), + classname: 'oe_advanced_filters_header', + }, + { + label: _t('To new selection'), + callback: function () + { + self.advanced_filters_save_selection.apply( + self, arguments); + }, + }, + { + label: _t('To existing selection'), + callback: function (item) + { + self.advanced_filters_combine_with_existing.apply( + self, ['union', 'ids', item]); + }, + }, + { + label: _t('Remove from existing selection'), + callback: function (item) + { + self.advanced_filters_combine_with_existing.apply( + self, ['complement', 'ids', item]); + }, + }, + { + label: _t('Whole result set'), + classname: 'oe_advanced_filters_header', + }, + { + label: _t('To existing selection'), + callback: function (item) + { + self.advanced_filters_combine_with_existing.apply( + self, ['union', 'domain', item]); + }, + }, + { + label: _t('Remove from existing selection'), + callback: function (item) + { + self.advanced_filters_combine_with_existing.apply( + self, ['complement', 'domain', item]); + }, + }, + ] + ); + this.do_select([], []); + return result; + }, + advanced_filters_save_selection: function(item) + { + var self = this; + this.do_action({ + name: item.label, + type: 'ir.actions.act_window', + res_model: 'ir.filters', + views: [[false, 'form']], + target: 'new', + context: { + default_model_id: this.dataset._model.name, + default_domain: JSON.stringify( + [ + ['id', 'in', this.groups.get_selection().ids], + ] + ), + default_context: JSON.stringify({}), + form_view_ref: 'advanced_filters.form_ir_filters_save_new', + }, + }, + { + on_close: function() + { + self.ViewManager.setup_search_view( + self.ViewManager.searchview.view_id, + self.ViewManager.searchview.defaults); + }, + }); + }, + advanced_filters_combine_with_existing: function(action, type, item) + { + var search = this.ViewManager.searchview.build_search_data(), + self = this; + instance.web.pyeval.eval_domains_and_contexts({ + domains: search.domains, + contexts: search.contexts, + group_by_seq: search.groupbys || [] + }).done(function(search) + { + var domain = [], ctx = {}; + switch(type) + { + case 'domain': + domain = search.domain; + ctx = search.context; + _(_.keys(instance.session.user_context)).each( + function (key) {delete ctx[key]}); + break; + case 'ids': + domain = [ + ['id', 'in', self.groups.get_selection().ids], + ] + ctx = {}; + break; + } + self.do_action({ + name: item.label, + type: 'ir.actions.act_window', + res_model: 'ir.filters.combine.with.existing', + views: [[false, 'form']], + target: 'new', + context: _.extend({ + default_model: self.dataset._model.name, + default_domain: JSON.stringify(domain), + default_action: action, + default_context: JSON.stringify(ctx), + }, + self.dataset.context.default_filter_id ? { + default_filter_id: + self.dataset.context.default_filter_id, + } : {}), + }, + { + on_close: function() + { + self.ViewManager.setup_search_view( + self.ViewManager.searchview.view_id, + self.ViewManager.searchview.defaults); + }, + }); + }); + }, + }); +} diff --git a/advanced_filters/view/ir_filters.xml b/advanced_filters/view/ir_filters.xml new file mode 100644 index 00000000..057db6b1 --- /dev/null +++ b/advanced_filters/view/ir_filters.xml @@ -0,0 +1,44 @@ + + + + + ir.filters + + + +
+
+
+ + + + + + + + + + +
+
+ + ir.filters + 999 + +
+ + + + +
+
+
+
+
+
+
diff --git a/advanced_filters/wizard/__init__.py b/advanced_filters/wizard/__init__.py new file mode 100644 index 00000000..608ed687 --- /dev/null +++ b/advanced_filters/wizard/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import ir_filters_combine_with_existing diff --git a/advanced_filters/wizard/ir_filters_combine_with_existing.py b/advanced_filters/wizard/ir_filters_combine_with_existing.py new file mode 100644 index 00000000..0641caf8 --- /dev/null +++ b/advanced_filters/wizard/ir_filters_combine_with_existing.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import time +from openerp.osv.orm import TransientModel +from openerp.osv import fields, expression +from openerp.tools.safe_eval import const_eval + + +class IrFiltersCombineWithExisting(TransientModel): + _name = 'ir.filters.combine.with.existing' + _description = 'Combine a selection with an existing filter' + + _columns = { + 'action': fields.selection( + [('union', 'Union'), ('complement', 'Complement')], + 'Action', required=True), + 'domain': fields.char('Domain', required=True), + 'context': fields.char('Context', required=True), + 'model': fields.char('Model', required=True), + 'filter_id': fields.many2one('ir.filters', 'Filter', required=True), + } + + def button_save(self, cr, uid, ids, context=None): + assert len(ids) == 1 + this = self.browse(cr, uid, ids[0], context=context) + domain = const_eval(this.domain) + is_frozen = (len(domain) == 1 and + expression.is_leaf(domain[0]) and + domain[0][0] == 'id') + + if this.action == 'union': + if is_frozen and this.filter_id.is_frozen: + domain[0][2] = list(set(domain[0][2]).union( + set(const_eval(this.filter_id.domain)[0][2]))) + this.filter_id.write({'domain': str(domain)}) + else: + this.filter_id.write( + { + 'union_filter_ids': [(0, 0, { + 'name': '%s_%s_%d' % ( + this.filter_id.name, 'add', time.time()), + 'active': False, + 'domain': str(domain), + 'context': this.context, + 'model_id': this.model, + 'user_id': uid, + })], + }) + elif this.action == 'complement': + if is_frozen and this.filter_id.is_frozen: + complement_set = set(const_eval(this.filter_id.domain)[0][2]) + domain[0][2] = list( + complement_set.difference(set(domain[0][2]))) + this.filter_id.write({'domain': str(domain)}) + else: + this.filter_id.write( + { + 'complement_filter_ids': [(0, 0, { + 'name': '%s_%s_%d' % ( + this.filter_id.name, 'remove', time.time()), + 'active': False, + 'domain': str(domain), + 'context': this.context, + 'model_id': this.model, + 'user_id': uid, + })], + }) + + return {'type': 'ir.actions.act_window.close'} diff --git a/advanced_filters/wizard/ir_filters_combine_with_existing.xml b/advanced_filters/wizard/ir_filters_combine_with_existing.xml new file mode 100644 index 00000000..bc7028d3 --- /dev/null +++ b/advanced_filters/wizard/ir_filters_combine_with_existing.xml @@ -0,0 +1,21 @@ + + + + + ir.filters.combine.with.existing + +
+ + + + + +
+
+
+
+