Skip to content

Field Expressions API Reference

Type-safe field expressions for building complex Odoo queries with fluent interface, comparison operators, and logical combinations.

Overview

The expressions system provides:

  • Field Class: Fluent interface for field-based conditions
  • Comparison Expressions: Type-safe comparison operators
  • Logical Expressions: AND, OR, NOT combinations
  • Pattern Matching: LIKE, ILIKE, contains, starts/ends with
  • Domain Generation: Automatic conversion to Odoo domain format

Field Class

Represents a field in query expressions with fluent interface for building conditions.

Constructor

class Field:
    """Represents a field in query expressions."""

    def __init__(self, name: str):
        """Initialize a field reference."""
        self.name = name

Parameters:

  • name (str): Field name (supports dot notation for related fields)

Example:

from zenoo_rpc.query.expressions import Field
from zenoo_rpc.models.common import ResPartner

# Simple field
name_field = Field("name")
active_field = Field("active")

# Related field with dot notation
country_name = Field("country_id.name")
parent_email = Field("parent_id.email")

Comparison Operators

Equality Operators

# Equality (==)
def __eq__(self, value: Any) -> Equal:
    """Create an equality condition."""

# Not equal (!=)
def __ne__(self, value: Any) -> NotEqual:
    """Create a not-equal condition."""

Example:

name_field = Field("name")
active_field = Field("active")

# Equality conditions
name_equals = name_field == "ACME Corp"
active_true = active_field == True

# Not equal conditions
name_not_test = name_field != "Test Company"
active_not_false = active_field != False

# Use in queries
partners = await client.model(ResPartner).filter(
    name_equals & active_true
).all()

Numeric Comparison Operators

# Greater than (>)
def __gt__(self, value: Any) -> GreaterThan:
    """Create a greater-than condition."""

# Greater than or equal (>=)
def __ge__(self, value: Any) -> GreaterEqual:
    """Create a greater-than-or-equal condition."""

# Less than (<)
def __lt__(self, value: Any) -> LessThan:
    """Create a less-than condition."""

# Less than or equal (<=)
def __le__(self, value: Any) -> LessEqual:
    """Create a less-than-or-equal condition."""

Example:

rank_field = Field("customer_rank")
age_field = Field("age")

# Numeric comparisons
high_rank = rank_field > 5
min_rank = rank_field >= 1
young = age_field < 30
adult = age_field >= 18

# Range conditions
adult_range = (age_field >= 18) & (age_field <= 65)

# Use in queries
customers = await client.model(ResPartner).filter(
    high_rank & adult_range
).all()

String Pattern Methods

like(pattern)

Create a LIKE condition (case-sensitive pattern matching).

Parameters:

  • pattern (str): SQL LIKE pattern with % and _ wildcards

Returns: Like - LIKE expression

Example:

name_field = Field("name")

# Case-sensitive LIKE
starts_with_acme = name_field.like("ACME%")
contains_corp = name_field.like("%Corp%")
ends_with_inc = name_field.like("%Inc")

# Use wildcards
pattern_match = name_field.like("A_ME%")  # A + any char + ME + anything

ilike(pattern)

Create an ILIKE condition (case-insensitive pattern matching).

Parameters:

  • pattern (str): SQL ILIKE pattern with % and _ wildcards

Returns: ILike - ILIKE expression

Example:

name_field = Field("name")

# Case-insensitive ILIKE
starts_with_acme = name_field.ilike("acme%")
contains_corp = name_field.ilike("%corp%")
ends_with_inc = name_field.ilike("%inc")

# Mixed case patterns
mixed_pattern = name_field.ilike("AcMe%")

contains(value)

Create a contains condition (field contains substring).

Parameters:

  • value (str): Substring to search for

Returns: Contains - Contains expression (uses ILIKE with % wildcards)

Example:

name_field = Field("name")
email_field = Field("email")

# Contains substring
name_contains_corp = name_field.contains("Corp")
email_contains_acme = email_field.contains("acme")

