From 3df71c126706b2358123485e969e2eca82ecf760 Mon Sep 17 00:00:00 2001 From: manu Date: Thu, 2 Jan 2025 09:14:52 +0100 Subject: [PATCH] [MIG+IMP]product_category_code_unique: restriction options --- product_category_code_unique/README.rst | 27 +- product_category_code_unique/__manifest__.py | 4 +- .../models/__init__.py | 1 + .../models/product_category.py | 59 ++++- .../models/res_config_settings.py | 37 +++ .../readme/CONFIGURE.rst | 5 + .../readme/CONTRIBUTORS.rst | 1 + product_category_code_unique/readme/USAGE.rst | 3 + .../static/description/index.html | 44 +++- .../tests/test_code_unique.py | 247 ++++++++++++++++-- .../views/res_config_settings_views.xml | 39 +++ 11 files changed, 423 insertions(+), 44 deletions(-) create mode 100644 product_category_code_unique/models/res_config_settings.py create mode 100644 product_category_code_unique/readme/CONFIGURE.rst create mode 100644 product_category_code_unique/readme/USAGE.rst create mode 100644 product_category_code_unique/views/res_config_settings_views.xml diff --git a/product_category_code_unique/README.rst b/product_category_code_unique/README.rst index fd244ec03da..3da59511af5 100644 --- a/product_category_code_unique/README.rst +++ b/product_category_code_unique/README.rst @@ -17,13 +17,13 @@ Product Category Code Unique :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github - :target: https://github.com/OCA/product-attribute/tree/15.0/product_category_code_unique + :target: https://github.com/OCA/product-attribute/tree/16.0/product_category_code_unique :alt: OCA/product-attribute .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/product-attribute-15-0/product-attribute-15-0-product_category_code_unique + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_category_code_unique :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=15.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=16.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -36,13 +36,29 @@ If no code is provided, a code is generated by a sequence. .. contents:: :local: +Configuration +============= + +There are 3 restriction options, which can be set from Settings > General Settings > Category Code Unique, under the "Product Category Code" section. + +#. **Whole system**. Product category codes must be unique within the whole system. +#. **Parent-Children**. Product category code for a given product category must be unique considering its parent category and its child categories. +#. **Category Hierarchy**. Product category codes must be unique within the same categories hierarchy. + +Usage +===== + +All product category codes are checked every time the category restriction is changed. An error message is shown when it is not possible to change the restriction because some codes are duplicated depending on the restriction selected. + +If no code is manually set in a product category, one is automatically assigned following a sequence. + 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 to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -59,6 +75,7 @@ Contributors * Denis Roussel * Rolando Duarte +* Manuel Regidor Maintainers ~~~~~~~~~~~ @@ -84,6 +101,6 @@ Current `maintainers `__: |maintainer-rousseldenis| |maintainer-luisg123v| -This module is part of the `OCA/product-attribute `_ project on GitHub. +This module is part of the `OCA/product-attribute `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_category_code_unique/__manifest__.py b/product_category_code_unique/__manifest__.py index ecb5d8b24f5..e709cf89da8 100644 --- a/product_category_code_unique/__manifest__.py +++ b/product_category_code_unique/__manifest__.py @@ -5,16 +5,16 @@ "name": "Product Category Code Unique", "summary": """ Allows to set product category code field as unique""", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["rousseldenis", "luisg123v"], "website": "https://github.com/OCA/product-attribute", "depends": [ - "product", "product_category_code", ], "data": [ + "views/res_config_settings_views.xml", "data/product_category_sequence.xml", ], "pre_init_hook": "pre_init_hook", diff --git a/product_category_code_unique/models/__init__.py b/product_category_code_unique/models/__init__.py index 53553f3f2de..29a8e401e93 100644 --- a/product_category_code_unique/models/__init__.py +++ b/product_category_code_unique/models/__init__.py @@ -1 +1,2 @@ from . import product_category +from . import res_config_settings diff --git a/product_category_code_unique/models/product_category.py b/product_category_code_unique/models/product_category.py index 52b8ca54b13..831d4769bc4 100644 --- a/product_category_code_unique/models/product_category.py +++ b/product_category_code_unique/models/product_category.py @@ -1,16 +1,67 @@ # Copyright 2021 ACSONE SA/NV +# Copyright 2024 Manuel Regidor # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import _, api, models +from odoo.exceptions import ValidationError class ProductCategory(models.Model): _inherit = "product.category" - _sql_constraints = [ - ("uniq_code", "unique(code)", "The category code must be unique!"), - ] + def _get_parents(self): + self.ensure_one() + parent = self.parent_id + parents = self.env["product.category"] + while parent: + parents += parent + parent = parent.parent_id + return parents + + def _get_children(self): + self.ensure_one() + return self.search([("parent_id", "child_of", self.id)]) + + def _get_hierarchy_cats(self): + self.ensure_one() + return self + self._get_parents() + self._get_children() + + def _code_restriction(self, restriction=False): + restriction = restriction or self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + if restriction: + for cat in self: + domain = [] + within = "" + if restriction == "system": + domain = [("code", "=", cat.code)] + within = _("the system") + elif restriction == "direct": + to_check = cat.parent_id + cat.child_id + domain = [ + "|", + ("id", "=", cat.id), + "&", + ("code", "=", cat.code), + ("id", "in", to_check.ids), + ] + within = _("parent and children") + elif restriction == "hierarchy": + to_check = cat._get_hierarchy_cats() + domain = [("code", "=", cat.code), ("id", "in", to_check.ids)] + within = _("category hierarchy") + if self.search_count(domain) > 1: + raise ValidationError( + _("The category code must be unique within {within}!").format( + within=within + ) + ) + + @api.constrains("code") + def _check_code(self): + self._code_restriction() @api.model def _get_next_code(self): diff --git a/product_category_code_unique/models/res_config_settings.py b/product_category_code_unique/models/res_config_settings.py new file mode 100644 index 00000000000..f682e13773c --- /dev/null +++ b/product_category_code_unique/models/res_config_settings.py @@ -0,0 +1,37 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2024 Manuel Regidor +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + + _inherit = "res.config.settings" + + product_cat_code_unique_restriction = fields.Selection( + [ + ("system", "Whole System"), + ("direct", "Parent-Children"), + ("hierarchy", "Category Hierarchy"), + ], + string="Product Category Code Uniqueness Restriction", + config_parameter="product_code_unique.product_code_unique_restriction", + help=( + "If no option is selected, no restriction applies.\n" + "If you select:\n" + "- Whole Sytem: Product Category Codes cannot be duplicated within the " + " whole system\n" + "- Parent-Children: Parent and Children Product Category Codes of the" + " same category cannot be duplicated.\n" + "- Category Hierarchy: Product Category Codes cannot be duplicated " + "within the same category hierarchy.\n" + ), + ) + + @api.constrains("product_cat_code_unique_restriction") + def _check_product_cat_code_unique_restriction(self): + for sel in self.filtered("product_cat_code_unique_restriction"): + self.env["product.category"].search([])._code_restriction( + sel.product_cat_code_unique_restriction + ) diff --git a/product_category_code_unique/readme/CONFIGURE.rst b/product_category_code_unique/readme/CONFIGURE.rst new file mode 100644 index 00000000000..e726654c983 --- /dev/null +++ b/product_category_code_unique/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +There are 3 restriction options, which can be set from Settings > General Settings > Category Code Unique, under the "Product Category Code" section. + +#. **Whole system**. Product category codes must be unique within the whole system. +#. **Parent-Children**. Product category code for a given product category must be unique considering its parent category and its child categories. +#. **Category Hierarchy**. Product category codes must be unique within the same categories hierarchy. diff --git a/product_category_code_unique/readme/CONTRIBUTORS.rst b/product_category_code_unique/readme/CONTRIBUTORS.rst index fc9156c336b..531c4ede032 100644 --- a/product_category_code_unique/readme/CONTRIBUTORS.rst +++ b/product_category_code_unique/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Denis Roussel * Rolando Duarte +* Manuel Regidor diff --git a/product_category_code_unique/readme/USAGE.rst b/product_category_code_unique/readme/USAGE.rst new file mode 100644 index 00000000000..9b801d32ab3 --- /dev/null +++ b/product_category_code_unique/readme/USAGE.rst @@ -0,0 +1,3 @@ +All product category codes are checked every time the category restriction is changed. An error message is shown when it is not possible to change the restriction because some codes are duplicated depending on the restriction selected. + +If no code is manually set in a product category, one is automatically assigned following a sequence. diff --git a/product_category_code_unique/static/description/index.html b/product_category_code_unique/static/description/index.html index 01e11657b3c..d8a02c3c848 100644 --- a/product_category_code_unique/static/description/index.html +++ b/product_category_code_unique/static/description/index.html @@ -1,4 +1,3 @@ - @@ -369,46 +368,63 @@

