From 393b512c43491d089a6bc558f00be142ee63997a Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 30 Aug 2017 16:49:34 +0200 Subject: [PATCH] Create module account_tag_category --- account_tag_category/README.rst | 62 +++++++++ account_tag_category/__init__.py | 2 + account_tag_category/__manifest__.py | 23 ++++ account_tag_category/models/__init__.py | 1 + account_tag_category/models/account.py | 127 ++++++++++++++++++ .../security/ir.model.access.csv | 3 + account_tag_category/tests/__init__.py | 0 .../tests/test_account_tag_category.py | 110 +++++++++++++++ account_tag_category/views/account.xml | 58 ++++++++ account_tag_category/wizard/__init__.py | 1 + account_tag_category/wizard/update_tags.xml | 31 +++++ .../wizard/update_tags_wizard.py | 35 +++++ 12 files changed, 453 insertions(+) create mode 100644 account_tag_category/README.rst create mode 100644 account_tag_category/__init__.py create mode 100644 account_tag_category/__manifest__.py create mode 100644 account_tag_category/models/__init__.py create mode 100644 account_tag_category/models/account.py create mode 100644 account_tag_category/security/ir.model.access.csv create mode 100644 account_tag_category/tests/__init__.py create mode 100644 account_tag_category/tests/test_account_tag_category.py create mode 100644 account_tag_category/views/account.xml create mode 100644 account_tag_category/wizard/__init__.py create mode 100644 account_tag_category/wizard/update_tags.xml create mode 100644 account_tag_category/wizard/update_tags_wizard.py diff --git a/account_tag_category/README.rst b/account_tag_category/README.rst new file mode 100644 index 000000000..d3997136e --- /dev/null +++ b/account_tag_category/README.rst @@ -0,0 +1,62 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +==================== +Account Tag Category +==================== + +This module extends the functionality of account module to support the grouping +of accounts tags by category and to allow you to set a category as optional +or required. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/82/10.0 + +Known issues / Roadmap +====================== + +* Color picker should be improved +* Taxes applicable tags should be supported + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Akim Juillerat + +Do not contact contributors directly about support or help with 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/account_tag_category/__init__.py b/account_tag_category/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/account_tag_category/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/account_tag_category/__manifest__.py b/account_tag_category/__manifest__.py new file mode 100644 index 000000000..0254a924d --- /dev/null +++ b/account_tag_category/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Account Tag Category", + "summary": "Group account tags by categories", + "version": "10.0.1.0.0", + "category": "Accounting & Finance", + "website": "https://www.camptocamp.com/", + "author": "Camptocamp SA,Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "account", + "web_m2x_options" + ], + "data": [ + "security/ir.model.access.csv", + "wizard/update_tags.xml", + "views/account.xml", + ], + "application": False, + "installable": True, +} diff --git a/account_tag_category/models/__init__.py b/account_tag_category/models/__init__.py new file mode 100644 index 000000000..2b77ae28f --- /dev/null +++ b/account_tag_category/models/__init__.py @@ -0,0 +1 @@ +from . import account diff --git a/account_tag_category/models/account.py b/account_tag_category/models/account.py new file mode 100644 index 000000000..831ddbb61 --- /dev/null +++ b/account_tag_category/models/account.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class AccountAccountTag(models.Model): + + _inherit = 'account.account.tag' + + tag_category_id = fields.Many2one('account.account.tag.category', + 'tag_ids', ondelete='set null') + + category_color = fields.Integer(related='tag_category_id.color') + + @api.multi + def read(self, fields=None, load='_classic_read'): + # This function is used to read from category_color field instead of + # color without having to change anything on the client side. + if 'color' in fields: + fields.append('category_color') + if 'tag_category_id' not in fields: + fields.append('tag_category_id') + result = super(AccountAccountTag, self).read(fields, load) + for rec in result: + if 'category_color' in rec and rec.get('tag_category_id', False): + color = rec.pop('category_color') + rec['color'] = color + return result + + +class AccountAccountTagCategory(models.Model): + + _name = 'account.account.tag.category' + + _description = 'Account Tag Category' + + name = fields.Char(required=True) + + color = fields.Integer('Color Index', compute='_compute_color_index', + store=True) + + color_picker = fields.Selection([('0', 'Grey'), + ('1', 'Green'), + ('2', 'Yellow'), + ('3', 'Orange'), + ('4', 'Red'), + ('5', 'Purple'), + ('6', 'Blue'), + ('7', 'Cyan'), + ('8', 'Aquamarine'), + ('9', 'Pink')], string='Tags Color', + required=True, default='0') + + enforce_policy = fields.Selection( + [('required', 'Required'), ('optional', 'Optional')], + required=True, default='optional', + help='If required, this option enforces the use of a tag from this ' + 'category. If optional, the user is not required to use a tag ' + 'from this category.') + + tag_ids = fields.One2many('account.account.tag', 'tag_category_id', + string='Tags',) + + # TODO support applicability for taxes + + applicability = fields.Selection([ + ('accounts', 'Accounts'), + # ('taxes', 'Taxes'), + ], default='accounts', required=True) + + @api.depends('color_picker') + def _compute_color_index(self): + for category in self: + category.color = int(category.color_picker) + + @api.constrains('tag_ids', 'applicability') + def _check_tags_applicability(self): + self.ensure_one() + for tag in self.tag_ids: + if tag.applicability != self.applicability: + raise ValidationError(_('Selected tag must be applicable on ' + 'the same model as this category (%s)' + ) % self.applicability) + + +class AccountAccount(models.Model): + + _inherit = 'account.account' + + @api.constrains('tag_ids') + def _check_tags_categories(self): + self.ensure_one() + self._check_required_categories() + self._check_unique_tag_per_category() + + def _check_unique_tag_per_category(self): + used_categories_ids = [] + for tag in self.tag_ids: + if not tag.tag_category_id: + continue + tag_id = tag.tag_category_id.id + if tag_id in used_categories_ids: + raise ValidationError(_('There is more than one tag from the ' + 'same category which is used')) + else: + used_categories_ids.append(tag_id) + + def _check_required_categories(self): + required_categories = self.env['account.account.tag.category'].search( + [('applicability', '=', 'accounts'), + ('enforce_policy', '=', 'required')]) + errors = [] + for category in required_categories: + found = False + for tag in self.tag_ids: + if tag in category.tag_ids: + found = True + if not found: + errors.append(category.name) + if errors: + text_error = '\n'.join(errors) + raise ValidationError(_('Following tag categories are set as ' + 'required, but there is no tag from these ' + 'categories : \n %s') % text_error) diff --git a/account_tag_category/security/ir.model.access.csv b/account_tag_category/security/ir.model.access.csv new file mode 100644 index 000000000..05ffa7887 --- /dev/null +++ b/account_tag_category/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_account_account_tag_category_user,access_account_account_tag_category_user,model_account_account_tag_category,account.group_account_user,1,0,0,0 +access_account_account_tag_category_manager,access_account_account_tag_category_manager,model_account_account_tag_category,account.group_account_manager,1,1,1,1 \ No newline at end of file diff --git a/account_tag_category/tests/__init__.py b/account_tag_category/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/account_tag_category/tests/test_account_tag_category.py b/account_tag_category/tests/test_account_tag_category.py new file mode 100644 index 000000000..09b6f028b --- /dev/null +++ b/account_tag_category/tests/test_account_tag_category.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + + +class TestAccountTagCategory(TransactionCase): + + def setUp(self): + super(TestAccountTagCategory, self).setUp() + + # Create a few tags + self.tag_model = self.env['account.account.tag'].with_context( + default_applicability='accounts') + category_model = self.env[ + 'account.account.tag.category'].with_context( + default_applicability='accounts') + + tags_to_create = ['123', '456', '789', 'ABC', 'DEF', 'GHI'] + + for tag_name in tags_to_create: + self.tag_model.create({ + 'name': tag_name, + }) + + self.letters_category = category_model.create({ + 'name': 'Letters', + 'enforce_policy': 'required', + 'color_picker': '1', + }) + self.numbers_category = category_model.create({ + 'name': 'Numbers', + 'enforce_policy': 'optional', + 'color_picker': '2', + }) + + update_wizard = self.env['account.tag.category.update.tags'] + + update_wizard.create({ + 'tag_category_id': self.letters_category.id, + 'tag_ids': [(6, False, self.tag_model.search( + ['|', '|', ('name', '=', 'ABC'), ('name', '=', 'DEF'), + ('name', '=', 'GHI')]).ids)], + }).save_tags_to_category() + + update_wizard.create({ + 'tag_category_id': self.numbers_category.id, + 'tag_ids': [(6, False, self.tag_model.search( + ['|', '|', ('name', '=', '123'), ('name', '=', '456'), + ('name', '=', '789')]).ids)], + }).save_tags_to_category() + + + def test_categories(self): + + self.assertEqual(self.letters_category.color, + int(self.letters_category.color_picker)) + + self.tag_model.invalidate_cache() + + self.assertEqual(self.tag_model.search( + [('name', '=', 'ABC')]).read(['color'])[0]['color'], + self.letters_category.color) + + self.assertEqual(len(self.letters_category.tag_ids), 3) + + # Missing required category + with self.assertRaises(ValidationError): + self.env['account.account'].create({ + 'name': "Dummy account", + 'code': "DUMMY", + 'user_type_id': self.env.ref( + 'account.data_account_type_equity').id, + }) + + with self.assertRaises(ValidationError): + self.env['account.account'].create({ + 'name': "Dummy account", + 'code': "DUMMY 2", + 'user_type_id': self.env.ref( + 'account.data_account_type_equity').id, + 'tag_ids': [(6, False, self.tag_model.search( + [('name', '=', '123')]).ids)] + }) + + # Two times same category + with self.assertRaises(ValidationError): + self.env['account.account'].create({ + 'name': "Dummy account", + 'code': "DUMMY 3", + 'user_type_id': self.env.ref( + 'account.data_account_type_equity').id, + 'tag_ids': [(6, False, + self.tag_model.search( + ['|', ('name', '=', 'ABC'), + ('name', '=', 'DEF')]).ids)] + }) + + self.env['account.account'].create({ + 'name': "Dummy account", + 'code': "DUMMY 4", + 'user_type_id': self.env.ref( + 'account.data_account_type_equity').id, + 'tag_ids': [(6, False, + self.tag_model.search( + ['|', ('name', '=', '123'), + ('name', '=', 'DEF')]).ids)] + }) diff --git a/account_tag_category/views/account.xml b/account_tag_category/views/account.xml new file mode 100644 index 000000000..9ccd743b9 --- /dev/null +++ b/account_tag_category/views/account.xml @@ -0,0 +1,58 @@ + + + + + account.account.tag.category.form + account.account.tag.category + +
+
+
+ + + + + + + + + +
+
+
+ + account.account.tag.category.tree + account.account.tag.category + + + + + + + + + + + Account Tag Categories + account.account.tag.category + tree,form + {'default_applicability': 'accounts'} + + + + + + account.account.form.inherit + account.account + + + + {'no_color_picker': True} + + + +
diff --git a/account_tag_category/wizard/__init__.py b/account_tag_category/wizard/__init__.py new file mode 100644 index 000000000..1dd346ecf --- /dev/null +++ b/account_tag_category/wizard/__init__.py @@ -0,0 +1 @@ +from . import update_tags_wizard diff --git a/account_tag_category/wizard/update_tags.xml b/account_tag_category/wizard/update_tags.xml new file mode 100644 index 000000000..38b1795d7 --- /dev/null +++ b/account_tag_category/wizard/update_tags.xml @@ -0,0 +1,31 @@ + + + + + Update Tags + account.tag.category.update.tags + form + form + + {'default_tag_category_id': active_id} + new + + + + account.tag.category.update.tags.form + account.tag.category.update.tags + +
+ + + + + +
+
+
+ +
diff --git a/account_tag_category/wizard/update_tags_wizard.py b/account_tag_category/wizard/update_tags_wizard.py new file mode 100644 index 000000000..acea01b68 --- /dev/null +++ b/account_tag_category/wizard/update_tags_wizard.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api + + +class AccountTagCategoryUpdateTags(models.TransientModel): + + _name = 'account.tag.category.update.tags' + + _description = 'Update account tags on account tag category' + + tag_ids = fields.Many2many('account.account.tag', + domain=[('applicability', '=', 'accounts')]) + # TODO support applicability for taxes in domain + + tag_category_id = fields.Many2one('account.account.tag.category') + + @api.model + def default_get(self, fields): + res = super(AccountTagCategoryUpdateTags, self).default_get(fields) + if 'tag_ids' in fields and not res.get('tag_ids'): + res['tag_ids'] = self.env['account.account.tag'].search( + [('tag_category_id', '=', res.get('tag_category_id'))]).ids + return res + + @api.multi + def save_tags_to_category(self): + + self.tag_category_id.write({ + 'tag_ids': [(6, False, self.tag_ids.ids)] + }) + + return