# Use in queries
partners = await client.model(ResPartner).filter(
    name_contains_corp | email_contains_acme
).all()

startswith(value)

Create a starts-with condition.

Parameters:

  • value (str): Prefix to match

Returns: StartsWith - Starts-with expression

Example:

name_field = Field("name")

# Starts with prefix
starts_with_acme = name_field.startswith("ACME")
starts_with_global = name_field.startswith("Global")

# Use in queries
companies = await client.model(ResPartner).filter(
    starts_with_acme | starts_with_global
).all()

endswith(value)

Create an ends-with condition.

Parameters:

  • value (str): Suffix to match

Returns: EndsWith - Ends-with expression

Example:

name_field = Field("name")

# Ends with suffix
ends_with_corp = name_field.endswith("Corp")
ends_with_inc = name_field.endswith("Inc")

# Use in queries
companies = await client.model(ResPartner).filter(
    ends_with_corp | ends_with_inc
).all()

List Operations

in_(values)

Create an IN condition (value in list).

Parameters:

  • values (List[Any]): List of values to match

Returns: In - IN expression

Example:

id_field = Field("id")
country_code_field = Field("country_id.code")

# IN conditions
specific_ids = id_field.in_([1, 2, 3, 4, 5])
us_ca_partners = country_code_field.in_(["US", "CA"])

# Use in queries
partners = await client.model(ResPartner).filter(
    specific_ids & us_ca_partners
).all()

not_in(values)

Create a NOT IN condition (value not in list).

Parameters:

  • values (List[Any]): List of values to exclude

Returns: NotIn - NOT IN expression

Example:

name_field = Field("name")
state_field = Field("state")

# NOT IN conditions
not_test_names = name_field.not_in(["Test Company", "Demo Company"])
not_draft_cancelled = state_field.not_in(["draft", "cancel"])

# Use in queries
valid_partners = await client.model(ResPartner).filter(
    not_test_names & not_draft_cancelled
).all()

Null Checks

is_null()

Create an IS NULL condition.

Returns: Equal - IS NULL expression (uses False value)

Example:

parent_field = Field("parent_id")
email_field = Field("email")

# Null checks
no_parent = parent_field.is_null()
no_email = email_field.is_null()

# Use in queries
top_level_partners = await client.model(ResPartner).filter(
    no_parent
).all()

is_not_null()

Create an IS NOT NULL condition.

Returns: NotEqual - IS NOT NULL expression (uses False value)

Example:

email_field = Field("email")
phone_field = Field("phone")

# Not null checks
has_email = email_field.is_not_null()
has_phone = phone_field.is_not_null()

# Use in queries
contactable_partners = await client.model(ResPartner).filter(
    has_email | has_phone
).all()

Comparison Expressions

Base classes for all comparison operations.

ComparisonExpression

Base class for comparison expressions.

class ComparisonExpression(Expression):
    """Base class for comparison expressions."""

    def __init__(self, field: str, operator: str, value: Any):
        self.field = field
        self.operator = operator
        self.value = value

    def to_domain(self) -> List[Tuple[str, str, Any]]:
        """Convert to domain tuple."""
        return [(self.field, self.operator, self.value)]

Specific Comparison Classes

Equal

Equality expression (=).

class Equal(ComparisonExpression):
    """Equality expression (=)."""

    def __init__(self, field: str, value: Any):
        super().__init__(field, "=", value)

NotEqual

Not equal expression (!=).

class NotEqual(ComparisonExpression):
    """Not equal expression (!=)."""

    def __init__(self, field: str, value: Any):
        super().__init__(field, "!=", value)

GreaterThan, GreaterEqual, LessThan, LessEqual

Numeric comparison expressions.

class GreaterThan(ComparisonExpression):
    """Greater than expression (>)."""

class GreaterEqual(ComparisonExpression):
    """Greater than or equal expression (>=)."""

class LessThan(ComparisonExpression):
    """Less than expression (<)."""

class LessEqual(ComparisonExpression):
    """Less than or equal expression (<=)."""

Like, ILike

