diff --git a/base_domain_inverse_function/README.rst b/base_domain_inverse_function/README.rst new file mode 100644 index 000000000..1761fe38d --- /dev/null +++ b/base_domain_inverse_function/README.rst @@ -0,0 +1,118 @@ +============================ +Base Domain Inverse Function +============================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/16.0/base_domain_inverse_function + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_domain_inverse_function + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/server-tools&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides functions to decompose normalized domains +into domains operands, as these functions are the inverse of +`AND` and `OR` functions available in `odoo.osv.expression`. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +If you have to decompose a complex domain to inject some conditions, +this shows what you can do: + +.. code-block:: python + + from odoo.osv.expression import AND, OR + from odoo.addons.base_domain_inverse_function.expression import inverse_AND, inverse_OR + + domain = AND([d1, d2, d3]) + d1, d2, d3 = inverse_AND(domain) + + domain = OR([d1, d2, d3]) + d1, d2, d3 = inverse_OR(domain) + +Known issues / Roadmap +====================== + +* Allow to inverse domains containing NOT `'!'` operator + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Akim Juillerat +* `Trobz `_: + + * Son Ho + +Other credits +~~~~~~~~~~~~~ + +The migration of this module from 13.0 to 16.0 was financially supported by Camptocamp + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-grindtildeath| image:: https://github.com/grindtildeath.png?size=40px + :target: https://github.com/grindtildeath + :alt: grindtildeath + +Current `maintainer `__: + +|maintainer-grindtildeath| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_domain_inverse_function/__init__.py b/base_domain_inverse_function/__init__.py new file mode 100644 index 000000000..4bfc27823 --- /dev/null +++ b/base_domain_inverse_function/__init__.py @@ -0,0 +1 @@ +from . import inverse_expression diff --git a/base_domain_inverse_function/__manifest__.py b/base_domain_inverse_function/__manifest__.py new file mode 100644 index 000000000..463106725 --- /dev/null +++ b/base_domain_inverse_function/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Base Domain Inverse Function", + "summary": "Provide function to inverse domain into parts", + "version": "16.0.1.0.0", + "development_status": "Alpha", + "category": "Others", + "website": "https://github.com/OCA/server-tools", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["grindtildeath"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "base", + ], +} diff --git a/base_domain_inverse_function/inverse_expression.py b/base_domain_inverse_function/inverse_expression.py new file mode 100644 index 000000000..a1e391dd3 --- /dev/null +++ b/base_domain_inverse_function/inverse_expression.py @@ -0,0 +1,92 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.osv.expression import ( + AND, + AND_OPERATOR, + DOMAIN_OPERATORS, + NOT_OPERATOR, + OR, + OR_OPERATOR, +) + + +def inverse_combine(domain, operator): + """Decompose normalized domain into operands according to operator + + The result can be then altered easily to inject domain operands before + rebuilding a new domain using the corresponding function from osv.expression. + + :param domain: A normalized domain + :param operator: The "main" domain operator of the domain being either '&' or '|' + (must be the first operator in a normalized domain) + + :return list: A list of domains + """ + if operator not in DOMAIN_OPERATORS: + raise Exception("Unsupported operator parameter: %s" % operator) + operator_func = {AND_OPERATOR: AND, OR_OPERATOR: OR} + other_operator = OR_OPERATOR if operator == AND_OPERATOR else AND_OPERATOR + result = [] + operator_elements_stack = [] + other_elements_stack = [] + elements_stack = [] + + last_element = False + + # 1. Loop over the domain in reverse order + for element in reversed(domain): + if element == NOT_OPERATOR: + raise Exception( + "Inversing domains including NOT operator ('!') is not supported" + ) + if element in DOMAIN_OPERATORS: + # 3. When we reach an operator: + # - pop the last item from the element stack to the corresponding operator stack + # - if such stack contains only one element, the actual operator applies to the two + # last items in the elements stack, so pop the penultimate item as well + if element != operator: + if len(elements_stack) > 0: + other_elements_stack.append([elements_stack.pop()]) + if ( + len(other_elements_stack) == 1 + and last_element not in DOMAIN_OPERATORS + ): + other_elements_stack.append([elements_stack.pop()]) + else: + if len(elements_stack) > 0: + operator_elements_stack.append([elements_stack.pop()]) + if ( + len(operator_elements_stack) == 1 + and last_element not in DOMAIN_OPERATORS + ): + operator_elements_stack.append([elements_stack.pop()]) + last_element = element + else: + # 4. If actual element is a tuple, but last element was an operator, empty the + # corresponding operator stack into the result + if last_element in DOMAIN_OPERATORS: + if last_element != operator: + result.append(operator_func[last_element](other_elements_stack)) + other_elements_stack = [] + else: + # TODO: Add tests to cover these lines (and eventually fix these) + result.append(operator_func[last_element](operator_elements_stack)) + operator_elements_stack = [] + # 2. Add any tuple element to the stack + elements_stack.append(element) + last_element = element + # 5. Empty operators stack when reaching the end + if operator_elements_stack: + operator_elements_stack.extend(result) + result = operator_elements_stack + elif other_elements_stack: + result.append(operator_func[other_operator](other_elements_stack)) + return result + + +def inverse_OR(domain): + return inverse_combine(domain, OR_OPERATOR) + + +def inverse_AND(domain): + return inverse_combine(domain, AND_OPERATOR) diff --git a/base_domain_inverse_function/readme/CONTRIBUTORS.rst b/base_domain_inverse_function/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..8f5c357c6 --- /dev/null +++ b/base_domain_inverse_function/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* Akim Juillerat +* `Trobz `_: + + * Son Ho diff --git a/base_domain_inverse_function/readme/CREDITS.rst b/base_domain_inverse_function/readme/CREDITS.rst new file mode 100644 index 000000000..d43b3aa65 --- /dev/null +++ b/base_domain_inverse_function/readme/CREDITS.rst @@ -0,0 +1 @@ +The migration of this module from 13.0 to 16.0 was financially supported by Camptocamp diff --git a/base_domain_inverse_function/readme/DESCRIPTION.rst b/base_domain_inverse_function/readme/DESCRIPTION.rst new file mode 100644 index 000000000..83701fee0 --- /dev/null +++ b/base_domain_inverse_function/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module provides functions to decompose normalized domains +into domains operands, as these functions are the inverse of +`AND` and `OR` functions available in `odoo.osv.expression`. diff --git a/base_domain_inverse_function/readme/ROADMAP.rst b/base_domain_inverse_function/readme/ROADMAP.rst new file mode 100644 index 000000000..77331c133 --- /dev/null +++ b/base_domain_inverse_function/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Allow to inverse domains containing NOT `'!'` operator diff --git a/base_domain_inverse_function/readme/USAGE.rst b/base_domain_inverse_function/readme/USAGE.rst new file mode 100644 index 000000000..beb7ebec8 --- /dev/null +++ b/base_domain_inverse_function/readme/USAGE.rst @@ -0,0 +1,13 @@ +If you have to decompose a complex domain to inject some conditions, +this shows what you can do: + +.. code-block:: python + + from odoo.osv.expression import AND, OR + from odoo.addons.base_domain_inverse_function.expression import inverse_AND, inverse_OR + + domain = AND([d1, d2, d3]) + d1, d2, d3 = inverse_AND(domain) + + domain = OR([d1, d2, d3]) + d1, d2, d3 = inverse_OR(domain) diff --git a/base_domain_inverse_function/static/description/index.html b/base_domain_inverse_function/static/description/index.html new file mode 100644 index 000000000..af3026b18 --- /dev/null +++ b/base_domain_inverse_function/static/description/index.html @@ -0,0 +1,465 @@ + + + + + + +Base Domain Inverse Function + + + +
+

