diff --git a/mass_sorting/__init__.py b/mass_sorting/__init__.py new file mode 100644 index 000000000..a0fdc10fe --- /dev/null +++ b/mass_sorting/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/mass_sorting/__openerp__.py b/mass_sorting/__openerp__.py new file mode 100644 index 000000000..81b9cc95f --- /dev/null +++ b/mass_sorting/__openerp__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +{ + 'name': 'Mass Sorting', + 'version': '1.0', + 'author': 'GRAP,Odoo Community Association (OCA)', + 'category': 'Tools', + 'website': 'http://www.grap.coop', + 'license': 'AGPL-3', + "description": """ +Mass Sorting +============ + +This module provides the functionality to sort an one2many fields in any model. + +Typically, you can sort sale order line on a sale order, using any fields. + +See screenshots in the description folder. + +This module is highly inspired by 'mass_editing' module. (by OCA and SerpentCS) + """, + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/mass_sort_config_view.xml', + 'views/mass_sort_wizard_view.xml', + 'views/action.xml', + 'views/menu.xml', + ], + 'images': [ + 'static/description/1_mass_sort_config.png', + 'static/description/2_button.png', + 'static/description/3_mass_sort_wizard.png', + 'static/description/3_mass_sort_wizard_custom.png', + 'static/description/4_before.png', + 'static/description/5_after.png', + ], +} diff --git a/mass_sorting/i18n/fr.po b/mass_sorting/i18n/fr.po new file mode 100644 index 000000000..7fb21ab21 --- /dev/null +++ b/mass_sorting/i18n/fr.po @@ -0,0 +1,179 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * mass_sorting +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 7.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-02-08 09:48+0000\n" +"PO-Revision-Date: 2016-02-08 09:48+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: mass_sorting +#: code:addons/mass_sorting/models/mass_sort_config.py:95 +#, python-format +msgid "%s (copy)" +msgstr "%s (copie)" + +#. module: mass_sorting +#: view:mass.sort.config:0 +msgid "Add sidebar button" +msgstr "Ajouter un bouton" + +#. module: mass_sorting +#: field:mass.sort.config,allow_custom_setting:0 +#: field:mass.sort.wizard,allow_custom_setting:0 +msgid "Allow Custom Setting" +msgstr "Autoriser un paramétrage personnalisé" + +#. module: mass_sorting +#: view:mass.sort.wizard:0 +msgid "Cancel" +msgstr "Annuler" + +#. module: mass_sorting +#: view:mass.sort.wizard:0 +msgid "Confirm" +msgstr "Confirmer" + +#. module: mass_sorting +#: field:mass.sort.wizard,description:0 +msgid "Description" +msgstr "Description" + +#. module: mass_sorting +#: view:mass.sort.config:0 +msgid "Display a button in the sidebar of related model to open a wizard" +msgstr "Afficher un bouton dans la barre de menu du modèle pour ouvrir un assistant" + +#. module: mass_sorting +#: field:mass.sort.config.line,field_id:0 +#: field:mass.sort.wizard.line,field_id:0 +msgid "Field" +msgstr "Champ" + +#. module: mass_sorting +#: field:mass.sort.config,one2many_field_id:0 +msgid "Field to Sort" +msgstr "Champ à trier" + +#. module: mass_sorting +#: help:mass.sort.config,allow_custom_setting:0 +msgid "If checked, any user could have the possibility to change fields, and use others." +msgstr "Si la case est cochée, chaque utilisateur aura la possibilité de changer les champs et d'en utiliser d'autres." + +#. module: mass_sorting +#: field:mass.sort.config.line,desc:0 +#: field:mass.sort.wizard.line,desc:0 +msgid "Inverse Order" +msgstr "Ordre inverse" + +#. module: mass_sorting +#: code:addons/mass_sorting/models/mass_sort_config.py:108 +#, python-format +msgid "Mass Sort (%s)" +msgstr "Tri en masse (%s)" + +#. module: mass_sorting +#: model:ir.actions.act_window,name:mass_sorting.action_mass_sort_config +#: model:ir.ui.menu,name:mass_sorting.menu_mass_sort_config +#: view:mass.sort.config:0 +#: view:mass.sort.wizard:0 +msgid "Mass Sorting" +msgstr "Tri en masse" + +#. module: mass_sorting +#: field:mass.sort.config,model_id:0 +msgid "Model" +msgstr "Modèle" + +#. module: mass_sorting +#: field:mass.sort.config,one2many_model:0 +#: field:mass.sort.wizard,one2many_model:0 +msgid "Model Name of the Field to Sort" +msgstr "Nom du modèle du champ à trier" + +#. module: mass_sorting +#: field:mass.sort.config,name:0 +msgid "Name" +msgstr "Nom" + +#. module: mass_sorting +#: constraint:mass.sort.wizard:0 +msgid "Please Select at least ona Sorting Criteria." +msgstr "Veuillez renseigner au moins un critère de tri." + +#. module: mass_sorting +#: model:mass.sort.config,name:mass_sorting.mass_sort_config +msgid "Recursive Demo Configuration" +msgstr "Configuration de démo (récursive)" + +#. module: mass_sorting +#: view:mass.sort.config:0 +msgid "Remove sidebar button" +msgstr "Retirer le bouton" + +#. module: mass_sorting +#: field:mass.sort.config.line,sequence:0 +#: field:mass.sort.wizard.line,sequence:0 +msgid "Sequence" +msgstr "Séquence" + +#. module: mass_sorting +#: field:mass.sort.config,ref_ir_act_window:0 +msgid "Sidebar Action" +msgstr "Action" + +#. module: mass_sorting +#: field:mass.sort.config,ref_ir_value:0 +msgid "Sidebar Button" +msgstr "Bouton" + +#. module: mass_sorting +#: field:mass.sort.config,line_ids:0 +#: view:mass.sort.wizard:0 +#: field:mass.sort.wizard,line_ids:0 +msgid "Sorting Criterias" +msgstr "Critères de tri" + +#. module: mass_sorting +#: constraint:mass.sort.config:0 +msgid "The selected Field to Sort doesn't belong to the selected model." +msgstr "Le champ sélectionné pour le tri n'appartient pas au modèle sélectionné." + +#. module: mass_sorting +#: constraint:mass.sort.config:0 +msgid "The selected Field to Sort doesn't not have 'sequence' field defined." +msgstr "Le champ sélectionné pour le tri n'a pas de champ séquence défini." + +#. module: mass_sorting +#: constraint:mass.sort.config.line:0 +msgid "The selected criteria must belong to the parent model." +msgstr "Le critère sélectionné doit appartenir au modèle parent." + +#. module: mass_sorting +#: field:mass.sort.config.line,config_id:0 +#: field:mass.sort.wizard.line,wizard_id:0 +msgid "Wizard" +msgstr "Assistant" + +#. module: mass_sorting +#: constraint:mass.sort.config:0 +msgid "You have to define field(s) in 'Sorting Criterias' if you uncheck 'Allow Custom Setting'." +msgstr "You have to define field(s) in 'Sorting Criterias' if you uncheck 'Allow Custom Setting'." + +#. module: mass_sorting +#: code:addons/mass_sorting/models/mass_sort_wizard.py:44 +#, python-format +msgid "You will sort the field '%(field)s' for %(qty)d %(model)s(s). \n" +"\n" +"The sorting will be done by %(field_list)s." +msgstr "Vous allez trier le champ '%(field)s' pour %(qty)d %(model)s(s). \n" +"\n" +"Le tri sera réalisé par %(field_list)s." diff --git a/mass_sorting/models/__init__.py b/mass_sorting/models/__init__.py new file mode 100644 index 000000000..4ce6c4b31 --- /dev/null +++ b/mass_sorting/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import mass_sort_config +from . import mass_sort_config_line +from . import mass_sort_wizard +from . import mass_sort_wizard_line diff --git a/mass_sorting/models/mass_sort_config.py b/mass_sorting/models/mass_sort_config.py new file mode 100644 index 000000000..47e4831d5 --- /dev/null +++ b/mass_sorting/models/mass_sort_config.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright (C): +# * 2012-Today Serpent Consulting Services () +# * 2016-Today GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.osv import fields +from openerp.osv.orm import Model +from openerp.tools.translate import _ + + +class MassSortConfig(Model): + _name = 'mass.sort.config' + + # Column Section + _columns = { + 'name': fields.char( + string='Name', translate=True, required=True), + 'model_id': fields.many2one( + 'ir.model', string='Model', required=True), + 'allow_custom_setting': fields.boolean( + string='Allow Custom Setting', help="If checked, any user could" + " have the possibility to change fields, and use others."), + 'one2many_field_id': fields.many2one( + 'ir.model.fields', string='Field to Sort', required=True, + domain="[('model_id', '=', model_id)," + "('ttype', '=', 'one2many')]"), + 'one2many_model': fields.related( + 'one2many_field_id', 'relation', type='char', + string='Model Name of the Field to Sort', readonly=True), + 'ref_ir_act_window': fields.many2one( + 'ir.actions.act_window', 'Sidebar Action', readonly=True), + 'ref_ir_value': fields.many2one( + 'ir.values', 'Sidebar Button', readonly=True), + 'line_ids': fields.one2many( + 'mass.sort.config.line', 'config_id', 'Sorting Criterias'), + } + + _defaults = { + 'allow_custom_setting': True, + } + + # Constraint Section + def _check_model_sequence(self, cr, uid, ids, context=None): + field_obj = self.pool['ir.model.fields'] + for config in self.browse(cr, uid, ids, context=context): + if len(field_obj.search(cr, uid, [ + ('model', '=', config.one2many_field_id.relation), + ('name', '=', 'sequence')], context=context)) != 1: + return False + return True + + def _check_model_field(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if config.model_id.id != config.one2many_field_id.model_id.id: + return False + return True + + def _check_line_ids(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if not config.allow_custom_setting and len(config.line_ids) == 0: + return False + return True + + _constraints = [ + (_check_model_sequence, "The selected Field to Sort doesn't not have" + " 'sequence' field defined.", ['one2many_field_id']), + (_check_model_field, "The selected Field to Sort doesn't belong to the" + " selected model.", ['model_id', 'one2many_field_id']), + (_check_line_ids, "You have to define field(s) in 'Sorting Criterias'" + " if you uncheck 'Allow Custom Setting'.", + ['line_ids', 'allow_custom_setting']), + ] + + # View Section + def on_change_one2many_field_id( + self, cr, uid, ids, one2many_field_id, context=None): + field_obj = self.pool['ir.model.fields'] + if not one2many_field_id: + return {'value': {'one2many_model': False}} + field = field_obj.browse(cr, uid, one2many_field_id, context=context) + return {'value': {'one2many_model': field.relation}} + + # Overload Section + def unlink(self, cr, uid, ids, context=None): + self.unlink_action(cr, uid, ids, context=context) + return super(MassSortConfig, self).unlink( + cr, uid, ids, context=context) + + def copy(self, cr, uid, id, value=None, context=None): + value = value or {} + config = self.browse(cr, uid, id, context=context) + value.update({ + 'name': _('%s (copy)') % config.name, + 'ref_ir_act_window': False, + 'ref_ir_value': False}) + return super(MassSortConfig, self).copy( + cr, uid, id, value, context=context) + + # Custom Section + def create_action(self, cr, uid, ids, context=None): + vals = {} + action_obj = self.pool['ir.actions.act_window'] + values_obj = self.pool['ir.values'] + for config in self.browse(cr, uid, ids, context=context): + src_obj = config.model_id.model + button_name = _('Mass Sort (%s)') % config.name + vals['ref_ir_act_window'] = action_obj.create(cr, uid, { + 'name': button_name, + 'type': 'ir.actions.act_window', + 'res_model': 'mass.sort.wizard', + 'src_model': src_obj, + 'view_type': 'form', + 'context': "{'mass_sort_config_id' : %d}" % (config.id), + 'view_mode': 'form,tree', + 'target': 'new', + 'auto_refresh': 1, + }, context) + vals['ref_ir_value'] = values_obj.create(cr, uid, { + 'name': button_name, + 'model': src_obj, + 'key2': 'client_action_multi', + 'value': ( + "ir.actions.act_window,%s" % vals['ref_ir_act_window']), + 'object': True, + }, context) + self.write(cr, uid, ids, { + 'ref_ir_act_window': vals.get('ref_ir_act_window', False), + 'ref_ir_value': vals.get('ref_ir_value', False), + }, context) + return True + + def unlink_action(self, cr, uid, ids, context=None): + action_obj = self.pool['ir.actions.act_window'] + values_obj = self.pool['ir.values'] + for config in self.browse(cr, uid, ids, context=context): + if config.ref_ir_act_window: + action_obj.unlink( + cr, uid, config.ref_ir_act_window.id, context=context) + if config.ref_ir_value: + values_obj.unlink( + cr, uid, config.ref_ir_value.id, context=context) + return True diff --git a/mass_sorting/models/mass_sort_config_line.py b/mass_sorting/models/mass_sort_config_line.py new file mode 100644 index 000000000..9a135605f --- /dev/null +++ b/mass_sorting/models/mass_sort_config_line.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.osv import fields +from openerp.osv.orm import Model + + +class MassSortConfigLine(Model): + _name = 'mass.sort.config.line' + _order = 'config_id, sequence, id' + + _columns = { + 'sequence': fields.integer('Sequence', required=True), + 'config_id': fields.many2one( + 'mass.sort.config', string='Wizard'), + 'field_id': fields.many2one( + 'ir.model.fields', string='Field', required=True, domain="[" + "('model', '=', parent.one2many_model)]"), + 'desc': fields.boolean('Inverse Order'), + } + + _defaults = { + 'sequence': 1, + } + + # Constraint Section + def _check_field_id(self, cr, uid, ids, context=None): + for line in self.browse(cr, uid, ids, context=context): + if line.field_id.model != line.config_id.one2many_model: + return False + return True + + _constraints = [ + (_check_field_id, "The selected criteria must belong to the parent" + " model.", ['config_id', 'field_id']), + ] diff --git a/mass_sorting/models/mass_sort_wizard.py b/mass_sorting/models/mass_sort_wizard.py new file mode 100644 index 000000000..c707babcf --- /dev/null +++ b/mass_sorting/models/mass_sort_wizard.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.osv import fields +from openerp.osv.orm import TransientModel +from openerp.tools.translate import _ + + +class MassSortWizard(TransientModel): + _name = 'mass.sort.wizard' + + _columns = { + 'description': fields.text( + string='Description', readonly=True), + 'allow_custom_setting': fields.boolean( + string='Allow Custom Setting', readonly=True), + 'one2many_model': fields.char( + string='Model Name of the Field to Sort', readonly=True), + 'line_ids': fields.one2many( + 'mass.sort.wizard.line', 'wizard_id', 'Sorting Criterias'), + } + + # Constraint Section + def _check_line_ids(self, cr, uid, ids, context=None): + for wizard in self.browse(cr, uid, ids, context=context): + if not len(wizard.line_ids): + return False + return True + + _constraints = [( + _check_line_ids, "Please Select at least ona Sorting Criteria.", + ['line_ids']), + ] + + # Default Section + def _default_description(self, cr, uid, context=None): + config_obj = self.pool['mass.sort.config'] + config = config_obj.browse( + cr, uid, context.get('mass_sort_config_id'), context=context) + + return _( + "You will sort the field '%(field)s' for %(qty)d %(model)s(s)" + ". \n\nThe sorting will be done by %(field_list)s.") % ({ + 'field': config.one2many_field_id.field_description, + 'qty': len(context.get('active_ids', False)), + 'model': config.model_id.name, + 'field_list': ', '.join( + [x.field_id.field_description for x in config.line_ids]) + }) + + def _default_allow_custom_setting(self, cr, uid, context=None): + config_obj = self.pool['mass.sort.config'] + return config_obj.browse( + cr, uid, context.get('mass_sort_config_id'), context=context)\ + .allow_custom_setting + + def _default_one2many_model(self, cr, uid, context=None): + config_obj = self.pool['mass.sort.config'] + return config_obj.browse( + cr, uid, context.get('mass_sort_config_id'), context=context)\ + .one2many_model + + def _default_line_ids(self, cr, uid, context=None): + config_obj = self.pool['mass.sort.config'] + res = [] + config = config_obj.browse( + cr, uid, context.get('mass_sort_config_id'), context=context) + for line in config.line_ids: + res.append((0, 0, { + 'sequence': line.sequence, + 'field_id': line.field_id.id, + 'desc': line.desc})) + return res + + _defaults = { + 'description': _default_description, + 'allow_custom_setting': _default_allow_custom_setting, + 'one2many_model': _default_one2many_model, + 'line_ids': _default_line_ids, + } + + # Action Section + def button_apply(self, cr, uid, ids, context=None): + config_obj = self.pool['mass.sort.config'] + model_obj = self.pool[context.get('active_model')] + active_ids = context.get('active_ids') + config = config_obj.browse( + cr, uid, context.get('mass_sort_config_id'), context=context) + wizard = self.browse(cr, uid, ids[0], context=context) + + one2many_obj = self.pool[config.one2many_field_id.relation] + parent_field = config.one2many_field_id.relation_field + + order_list = [] + for line in wizard.line_ids: + order_list.append( + line.desc and + '%s desc' % line.field_id.name or line.field_id.name) + order = ', '.join(order_list) + + for item in model_obj.browse(cr, uid, active_ids, context=context): + # DB Query sort by the correct order + line_ids = one2many_obj.search( + cr, uid, [(parent_field, '=', item.id)], order=order, + context=context) + # Write new sequence to sort lines + sequence = 1 + for id in line_ids: + one2many_obj.write( + cr, uid, [id], {'sequence': sequence}, context=context) + sequence += 1 + return {'type': 'ir.actions.act_window_close'} diff --git a/mass_sorting/models/mass_sort_wizard_line.py b/mass_sorting/models/mass_sort_wizard_line.py new file mode 100644 index 000000000..28d4b571a --- /dev/null +++ b/mass_sorting/models/mass_sort_wizard_line.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.osv import fields +from openerp.osv.orm import TransientModel + + +class TransientModelLine(TransientModel): + _name = 'mass.sort.wizard.line' + + _columns = { + 'sequence': fields.integer('Sequence', required=True), + 'wizard_id': fields.many2one( + 'mass.sort.wizard', string='Wizard'), + 'field_id': fields.many2one( + 'ir.model.fields', string='Field', required=True, domain="[" + "('model', '=', parent.one2many_model)]"), + 'desc': fields.boolean('Inverse Order'), + } + + _defaults = { + 'sequence': 1, + } diff --git a/mass_sorting/security/ir.model.access.csv b/mass_sorting/security/ir.model.access.csv new file mode 100755 index 000000000..8b9b4fb0c --- /dev/null +++ b/mass_sorting/security/ir.model.access.csv @@ -0,0 +1,6 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_mass_sort_config_user","Mass Sorting Configurations - User","model_mass_sort_config","base.group_user",1,0,0,0 +"access_mass_sort_config_manager","Mass Sorting Configurations - Manager","model_mass_sort_config","base.group_system",1,1,1,1 + +"access_mass_sort_config_line_user","Mass Sorting Configuration Lines - User","model_mass_sort_config_line","base.group_user",1,0,0,0 +"access_mass_sort_config_line_manager","Mass Sorting Configuration Lines - Manager","model_mass_sort_config_line","base.group_system",1,1,1,1 diff --git a/mass_sorting/static/description/1_mass_sort_config.png b/mass_sorting/static/description/1_mass_sort_config.png new file mode 100644 index 000000000..417a55018 Binary files /dev/null and b/mass_sorting/static/description/1_mass_sort_config.png differ diff --git a/mass_sorting/static/description/2_button.png b/mass_sorting/static/description/2_button.png new file mode 100644 index 000000000..6fd1fe0f4 Binary files /dev/null and b/mass_sorting/static/description/2_button.png differ diff --git a/mass_sorting/static/description/3_mass_sort_wizard.png b/mass_sorting/static/description/3_mass_sort_wizard.png new file mode 100644 index 000000000..bcc36f685 Binary files /dev/null and b/mass_sorting/static/description/3_mass_sort_wizard.png differ diff --git a/mass_sorting/static/description/3_mass_sort_wizard_custom.png b/mass_sorting/static/description/3_mass_sort_wizard_custom.png new file mode 100644 index 000000000..5ed7fcc50 Binary files /dev/null and b/mass_sorting/static/description/3_mass_sort_wizard_custom.png differ diff --git a/mass_sorting/static/description/4_before.png b/mass_sorting/static/description/4_before.png new file mode 100644 index 000000000..01d94b30f Binary files /dev/null and b/mass_sorting/static/description/4_before.png differ diff --git a/mass_sorting/static/description/5_after.png b/mass_sorting/static/description/5_after.png new file mode 100644 index 000000000..faf52af83 Binary files /dev/null and b/mass_sorting/static/description/5_after.png differ diff --git a/mass_sorting/views/action.xml b/mass_sorting/views/action.xml new file mode 100644 index 000000000..780d0d77d --- /dev/null +++ b/mass_sorting/views/action.xml @@ -0,0 +1,17 @@ + + + + + + + Mass Sorting + mass.sort.config + form + tree,form + + + diff --git a/mass_sorting/views/mass_sort_config_view.xml b/mass_sorting/views/mass_sort_config_view.xml new file mode 100644 index 000000000..0ee0868e2 --- /dev/null +++ b/mass_sorting/views/mass_sort_config_view.xml @@ -0,0 +1,56 @@ + + + + + + + mass.sort.config + +
+ +
+
+

+
+ + + + + +