Pattern matching expressions.

class Like(ComparisonExpression):
    """LIKE expression (case-sensitive pattern matching)."""

class ILike(ComparisonExpression):
    """ILIKE expression (case-insensitive pattern matching)."""

In, NotIn

List membership expressions.

class In(ComparisonExpression):
    """IN expression (value in list)."""

class NotIn(ComparisonExpression):
    """NOT IN expression (value not in list)."""

Contains, StartsWith, EndsWith

String pattern expressions (automatically convert to ILIKE patterns).

class Contains(ComparisonExpression):
    """Contains expression (field contains substring)."""

    def __init__(self, field: str, value: str):
        # Convert to ILIKE pattern
        pattern = f"%{value}%"
        super().__init__(field, "ilike", pattern)

class StartsWith(ComparisonExpression):
    """Starts with expression (field starts with substring)."""

    def __init__(self, field: str, value: str):
        # Convert to ILIKE pattern
        pattern = f"{value}%"
        super().__init__(field, "ilike", pattern)

class EndsWith(ComparisonExpression):
    """Ends with expression (field ends with substring)."""

    def __init__(self, field: str, value: str):
        # Convert to ILIKE pattern
        pattern = f"%{value}"
        super().__init__(field, "ilike", pattern)

Logical Expressions

Combine multiple expressions with logical operators.

LogicalExpression

Base class for logical expressions.

class LogicalExpression(Expression):
    """Base class for logical expressions."""

    def __init__(self, *expressions: Expression):
        self.expressions = expressions

AndExpression

AND expression combining multiple conditions.

class AndExpression(LogicalExpression):
    """AND expression combining multiple conditions."""

    def to_domain(self) -> List[Union[str, Tuple[str, str, Any]]]:
        """Convert to domain with AND logic."""
        domain = []
        for expr in self.expressions:
            expr_domain = expr.to_domain()
            domain.extend(expr_domain)
        return domain

Example:

from zenoo_rpc.query.expressions import Field, AndExpression

name_field = Field("name")
active_field = Field("active")
rank_field = Field("customer_rank")

# Create individual conditions
name_condition = name_field.contains("Corp")
active_condition = active_field == True
rank_condition = rank_field > 0

# Combine with AND
and_expr = AndExpression(name_condition, active_condition, rank_condition)

# Use in query
partners = await client.model(ResPartner).filter(and_expr).all()

# Or use & operator
combined = name_condition & active_condition & rank_condition

OrExpression

OR expression combining multiple conditions.

class OrExpression(LogicalExpression):
    """OR expression combining multiple conditions."""

    def to_domain(self) -> List[Union[str, Tuple[str, str, Any]]]:
        """Convert to domain with OR logic."""
        if len(self.expressions) <= 1:
            return self.expressions[0].to_domain() if self.expressions else []

        domain = ["|"]  # OR operator
        for expr in self.expressions:
            expr_domain = expr.to_domain()
            domain.extend(expr_domain)
        return domain

Example:

from zenoo_rpc.query.expressions import Field, OrExpression

name_field = Field("name")
email_field = Field("email")

# Create search conditions
name_search = name_field.contains("acme")
email_search = email_field.contains("acme")

# Combine with OR
or_expr = OrExpression(name_search, email_search)

# Use in query
partners = await client.model(ResPartner).filter(or_expr).all()

# Or use | operator
search_expr = name_search | email_search

NotExpression

NOT expression negating a condition.

class NotExpression(Expression):
    """NOT expression negating a condition."""

    def __init__(self, expression: Expression):
        self.expression = expression

    def to_domain(self) -> List[Union[str, Tuple[str, str, Any]]]:
        """Convert to domain with NOT logic."""
        expr_domain = self.expression.to_domain()
        return ["!"] + expr_domain  # NOT operator

Example:

from zenoo_rpc.query.expressions import Field, NotExpression

active_field = Field("active")
name_field = Field("name")

# Create condition to negate
inactive_condition = active_field == False
test_name_condition = name_field.startswith("Test")

# Negate conditions
not_inactive = NotExpression(inactive_condition)
not_test = NotExpression(test_name_condition)

# Use in query
partners = await client.model(ResPartner).filter(
    not_inactive & not_test
).all()

# Or use ~ operator
active_partners = ~inactive_condition
non_test_partners = ~test_name_condition

Expression Operators

All expressions support logical operators for easy combination.

__and__ (& operator)

Combine expressions with AND.

def __and__(self, other: Expression) -> AndExpression:
    """Combine expressions with AND operator."""
    return AndExpression(self, other)

__or__ (| operator)

Combine expressions with OR.

def __or__(self, other: Expression) -> OrExpression:
    """Combine expressions with OR operator."""
    return OrExpression(self, other)

__invert__ (~ operator)

Negate expression with NOT.

def __invert__(self) -> NotExpression:
    """Negate the expression with NOT operator."""
    return NotExpression(self)

Advanced Usage Patterns

Complex Query Building

from zenoo_rpc.query.expressions import Field

# Define fields
name = Field("name")
email = Field("email")
active = Field("active")
rank = Field("customer_rank")
country = Field("country_id.code")

# Build complex query
complex_query = (
    # Search in name or email
    (name.contains("acme") | email.contains("acme")) &
    # Must be active
    active == True &
    # Must be customer
    rank > 0 &
    # In specific countries
    country.in_(["US", "CA", "GB"]) &
    # Not test companies
    ~name.startswith("Test")
)

# Use in query
partners = await client.model(ResPartner).filter(complex_query).all()

Dynamic Expression Building

def build_search_expression(search_fields: List[str], search_term: str) -> Expression:
    """Build dynamic search expression across multiple fields."""
    if not search_fields or not search_term:
        return Field("id") > 0  # Always true condition

    # Create search conditions for each field
    conditions = []
    for field_name in search_fields:
        field = Field(field_name)
        condition = field.contains(search_term)
        conditions.append(condition)

    # Combine with OR
    if len(conditions) == 1:
        return conditions[0]
    else:
        result = conditions[0]
        for condition in conditions[1:]:
            result = result | condition
        return result

# Usage
search_expr = build_search_expression(
    ["name", "email", "phone", "ref"],
    "acme"
)

partners = await client.model(ResPartner).filter(search_expr).all()

Range Queries

def create_date_range(field_name: str, start_date: str, end_date: str) -> Expression:
    """Create date range expression."""
    field = Field(field_name)
    return (field >= start_date) & (field <= end_date)

def create_numeric_range(field_name: str, min_val: float, max_val: float) -> Expression:
    """Create numeric range expression."""
    field = Field(field_name)
    return (field >= min_val) & (field <= max_val)

# Usage
date_range = create_date_range("create_date", "2023-01-01", "2023-12-31")
rank_range = create_numeric_range("customer_rank", 1, 5)

partners = await client.model(ResPartner).filter(
    date_range & rank_range
).all()

Best Practices

1. Use Field Objects for Reusability

# ✅ Good: Define field objects once
name_field = Field("name")
email_field = Field("email")
active_field = Field("active")

# Reuse in multiple expressions
search_expr = name_field.contains("acme") | email_field.contains("acme")
active_expr = active_field == True

# ❌ Avoid: Creating fields repeatedly
search_expr = Field("name").contains("acme") | Field("email").contains("acme")

2. Use Appropriate Comparison Methods

# ✅ Good: Use specific methods for clarity
name_field.startswith("ACME")  # Clear intent
rank_field >= 1                # Natural operator

# ❌ Avoid: Generic patterns when specific methods exist
name_field.like("ACME%")       # Less clear than startswith
# ✅ Good: Group logically related conditions
customer_conditions = (rank_field > 0) & (active_field == True)
location_conditions = country_field.in_(["US", "CA"])
search_conditions = name_field.contains("corp") | email_field.contains("corp")

final_query = customer_conditions & location_conditions & search_conditions

# ❌ Avoid: Flat, unstructured conditions

Next Steps