Base Domain Inverse Function

+ + +

Alpha License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module provides functions to decompose normalized domains +into domains operands, as these functions are the inverse of +AND and OR functions available in odoo.osv.expression.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

If you have to decompose a complex domain to inject some conditions, +this shows what you can do:

+
+from odoo.osv.expression import AND, OR
+from odoo.addons.base_domain_inverse_function.expression import inverse_AND, inverse_OR
+
+domain = AND([d1, d2, d3])
+d1, d2, d3 = inverse_AND(domain)
+
+domain = OR([d1, d2, d3])
+d1, d2, d3 = inverse_OR(domain)
+
+
+
+

Known issues / Roadmap

+
    +
  • Allow to inverse domains containing NOT ‘!’ operator
  • +
+
+
+

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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 13.0 to 16.0 was financially supported by Camptocamp

+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

grindtildeath

+

This module is part of the OCA/server-tools project on GitHub.

+

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

+
+
+
+ + diff --git a/base_domain_inverse_function/tests/__init__.py b/base_domain_inverse_function/tests/__init__.py new file mode 100644 index 000000000..11a470ed1 --- /dev/null +++ b/base_domain_inverse_function/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_inverse_function +from . import test_partner_domains diff --git a/base_domain_inverse_function/tests/test_inverse_function.py b/base_domain_inverse_function/tests/test_inverse_function.py new file mode 100644 index 000000000..29570302e --- /dev/null +++ b/base_domain_inverse_function/tests/test_inverse_function.py @@ -0,0 +1,96 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.osv.expression import AND, OR +from odoo.tests import TransactionCase + +from ..inverse_expression import inverse_AND, inverse_OR + + +class TestInverseFunctions(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.basic_domain_and = ["&", "&", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)] + cls.basic_domain_or = ["|", "|", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)] + cls.complex_domain_and_and_or = ( + ["&"] + cls.basic_domain_and + cls.basic_domain_or + ) + cls.complex_domain_or_or_and = ( + ["|"] + cls.basic_domain_or + cls.basic_domain_and + ) + cls.complex_domain_and_or_or = ["&"] + cls.basic_domain_or + cls.basic_domain_or + cls.complex_domain_or_and_and = ( + ["|"] + cls.basic_domain_and + cls.basic_domain_and + ) + + def test_neutral_basic_and(self): + result = AND(inverse_AND(self.basic_domain_and)) + self.assertEqual(result, self.basic_domain_and) + + def test_neutral_basic_or(self): + result = OR(inverse_OR(self.basic_domain_or)) + self.assertEqual(result, self.basic_domain_or) + + def test_neutral_complex_and_and_or(self): + result = AND(inverse_AND(self.complex_domain_and_and_or)) + self.assertEqual(result, self.complex_domain_and_and_or) + + def test_neutral_complex_or_or_and(self): + result = OR(inverse_OR(self.complex_domain_or_or_and)) + self.assertEqual(result, self.complex_domain_or_or_and) + + def test_neutral_complex_and_or_or(self): + result = AND(inverse_AND(self.complex_domain_and_or_or)) + self.assertEqual(result, self.complex_domain_and_or_or) + + def test_neutral_complex_or_and_and(self): + result = OR(inverse_OR(self.complex_domain_or_and_and)) + self.assertEqual(result, self.complex_domain_or_and_and) + + def test_inverse_basic_and(self): + result = [ + [("a", "=", 1)], + [("b", "=", 2)], + [("c", "=", 3)], + ] + self.assertEqual(inverse_AND(self.basic_domain_and), result) + + def test_inverse_basic_or(self): + result = [ + [("a", "=", 1)], + [("b", "=", 2)], + [("c", "=", 3)], + ] + self.assertEqual(inverse_OR(self.basic_domain_or), result) + + def test_inverse_complex_and_and_or(self): + result = [ + [("a", "=", 1)], + [("b", "=", 2)], + [("c", "=", 3)], + ["|", "|", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ] + self.assertEqual(inverse_AND(self.complex_domain_and_and_or), result) + + def test_inverse_complex_or_or_and(self): + result = [ + [("a", "=", 1)], + [("b", "=", 2)], + [("c", "=", 3)], + ["&", "&", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ] + self.assertEqual(inverse_OR(self.complex_domain_or_or_and), result) + + def test_inverse_complex_and_or_or(self): + result = [ + ["|", "|", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ["|", "|", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ] + self.assertEqual(inverse_AND(self.complex_domain_and_or_or), result) + + def test_inverse_complex_or_and_and(self): + result = [ + ["&", "&", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ["&", "&", ("a", "=", 1), ("b", "=", 2), ("c", "=", 3)], + ] + self.assertEqual(inverse_OR(self.complex_domain_or_and_and), result) diff --git a/base_domain_inverse_function/tests/test_partner_domains.py b/base_domain_inverse_function/tests/test_partner_domains.py new file mode 100644 index 000000000..5e7568578 --- /dev/null +++ b/base_domain_inverse_function/tests/test_partner_domains.py @@ -0,0 +1,80 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.osv.expression import AND, OR +from odoo.tests import TransactionCase + +from ..inverse_expression import inverse_AND, inverse_OR + + +class TestPartnerDomains(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner_model = cls.env["res.partner"] + cls.partner_domains = [ + [("display_name", "ilike", "Deco")], + [("email", "ilike", "example.com")], + [("country_id", "=", cls.env.ref("base.us").id)], + ] + + def test_inverse_partner_domain_and(self): + and_domains = AND(self.partner_domains) + partner_domains = inverse_AND(and_domains) + # Ensure there is at least 1 result + self.assertTrue(self.partner_model.search(and_domains)) + # Ensure result is same after inverse + self.assertEqual( + self.partner_model.search(and_domains), + self.partner_model.search(AND(partner_domains)), + ) + + def test_inverse_partner_domain_or(self): + or_domains = OR(self.partner_domains) + partner_domains = inverse_OR(or_domains) + # Ensure there is at least 1 result + self.assertTrue(self.partner_model.search(or_domains)) + # Ensure result is same after inverse + self.assertEqual( + self.partner_model.search(or_domains), + self.partner_model.search(OR(partner_domains)), + ) + + def test_inverse_partner_domain_or_subdomain_and(self): + partner_domains_2 = [ + [("display_name", "ilike", "Gemini")], + [("email", "ilike", "example.com")], + [("country_id", "=", self.env.ref("base.us").id)], + ] + composed_domain = OR([AND(self.partner_domains), AND(partner_domains_2)]) + decomposed_or_domains = inverse_OR(composed_domain) + decomposed_and_domains_1 = inverse_AND(decomposed_or_domains[0]) + decomposed_and_domains_2 = inverse_AND(decomposed_or_domains[1]) + # Ensure there is at least 1 result + self.assertTrue(self.partner_model.search(composed_domain)) + # Ensure result is same after inverse + self.assertEqual( + self.partner_model.search(composed_domain), + self.partner_model.search( + OR([AND(decomposed_and_domains_1), AND(decomposed_and_domains_2)]) + ), + ) + + def test_inverse_partner_domain_and_subdomain_or(self): + partner_domains_2 = [ + [("display_name", "ilike", "Gemini")], + [("email", "ilike", "example.com")], + [("country_id", "=", self.env.ref("base.us").id)], + ] + composed_domain = AND([OR(self.partner_domains), OR(partner_domains_2)]) + decomposed_and_domains = inverse_AND(composed_domain) + decomposed_or_domains_1 = inverse_OR(decomposed_and_domains[0]) + decomposed_or_domains_2 = inverse_OR(decomposed_and_domains[1]) + # Ensure there is at least 1 result + self.assertTrue(self.partner_model.search(composed_domain)) + # Ensure result is same after inverse + self.assertEqual( + self.partner_model.search(composed_domain), + self.partner_model.search( + AND([OR(decomposed_or_domains_1), OR(decomposed_or_domains_2)]) + ), + ) diff --git a/setup/base_domain_inverse_function/odoo/addons/base_domain_inverse_function b/setup/base_domain_inverse_function/odoo/addons/base_domain_inverse_function new file mode 120000 index 000000000..4dbdaebb4 --- /dev/null +++ b/setup/base_domain_inverse_function/odoo/addons/base_domain_inverse_function @@ -0,0 +1 @@ +../../../../base_domain_inverse_function \ No newline at end of file diff --git a/setup/base_domain_inverse_function/setup.py b/setup/base_domain_inverse_function/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/base_domain_inverse_function/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)