Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: odoo/upgrade-util
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2355c774606310e0afa8013b99aa3fe4fb88380d
Choose a base ref
..
head repository: odoo/upgrade-util
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 6df6be9442e5a27fad37954d5cd67f0c8f915cdf
Choose a head ref
Showing with 90 additions and 56 deletions.
  1. +39 −13 src/base/tests/test_util.py
  2. +9 −43 src/util/domains.py
  3. +42 −0 src/util/hr_payroll.py
52 changes: 39 additions & 13 deletions src/base/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -27,6 +27,9 @@
)
from odoo.addons.base.maintenance.migrations.util.exceptions import MigrationError

USE_ORM_DOMAIN = util.misc.version_gte("saas~18.2")
NOTNOT = () if USE_ORM_DOMAIN else ("!", "!")


class TestAdaptOneDomain(UnitTestCase):
def setUp(self):
@@ -176,7 +179,7 @@ def test_adapter_more_domains(self):
# double '!'
self.mock_adapter.reset_mock()
domain = ["!", "!", ("partner_id.user_id", "=", 1)]
match_domain = ["!", "!", ("partner_id.friend_id", "=", 2)]
match_domain = [*NOTNOT, ("partner_id.friend_id", "=", 2)]
new_domain = _adapt_one_domain(
self.cr, "res.partner", "user_id", "friend_id", "res.users", domain, adapter=self.mock_adapter
)
@@ -186,7 +189,7 @@ def test_adapter_more_domains(self):
# triple '!'
self.mock_adapter.reset_mock()
domain = ["!", "!", "!", ("partner_id.user_id", "=", 1)]
match_domain = ["!", "!", "!", ("partner_id.friend_id", "=", 2)]
match_domain = [*NOTNOT, "!", ("partner_id.friend_id", "=", 2)]
new_domain = _adapt_one_domain(
self.cr, "res.partner", "user_id", "friend_id", "res.users", domain, adapter=self.mock_adapter
)
@@ -196,7 +199,7 @@ def test_adapter_more_domains(self):
# '|' double '!'
self.mock_adapter.reset_mock()
domain = ["|", "!", "!", ("partner_id.user_id", "=", 1), ("name", "=", False)]
match_domain = ["|", "!", "!", ("partner_id.friend_id", "=", 2), ("name", "=", False)]
match_domain = ["|", *NOTNOT, ("partner_id.friend_id", "=", 2), ("name", "=", False)]
new_domain = _adapt_one_domain(
self.cr, "res.partner", "user_id", "friend_id", "res.users", domain, adapter=self.mock_adapter
)
@@ -206,7 +209,7 @@ def test_adapter_more_domains(self):
# '&' double '!'
self.mock_adapter.reset_mock()
domain = ["&", "!", "!", ("partner_id.user_id", "=", 1), ("name", "=", False)]
match_domain = ["&", "!", "!", ("partner_id.friend_id", "=", 2), ("name", "=", False)]
match_domain = ["&", *NOTNOT, ("partner_id.friend_id", "=", 2), ("name", "=", False)]
new_domain = _adapt_one_domain(
self.cr, "res.partner", "user_id", "friend_id", "res.users", domain, adapter=self.mock_adapter
)
@@ -378,9 +381,9 @@ class TestRemoveFieldDomains(UnitTestCase):
# operator is not relevant
([("updated", "!=", 0)], [TRUE_LEAF]),
# if negate we should end with "not false"
(["!", ("updated", "!=", 0)], ["!", FALSE_LEAF]),
(["!", ("updated", "!=", 0)], [TRUE_LEAF] if USE_ORM_DOMAIN else ["!", FALSE_LEAF]),
# multiple !, we should still end with a true leaf
(["!", "!", ("updated", ">", 0)], ["!", "!", TRUE_LEAF]),
(["!", "!", ("updated", ">", 0)], [*NOTNOT, TRUE_LEAF]),
# with operator
([("updated", "=", 0), ("state", "=", "done")], ["&", TRUE_LEAF, ("state", "=", "done")]),
(["&", ("updated", "=", 0), ("state", "=", "done")], ["&", TRUE_LEAF, ("state", "=", "done")]),
@@ -389,11 +392,31 @@ class TestRemoveFieldDomains(UnitTestCase):
(["&", ("state", "=", "done"), ("updated", "=", 0)], ["&", ("state", "=", "done"), TRUE_LEAF]),
(["|", ("state", "=", "done"), ("updated", "=", 0)], ["|", ("state", "=", "done"), FALSE_LEAF]),
# combination with !
(["&", "!", ("updated", "=", 0), ("state", "=", "done")], ["&", "!", FALSE_LEAF, ("state", "=", "done")]),
(["|", "!", ("updated", "=", 0), ("state", "=", "done")], ["|", "!", TRUE_LEAF, ("state", "=", "done")]),
(
["&", "!", ("updated", "=", 0), ("state", "=", "done")],
["&", TRUE_LEAF, ("state", "=", "done")]
if USE_ORM_DOMAIN
else ["&", "!", FALSE_LEAF, ("state", "=", "done")],
),
(
["|", "!", ("updated", "=", 0), ("state", "=", "done")],
["|", FALSE_LEAF, ("state", "=", "done")]
if USE_ORM_DOMAIN
else ["|", "!", TRUE_LEAF, ("state", "=", "done")],
),
# here, the ! apply on the whole &/| and should not invert the replaced leaf
(["!", "&", ("updated", "=", 0), ("state", "=", "done")], ["!", "&", TRUE_LEAF, ("state", "=", "done")]),
(["!", "|", ("updated", "=", 0), ("state", "=", "done")], ["!", "|", FALSE_LEAF, ("state", "=", "done")]),
(
["!", "&", ("updated", "=", 0), ("state", "=", "done")],
["|", FALSE_LEAF, ("state", "!=", "done")]
if USE_ORM_DOMAIN
else ["!", "&", TRUE_LEAF, ("state", "=", "done")],
),
(
["!", "|", ("updated", "=", 0), ("state", "=", "done")],
["&", TRUE_LEAF, ("state", "!=", "done")]
if USE_ORM_DOMAIN
else ["!", "|", FALSE_LEAF, ("state", "=", "done")],
),
]
)
def test_remove_field(self, domain, expected):
@@ -845,7 +868,8 @@ def test_invert_boolean_field(self):
util.invert_boolean_field(cr, model, old_name, new_name)

