Create module base_domain_inverse_function
parent
c0064d6939
commit
411c3af759
|
@ -0,0 +1 @@
|
|||
To auto generate
|
|
@ -0,0 +1 @@
|
|||
from . import inverse_expression
|
|
@ -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",
|
||||
],
|
||||
}
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
* Akim Juillerat <akim.juillerat@camptocamp.com>
|
|
@ -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`.
|
|
@ -0,0 +1 @@
|
|||
* Allow to inverse domains containing NOT `'!'` operator
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
from . import test_inverse_function
|
||||
from . import test_partner_domains
|
|
@ -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
|
||||
)
|
|
@ -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)])
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue