Skip to content

Commit e331eb5

Browse files
committed
fix(spp_cel_domain): normalize Odoo False to None for null comparisons
Odoo ORM returns False for unset non-boolean fields (Datetime, Date, Char, Many2one, etc.), but CEL null maps to Python None. This caused expressions like `m.disabled != null` to evaluate as `False != None` which is True, matching every record even when the field is actually NULL in the database. _safe_getattr now detects Odoo records (via _fields attribute) and converts False to None for non-boolean fields, so CEL null comparisons behave correctly. Boolean fields retain their False value.
1 parent 44f6d00 commit e331eb5

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

spp_cel_domain/services/cel_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ def _safe_getattr(obj: Any, name: str, default: Any = None) -> Any:
7777
raise AttributeError(f"Access to attribute '{name}' is not allowed")
7878

7979
if hasattr(obj, name):
80-
return getattr(obj, name)
80+
value = getattr(obj, name)
81+
# Odoo ORM returns False for unset non-boolean fields (Datetime,
82+
# Date, Char, Many2one, etc.). Normalize to None so that CEL null
83+
# comparisons work correctly (e.g., `m.disabled != null`).
84+
if value is False and hasattr(obj, "_fields") and name in obj._fields:
85+
if obj._fields[name].type != "boolean":
86+
return None
87+
return value
8188
if isinstance(obj, dict):
8289
return obj.get(name, default)
8390
return default

spp_cel_domain/tests/test_cel_parser.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,59 @@ def test_position_tracking_in_tokens(self):
409409
for tok in tokens:
410410
self.assertIsInstance(tok.pos, int)
411411
self.assertGreaterEqual(tok.pos, 0)
412+
413+
def test_odoo_null_field_equals_null(self):
414+
"""Odoo returns False for unset non-boolean fields; CEL null comparison should treat as None.
415+
416+
When an Odoo Datetime/Date/Char/etc. field is NULL in the DB, the ORM
417+
returns False. CEL 'null' maps to Python None. Without normalization,
418+
`m.disabled != null` becomes `False != None` which is True for every
419+
record, even though the field IS null.
420+
"""
421+
# Create a registrant with an unset disabled (Datetime) field
422+
partner = self.env["res.partner"].create(
423+
{"name": "CEL Null Test Person", "is_registrant": True, "is_group": False}
424+
)
425+
426+
# Odoo ORM returns False for unset Datetime fields
427+
self.assertIs(partner.disabled, False)
428+
429+
# CEL: m.disabled == null should be True (field is unset)
430+
ast = P.parse("m.disabled == null")
431+
result = P.evaluate(ast, {"m": partner})
432+
self.assertTrue(
433+
result,
434+
"m.disabled == null should be True when disabled is unset "
435+
f"(ORM returns {partner.disabled!r})",
436+
)
437+
438+
# CEL: m.disabled != null should be False (field is unset)
439+
ast = P.parse("m.disabled != null")
440+
result = P.evaluate(ast, {"m": partner})
441+
self.assertFalse(
442+
result,
443+
"m.disabled != null should be False when disabled is unset "
444+
f"(ORM returns {partner.disabled!r})",
445+
)
446+
447+
def test_odoo_boolean_false_not_treated_as_null(self):
448+
"""Boolean fields that are legitimately False should NOT be normalized to None."""
449+
partner = self.env["res.partner"].create(
450+
{"name": "CEL Bool Test", "is_registrant": False}
451+
)
452+
453+
# is_registrant is a Boolean field, False is a real value
454+
ast = P.parse("m.is_registrant == false")
455+
result = P.evaluate(ast, {"m": partner})
456+
self.assertTrue(
457+
result,
458+
"Boolean False should remain False, not become None",
459+
)
460+
461+
# Boolean False should NOT equal null
462+
ast = P.parse("m.is_registrant == null")
463+
result = P.evaluate(ast, {"m": partner})
464+
self.assertFalse(
465+
result,
466+
"Boolean False should not equal null",
467+
)

0 commit comments

Comments
 (0)