util.invalidate(fltr)
self.assertEqual(literal_eval(fltr.domain), ["!", (new_name, "=", True)])
expected = ["!", (new_name, "=", True)]
self.assertEqual(literal_eval(fltr.domain), expected)

cr.execute(util.format_query(cr, query, table, new_name))
inverted_repartition = dict(cr.fetchall())
@@ -859,14 +883,16 @@ def test_invert_boolean_field(self):
util.rename_field(cr, model, new_name, old_name)

util.invalidate(fltr)
self.assertEqual(literal_eval(fltr.domain), ["!", (old_name, "=", True)])
expected = [(old_name, "!=", True)] if USE_ORM_DOMAIN else ["!", (old_name, "=", True)]
self.assertEqual(literal_eval(fltr.domain), expected)

# invert with same name; will invert domains and data
with mock.patch.object(cr, "commit", lambda: ...):
util.invert_boolean_field(cr, model, old_name, old_name)

util.invalidate(fltr)
self.assertEqual(literal_eval(fltr.domain), ["!", "!", (old_name, "=", True)])
expected = ["!", (old_name, "!=", True)] if USE_ORM_DOMAIN else ["!", "!", (old_name, "=", True)]
self.assertEqual(literal_eval(fltr.domain), expected)

cr.execute(util.format_query(cr, query, table, old_name))
back_repartition = dict(cr.fetchall())
52 changes: 9 additions & 43 deletions src/util/domains.py
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@
from .const import NEARLYWARN
from .helpers import _dashboard_actions, _validate_model, resolve_model_fields_path
from .inherit import for_each_inherit
from .misc import SelfPrintEvalContext
from .misc import SelfPrintEvalContext, version_gte
from .pg import column_exists, get_value_or_en_translation, table_exists
from .records import edit_view

