Create module base_domain_inverse_function

pull/2592/head
Akim Juillerat 2022-11-24 16:24:10 +01:00 committed by sonhd91
parent c0064d6939
commit 411c3af759
11 changed files with 319 additions and 0 deletions

View File

@ -0,0 +1 @@
To auto generate

View File

@ -0,0 +1 @@
from . import inverse_expression

View File

@ -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": "13.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",
],
}

View File

@ -0,0 +1,90 @@
# 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)

View File

@ -0,0 +1 @@
* Akim Juillerat <akim.juillerat@camptocamp.com>

View File

@ -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`.

View File

@ -0,0 +1 @@
* Allow to inverse domains containing NOT `'!'` operator

View File

@ -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)

View File

@ -0,0 +1,2 @@
from . import test_inverse_function
from . import test_partner_domains

View File

@ -0,0 +1,109 @@
# 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 SavepointCase
from ..inverse_expression import inverse_AND, inverse_OR
class TestInverseFunctions(SavepointCase):
@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
)

View File

@ -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 SavepointCase
from ..inverse_expression import inverse_AND, inverse_OR
class TestPartnerDomains(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_model = cls.env["res.partner"]
cls.partner_domains = [
[("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 = [
[("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 = [
[("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(
OR([AND(decomposed_or_domains_1), AND(decomposed_or_domains_2)])
)
)