diff --git a/oca_dependencies.txt b/oca_dependencies.txt new file mode 100644 index 00000000..d6ebc90e --- /dev/null +++ b/oca_dependencies.txt @@ -0,0 +1 @@ +server-tools https://github.com/hbrunn/server-tools 8.0-base_view_inheritance_extension-user_ids diff --git a/web_listview_custom_column/README.rst b/web_listview_custom_column/README.rst new file mode 100644 index 00000000..92f53724 --- /dev/null +++ b/web_listview_custom_column/README.rst @@ -0,0 +1,79 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================== +Custom columns in listview +========================== + +This module was written to allow users to rearrange columns in list views. This can be done organization wide or just for the user herself. + +Configuration +============= + +To configure this module, you need to add users supposed to be able to customize columns to the group `Customize list views`. Note that this permission allows all sorts of mischief up to privilege escalation under certain circumstances, so this is only for trusted users. + +Usage +===== + +To use this module, you need to: + +#. go to some list and click the columns symbol left of the view switcher +#. use the buttons appearing in column headers to remove and rearrange columns +#. use the dropdown appearing next to the columns symbol to add columns +#. use the person or group symbols next to the dropdown to switch between for whom you customize +#. use the cross next to those to delete your customization. If there's a customization both for yourself and everyone, the first reset will put you on the customization for everyone, and the second will delete the customization for everyone + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/162/8.0 + +Known issues +============ + +- this addon creates standard view overrides. Those are created with priority 10000 to avoid side effects, but the views will break if you remove fields from the database. Uninstalling the module will remove all view customizations. +- when rearranging columns, invisible columns count. So if it seems like nothing happens, you'll probably have some invisible columns + +Roadmap +======= + +- support some kind of group level customization +- allow sharing customizations with others + +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. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Holger Brunn + +Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list `_ or the `appropriate specialized mailinglist `_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/web_listview_custom_column/__init__.py b/web_listview_custom_column/__init__.py new file mode 100644 index 00000000..a3e818a4 --- /dev/null +++ b/web_listview_custom_column/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/web_listview_custom_column/__openerp__.py b/web_listview_custom_column/__openerp__.py new file mode 100644 index 00000000..b7c1a60e --- /dev/null +++ b/web_listview_custom_column/__openerp__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Custom columns in listview", + "version": "8.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Tools", + "summary": "Remove or add columns to list views", + "depends": [ + 'web', + 'base_view_inheritance_extension', + ], + "data": [ + "security/res_groups.xml", + 'views/templates.xml', + 'security/ir.model.access.csv', + ], + "qweb": [ + 'static/src/xml/web_listview_custom_column.xml', + ], +} diff --git a/web_listview_custom_column/models/__init__.py b/web_listview_custom_column/models/__init__.py new file mode 100644 index 00000000..fe9c5d7e --- /dev/null +++ b/web_listview_custom_column/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import ir_ui_view diff --git a/web_listview_custom_column/models/ir_ui_view.py b/web_listview_custom_column/models/ir_ui_view.py new file mode 100644 index 00000000..4cb95b02 --- /dev/null +++ b/web_listview_custom_column/models/ir_ui_view.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# © 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from lxml import etree +from openerp import _, api, models, tools + + +class IrUiView(models.Model): + _inherit = 'ir.ui.view' + + @api.multi + def custom_column(self, diff): + """Apply a change for a custom view. Create a new custom view or + view override as necessary""" + self.ensure_one() + tree = etree.fromstring(self.read_combined(self.id)['arch']) + customized_view = self.env.ref( + self._custom_column_xmlid(diff), raise_if_not_found=False + ) or self.browse([]) + if diff['operation'] == 'add': + etree.SubElement(tree, 'field', {'name': diff['name']}) + elif diff['operation'] == 'remove': + for element in tree: + if element.attrib['name'] == diff['name'] and\ + element.tag == 'field': + tree.remove(element) + elif diff['operation'] == 'left': + for element in tree: + if element.attrib['name'] == diff['name'] and\ + element.tag == 'field' and\ + element.getprevious() is not None: + element.getprevious().addprevious(element) + break + elif diff['operation'] == 'right': + for element in tree: + if element.attrib['name'] == diff['name'] and\ + element.tag == 'field' and\ + element.getnext() is not None: + element.getnext().addnext(element) + break + elif diff['operation'] == 'reset': + customized_view.unlink() + return [] + elif diff['operation'] == 'to_user': + diff['type'] = 'user' + customized_view = self.env.ref( + self._custom_column_xmlid(diff), raise_if_not_found=False + ) or self.browse([]) + elif diff['operation'] == 'to_all': + customized_view.unlink() + diff['type'] = 'all' + customized_view = self.env.ref( + self._custom_column_xmlid(diff), raise_if_not_found=False + ) or self.browse([]) + else: + raise NotImplementedError( + 'Unknown operation %s' % diff['operation'] + ) + + replacement = etree.Element('tree', {'position': 'replace'}) + replacement.append(tree) + arch = etree.tostring(replacement, pretty_print=True) + if customized_view: + customized_view.write({'arch': arch}) + else: + customized_view = self._custom_column_create_view(diff, arch) + return customized_view.id + + @api.multi + def custom_column_desc(self): + """Return metadata necessary for UI""" + self.ensure_one() + return { + 'fields': self.env[self.model].fields_get(), + 'type': bool(self.env.ref( + self._custom_column_xmlid({'type': 'user'}), + raise_if_not_found=False + )) and 'user' or bool(self.env.ref( + self._custom_column_xmlid({'type': 'all'}), + raise_if_not_found=False + )) and 'all' or 'user', + } + + @api.multi + def _custom_column_xmlid(self, diff, qualify=True): + """Return an xmlid for the view of a type of customization""" + self.ensure_one() + customization_type = diff['type'] + return '%scustom_view_%d_%s%s' % ( + qualify and 'web_listview_custom_column.' or '', + self.id, + customization_type, + '_%d' % self.env.uid if customization_type == 'user' else '', + ) + + @api.multi + def _custom_column_create_view(self, diff, arch): + """Actually create a view for customization""" + self.ensure_one() + values = self.copy_data(default={ + 'name': _('%s customized') % self.name, + 'arch': arch, + 'inherit_id': self.id, + 'mode': 'extension', + 'priority': 10000 + (diff['type'] == 'user' and 1 or 0), + 'user_ids': [(4, self.env.uid)] if diff['type'] == 'user' else [], + })[0] + result = self.create(values) + self.env['ir.model.data'].create({ + 'name': self._custom_column_xmlid(diff, qualify=False), + 'module': 'web_listview_custom_column', + 'model': self._name, + 'res_id': result.id, + 'noupdate': True, + }) + return result + + @api.multi + def _check_xml(self): + """Don't validate our custom views, this will break in init mode""" + if self.env.registry._init: + self = self.filtered( + lambda x: not x.xml_id or not x.xml_id.startswith( + 'web_listview_custom_column.custom_view_' + ) + ) + return super(IrUiView, self)._check_xml() + + _constraints = [(_check_xml, 'Invalid view definition', ['arch'])] + + @api.model + def get_inheriting_views_arch(self, view_id, model): + """Don't apply our view inheritance in init mode for the same reason""" + return [ + (arch, view_id_) + for arch, view_id_ in + super(IrUiView, self).get_inheriting_views_arch(view_id, model) + if (not self.env.registry._init or tools.config['test_enable']) or + not self.sudo().browse(view_id_).xml_id or + not self.sudo().browse(view_id_).xml_id.startswith( + 'web_listview_custom_column.custom_view_' + ) + ] diff --git a/web_listview_custom_column/security/ir.model.access.csv b/web_listview_custom_column/security/ir.model.access.csv new file mode 100644 index 00000000..70df5ccf --- /dev/null +++ b/web_listview_custom_column/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +crud_ir_ui_view,"CRUD ir.ui.view",base.model_ir_ui_view,group_custom_column,1,1,1,1 diff --git a/web_listview_custom_column/security/res_groups.xml b/web_listview_custom_column/security/res_groups.xml new file mode 100644 index 00000000..97d34bc0 --- /dev/null +++ b/web_listview_custom_column/security/res_groups.xml @@ -0,0 +1,12 @@ + + + + + Customize list views + + + + + + + diff --git a/web_listview_custom_column/static/description/icon.png b/web_listview_custom_column/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/web_listview_custom_column/static/description/icon.png differ diff --git a/web_listview_custom_column/static/src/css/web_listview_custom_column.css b/web_listview_custom_column/static/src/css/web_listview_custom_column.css new file mode 100644 index 00000000..0546a764 --- /dev/null +++ b/web_listview_custom_column/static/src/css/web_listview_custom_column.css @@ -0,0 +1,40 @@ +.openerp .oe_view_manager_custom_column +{ + height: 24px; + line-height: 24px; + display: inline-block; + border: 1px solid #ababab; + cursor: pointer; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + max-width: calc(100% - 200px); +} +.openerp .oe_view_manager_custom_column a +{ + padding: 1px 6px; + color: #4c4c4c; +} +.openerp .oe_view_manager_custom_column a.active i +{ + text-shadow: 0 0 3px #ababab; +} +.openerp .oe_view_manager_custom_column select +{ + margin: 1px; + background: transparent; + max-width: calc(100% - 110px); +} +.openerp .oe_list th .oe_custom_column_remove, +.openerp .oe_list th .oe_custom_column_left, +.openerp .oe_list th .oe_custom_column_right +{ + color: #4c4c4c; + font-size: smaller; +} +.openerp .oe_list th .oe_custom_column_remove:hover, +.openerp .oe_list th .oe_custom_column_left:hover, +.openerp .oe_list th .oe_custom_column_right:hover +{ + text-shadow: 0px 0px 1px; +} diff --git a/web_listview_custom_column/static/src/js/web_listview_custom_column.js b/web_listview_custom_column/static/src/js/web_listview_custom_column.js new file mode 100644 index 00000000..d94259ac --- /dev/null +++ b/web_listview_custom_column/static/src/js/web_listview_custom_column.js @@ -0,0 +1,136 @@ +//-*- coding: utf-8 -*- +//Copyright 2017 Therp BV +//License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +openerp.web_listview_custom_column = function(instance) +{ + instance.web.ListView.include({ + init: function(parent, dataset, view_id, options) + { + this._super.apply(this, arguments) + this.ViewManager.on('switch_mode', this, function(view_type) + { + this._custom_column_get_element().toggle(view_type == 'list'); + }); + }, + load_list: function() + { + var self = this; + this._super.apply(this, arguments); + this.$custom_column = jQuery(instance.web.qweb.render( + 'ListView.CustomColumn', {widget: this} + )); + this._custom_column_get_element() + .empty() + .append(this.$custom_column); + this.$custom_column.filter('.oe_custom_column_activate') + .click(this.proxy(this._custom_column_activate)); + this.$custom_column.filter('.oe_custom_column_reset') + .click('reset', this.proxy(this._custom_column_diff)); + this.$custom_column.filter('.oe_custom_column_all') + .click('to_all', this.proxy(this._custom_column_diff)); + this.$custom_column.filter('.oe_custom_column_user') + .click('to_user', this.proxy(this._custom_column_diff)); + this.$custom_column.filter('[name="oe_custom_column_field"]') + .change(this.proxy(this._custom_column_add)); + this.$('th a.oe_custom_column_left') + .click('left', this.proxy(this._custom_column_diff)); + this.$('th a.oe_custom_column_right') + .click('right', this.proxy(this._custom_column_diff)); + this.$('th a.oe_custom_column_remove') + .click('remove', this.proxy(this._custom_column_diff)); + this.$custom_column.filter('[name="oe_custom_column_field"]') + .find('option') + .each(function(index, option) + { + jQuery(option).prop( + 'disabled', + _.any(self.columns, function(column) + { + return column.id == jQuery(option).val(); + }) + ); + }); + }, + _custom_column_get_element: function() + { + if(this.options.$pager) + { + return this.options.$pager + .siblings('.oe_view_manager_custom_column'); + } + else + { + return this.$('.oe_view_manager_custom_column'); + } + }, + _custom_column_activate: function() + { + if(this.options.custom_column_active) + { + return this._custom_column_deactivate(); + } + var deferred = new jQuery.when(), + self = this; + this.options.custom_column_active = true; + if(!this.options.custom_column_fields) + { + deferred = this._custom_column_get_desc(); + } + return deferred + .then(this.proxy(this.reload_view)) + .then(this.proxy(this.reload_content)); + }, + _custom_column_get_desc: function() + { + var self = this; + return new instance.web.Model('ir.ui.view') + .call( + 'custom_column_desc', [this.fields_view.view_id], + {context: instance.session.user_context} + ) + .then(function(desc) + { + self.options.custom_column_fields = desc.fields; + self.options.custom_column_type = desc.type; + }); + }, + _custom_column_get_fields: function() + { + var fields = this.options.custom_column_fields; + return _.chain(fields).keys().sortBy(function(field) + { + return fields[field].string; + }).value(); + }, + _custom_column_deactivate: function() + { + this.options.custom_column_active = false; + return this.reload_view().then(this.proxy(this.reload_content)); + }, + _custom_column_add: function(ev) + { + ev.data = 'add'; + return this._custom_column_diff(ev, jQuery(ev.target).val()); + }, + _custom_column_diff: function(ev, field) + { + ev.stopPropagation(); + return new instance.web.Model('ir.ui.view').call( + 'custom_column', + [ + this.fields_view.view_id, + { + type: this.options.custom_column_type, + operation: ev.data, + name: + field || jQuery(ev.target).parents('th').data('id'), + } + ] + ) + .then(this.proxy(this._custom_column_get_desc)) + .then(this.proxy(this.reload_view)) + .then(this.proxy(this.reload_content)); + }, + }); +}; diff --git a/web_listview_custom_column/static/src/xml/web_listview_custom_column.xml b/web_listview_custom_column/static/src/xml/web_listview_custom_column.xml new file mode 100644 index 00000000..4c59d9a9 --- /dev/null +++ b/web_listview_custom_column/static/src/xml/web_listview_custom_column.xml @@ -0,0 +1,51 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web_listview_custom_column/tests/__init__.py b/web_listview_custom_column/tests/__init__.py new file mode 100644 index 00000000..fc91babb --- /dev/null +++ b/web_listview_custom_column/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_web_listview_custom_column diff --git a/web_listview_custom_column/tests/test_web_listview_custom_column.py b/web_listview_custom_column/tests/test_web_listview_custom_column.py new file mode 100644 index 00000000..aebea70d --- /dev/null +++ b/web_listview_custom_column/tests/test_web_listview_custom_column.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp.tests.common import TransactionCase + + +class TestWebListviewCustomColumn(TransactionCase): + def test_web_listview_custom_column(self): + view = self.env.ref('base.module_tree') + view.custom_column({ + 'type': 'user', 'operation': 'add', 'name': 'display_name', + }) + self.assertIn( + 'display_name', + self.env['ir.module.module'] + .fields_view_get(view_id=view.id)['arch'] + ) + view.custom_column({ + 'type': 'user', 'operation': 'left', 'name': 'display_name', + }) + view.custom_column({ + 'type': 'user', 'operation': 'right', 'name': 'display_name', + }) + view.custom_column({ + 'type': 'user', 'operation': 'remove', 'name': 'display_name', + }) + self.assertNotIn( + 'display_name', + self.env['ir.module.module'] + .fields_view_get(view_id=view.id)['arch'] + ) + view.custom_column({ + 'type': 'user', 'operation': 'to_all', + }) + self.assertFalse( + self.env.ref(view._custom_column_xmlid({'type': 'user'}), False) + ) + self.assertTrue( + self.env.ref(view._custom_column_xmlid({'type': 'all'})) + ) + view.custom_column({ + 'type': 'all', 'operation': 'to_user', + }) + self.assertTrue( + self.env.ref(view._custom_column_xmlid({'type': 'all'})) + ) + self.assertTrue( + self.env.ref(view._custom_column_xmlid({'type': 'user'})) + ) + view.custom_column({ + 'type': 'user', 'operation': 'reset', + }) + self.assertFalse( + self.env.ref(view._custom_column_xmlid({'type': 'user'}), False) + ) diff --git a/web_listview_custom_column/views/templates.xml b/web_listview_custom_column/views/templates.xml new file mode 100644 index 00000000..a5b2a052 --- /dev/null +++ b/web_listview_custom_column/views/templates.xml @@ -0,0 +1,11 @@ + + + + + +