@@ -45,7 +45,9 @@

# import from domains/expression
try:
import odoo.domains as _dom
if not version_gte("saas~18.2"):
raise ImportError("Use osv.expression to migrate") # noqa: TRY301
import odoo.orm.domains as _dom

FALSE_LEAF = _dom._FALSE_LEAF
TRUE_LEAF = _dom._TRUE_LEAF
@@ -54,8 +56,7 @@
OR_OPERATOR = _dom.DomainOr.OPERATOR
DOMAIN_OPERATORS = {NOT_OPERATOR, AND_OPERATOR, OR_OPERATOR}

# normalization functions are redefined here because they will be deprecated
# the Domain factory normalized but also distributes operators during creation of domains
# normalization functions based on odoo.orm.domains

def normalize_domain(domain):
"""Return a normalized version of the domain.
@@ -64,49 +65,14 @@ def normalize_domain(domain):
One property of normalized domain expressions is that they
can be easily combined together as if they were single domain components.
"""
assert isinstance(
domain, (list, tuple)
), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
if not domain:
return [TRUE_LEAF]
result = []
expected = 1
op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
for token in domain:
if expected == 0: # more than expected, like in [A, B]
result[0:0] = [AND_OPERATOR] # put an extra '&' in front
expected = 1
if isinstance(token, (list, tuple)): # domain term
expected -= 1
if len(token) == 3 and token[1] in ("any", "not any"):
new_token = (token[0], token[1], normalize_domain(token[2]))
result.append(new_token)
else:
result.append(normalize_leaf(token))
else:
expected += op_arity.get(token, 0) - 1
result.append(token)
if expected:
raise ValueError("Domain {!r} is syntactically not correct.".format(domain))
return result
return list(_dom.Domain(domain))

def normalize_leaf(leaf):
if not is_leaf(leaf):
raise TypeError("Leaf must be a tuple or list of 3 values")
left, operator, right = leaf
original = operator
operator = operator.lower()
if operator == "<>":
operator = "!="
if isinstance(right, bool) and operator in ("in", "not in"):
_logger.warning("The domain term '%s' should use the '=' or '!=' operator.", ((left, original, right),))
operator = "=" if operator == "in" else "!="
if isinstance(right, (list, tuple)) and operator in ("=", "!="):
_logger.warning(
"The domain term '%s' should use the 'in' or 'not in' operator.", ((left, original, right),)
)
operator = "in" if operator == "=" else "not in"
return left, operator, right
domain = _dom.Domain(*leaf)
assert isinstance(domain, _dom.DomainCondition)
return next(iter(domain))

def is_leaf(leaf):
return (
42 changes: 42 additions & 0 deletions src/util/hr_payroll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging

from .fields import remove_field
from .records import delete_unused, ref

_logger = logging.getLogger(__name__)


def remove_salary_rule(cr, xmlid):
rid = ref(cr, xmlid)
cr.execute(
r"""
SELECT f.name
FROM ir_model_fields f,
hr_salary_rule r
JOIN hr_payroll_structure s
ON r.struct_id = s.id
LEFT JOIN res_country c
ON s.country_id = c.id
WHERE r.id = %s
AND f.model = 'hr.payroll.report'
AND f.name = regexp_replace(
concat_ws(
'_',
'x_l10n',
COALESCE(lower(c.code), 'xx'),
lower(r.code)
),
'[\.\- ]',
'_'
)
""",
[rid],
)
for (fname,) in cr.fetchall():
_logger.info(
"Removing field %r from model 'hr.payroll.report' since salary rule %r is being removed",
fname,
xmlid,
)
remove_field(cr, "hr.payroll.report", fname)
delete_unused(cr, xmlid)