Product Category Code Unique

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:b7c0200956cf578607572df0b8b215a886d91de7f488a5b1da447cfea042a947 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

This module allows to restrict unique codes on product categories. If no code is provided, a code is generated by a sequence.

Table of contents

+
+

Configuration

+

There are 3 restriction options, which can be set from Settings > General Settings > Category Code Unique, under the “Product Category Code” section.

+
    +
  1. Whole system. Product category codes must be unique within the whole system.
  2. +
  3. Parent-Children. Product category code for a given product category must be unique considering its parent category and its child categories.
  4. +
  5. Category Hierarchy. Product category codes must be unique within the same categories hierarchy.
  6. +
+
+
+

Usage

+

All product category codes are checked every time the category restriction is changed. An error message is shown when it is not possible to change the restriction because some codes are duplicated depending on the restriction selected.

+

If no code is manually set in a product category, one is automatically assigned following a sequence.

+
-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed -feedback.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose @@ -416,7 +432,7 @@

Maintainers

promote its widespread use.

Current maintainers:

rousseldenis luisg123v

-

This module is part of the OCA/product-attribute project on GitHub.

+

This module is part of the OCA/product-attribute project on GitHub.

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

diff --git a/product_category_code_unique/tests/test_code_unique.py b/product_category_code_unique/tests/test_code_unique.py index 91861fb7f77..c3aa9cf6fca 100644 --- a/product_category_code_unique/tests/test_code_unique.py +++ b/product_category_code_unique/tests/test_code_unique.py @@ -1,8 +1,9 @@ # Copyright 2021 ACSONE SA/NV () +# Copyright 2024 Manuel Regidor # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from psycopg2 import IntegrityError import odoo.tests.common as common +from odoo.exceptions import ValidationError from odoo.tools import mute_logger @@ -10,29 +11,237 @@ class TestProductCode(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + # Catetories hierarchy + # A + # AA + # AB + # ABA + # ABAA + # ABAB + # ABB + # ABBA + # ABBB vals = { - "name": "Category Test", - "code": "TEST", + "name": "A", + "code": "A", + "child_id": [ + ( + 0, + 0, + { + "name": "AA", + "code": "AA", + }, + ), + ( + 0, + 0, + { + "name": "AB", + "code": "AB", + "child_id": [ + ( + 0, + 0, + { + "name": "ABA", + "code": "ABA", + "child_id": [ + (0, 0, {"name": "ABAA", "code": "ABAA"}), + (0, 0, {"name": "ABAB", "code": "ABAB"}), + ], + }, + ), + ( + 0, + 0, + { + "name": "ABB", + "code": "ABB", + "child_id": [ + (0, 0, {"name": "ABBA", "code": "ABBA"}), + (0, 0, {"name": "ABBB", "code": "ABBB"}), + ], + }, + ), + ], + }, + ), + ], } cls.category = cls.env["product.category"].create(vals) @mute_logger("odoo.sql_db") - def test_category_code_unique(self): - vals = { - "name": "Category Test duplicate", - "code": "TEST", - } - with self.assertRaises(IntegrityError), self.env.cr.savepoint(): - self.env["product.category"].create(vals) + def test_category_code_unique_no_restriction(self): + # Create + self.env["product.category"].create({"name": "Test", "code": "A"}) + categories = self.env["product.category"].search([("code", "=", "A")]) + self.assertTrue(len(categories) > 1) + # Write + self.env["product.category"].search([("code", "=", "AA")]).write({"code": "AB"}) + categories = self.env["product.category"].search([("code", "=", "AB")]) + self.assertTrue(len(categories) > 1) - vals.update({"code": "TEST1"}) - self.env["product.category"].create(vals) + @mute_logger("odoo.sql_db") + def test_category_code_unique_whole_system(self): + config = self.env["res.config.settings"].create({}) + config.write({"product_cat_code_unique_restriction": "system"}) + config.execute() + # Create + with self.assertRaises(ValidationError): + self.env["product.category"].create({"name": "Test", "code": "AB"}) + categories = self.env["product.category"].search([("code", "=", "AB")]) + self.assertTrue(len(categories) == 1) + with self.assertRaises(ValidationError): + self.env["product.category"].search([("code", "=", "AA")]).write( + {"code": "AB"} + ) + categories = self.env["product.category"].search([("code", "=", "AB")]) + self.assertTrue(len(categories) == 1) - vals = { - "name": "Category Auto", - } - self.category_2 = self.env["product.category"].create(vals) - self.assertIn( - "PC/", - self.category_2.code, + @mute_logger("odoo.sql_db") + def test_category_code_unique_parent_children(self): + config = self.env["res.config.settings"].create({}) + config.write({"product_cat_code_unique_restriction": "direct"}) + config.execute() + + # Create + cat = self.env["product.category"].search([("code", "=", "AB")]) + with self.assertRaises(ValidationError): + self.env["product.category"].create( + { + "name": "Test", + "code": cat.code, + "parent_id": cat.id, + } + ) + with self.assertRaises(ValidationError): + cat.write({"child_id": [(0, 0, {"name": "Test", "code": cat.code})]}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 1) + last_cat = self.env["product.category"].search([("code", "=", "ABBB")], limit=1) + + # Write + child_cat = self.env["product.category"].search( + [("parent_id", "=", cat.id)], limit=1 + ) + with self.assertRaises(ValidationError): + child_cat.write({"code": cat.code}) + with self.assertRaises(ValidationError): + child_cat.child_id[0].write({"code": child_cat.code}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 1) + last_cat.write({"code": cat.code}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 2) + + @mute_logger("odoo.sql_db") + def test_category_code_unique_hierarchy(self): + config = self.env["res.config.settings"].create({}) + config.write({"product_cat_code_unique_restriction": "hierarchy"}) + config.execute() + + # Create + cat = self.env["product.category"].search([("code", "=", "AB")]) + last_cat = self.env["product.category"].search([("code", "=", "ABBB")], limit=1) + with self.assertRaises(ValidationError): + self.env["product.category"].create( + { + "name": "Test", + "code": cat.code, + "parent_id": last_cat.id, + } + ) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 1) + + # Write + with self.assertRaises(ValidationError): + last_cat.write({"code": cat.code}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 1) + + @mute_logger("odoo.sql_db") + def test_category_code_unique_system_param(self): + cat_a = self.env["product.category"].search([("code", "=", "AA")], limit=1) + cat_b = self.env["product.category"].search([("code", "=", "AB")], limit=1) + cat_b.write({"code": cat_a.code}) + categories = self.env["product.category"].search([("code", "=", cat_a.code)]) + self.assertTrue(len(categories) == 2) + config = self.env["res.config.settings"].create({}) + with self.assertRaises(ValidationError): + config.write({"product_cat_code_unique_restriction": "system"}) + config.execute() + categories = self.env["product.category"].search([("code", "=", cat_a.code)]) + self.assertTrue(len(categories) == 2) + self.assertFalse( + self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + ) + self.assertFalse( + self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + ) + cat_b.write({"code": "B"}) + config.write({"product_cat_code_unique_restriction": "system"}) + config.execute() + param = self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + self.assertEqual(param, "system") + + @mute_logger("odoo.sql_db") + def test_category_code_unique_parent_children_param(self): + cat = self.env["product.category"].search([("code", "=", "A")], limit=1) + child_cat = self.env["product.category"].search( + [("parent_id", "=", cat.id)], limit=1 + ) + child_cat.write({"code": cat.code}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 2) + config = self.env["res.config.settings"].create({}) + with self.assertRaises(ValidationError): + config.write({"product_cat_code_unique_restriction": "direct"}) + config.execute() + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 2) + self.assertFalse( + self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + ) + child_cat.write({"code": "B"}) + config.write({"product_cat_code_unique_restriction": "direct"}) + config.execute() + param = self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + self.assertEqual(param, "direct") + + @mute_logger("odoo.sql_db") + def test_category_code_unique_hierarchy_param(self): + cat = self.env["product.category"].search([("code", "=", "AB")], limit=1) + last_cat = self.env["product.category"].search([("code", "=", "ABBB")], limit=1) + last_cat.write({"code": cat.code}) + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 2) + config = self.env["res.config.settings"].create({}) + with self.assertRaises(ValidationError): + config.write({"product_cat_code_unique_restriction": "hierarchy"}) + config.execute() + categories = self.env["product.category"].search([("code", "=", cat.code)]) + self.assertTrue(len(categories) == 2) + self.assertFalse( + self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" + ) + ) + last_cat.write({"code": "B"}) + config.write({"product_cat_code_unique_restriction": "hierarchy"}) + config.execute() + param = self.env["ir.config_parameter"].get_param( + "product_code_unique.product_code_unique_restriction" ) + self.assertEqual(param, "hierarchy") diff --git a/product_category_code_unique/views/res_config_settings_views.xml b/product_category_code_unique/views/res_config_settings_views.xml new file mode 100644 index 00000000000..cf55f206803 --- /dev/null +++ b/product_category_code_unique/views/res_config_settings_views.xml @@ -0,0 +1,39 @@ + + + + res.config.settings.view.form.product.code.unique + res.config.settings + + + +

Product Category Code

+
+
+
+
+
+
+
+
+
+
+
+