Skip to content

Models API Reference

The models module provides type-safe, Pydantic-based representations of Odoo models with automatic validation, relationship handling, and ORM-like functionality.

Overview

Zenoo RPC models offer:

  • Type Safety: Full type hints and IDE support
  • Validation: Automatic data validation with Pydantic
  • Relationships: Lazy loading and relationship management
  • Registry: Dynamic model registration and discovery
  • Field Types: Specialized field types for Odoo data

Base Classes

OdooModel

Base class for all Odoo model representations.

from zenoo_rpc.models.base import OdooModel
from typing import Optional, ClassVar

class CustomModel(OdooModel):
    odoo_name: ClassVar[str] = "custom.model"

    name: str
    description: Optional[str] = None
    active: bool = True

Key Features:

  • Inherits from Pydantic BaseModel
  • Automatic field validation
  • JSON serialization/deserialization
  • Relationship field support
  • Computed properties

Class Variables:

  • odoo_name: Odoo model name (required)
  • model_fields: Field definitions (auto-generated by Pydantic)
  • Relationship fields are defined using field descriptors

OdooRecord

Enhanced model with client integration for active record pattern.

from zenoo_rpc.models.base import OdooRecord

class ActivePartner(OdooRecord):
    odoo_name: ClassVar[str] = "res.partner"

    name: str
    email: Optional[str] = None

    async def save(self):
        """Save changes to Odoo."""
        if self.id:
            await self._client.write(self.odoo_name, [self.id], self.model_dump())
        else:
            self.id = await self._client.create(self.odoo_name, self.model_dump())

    async def delete(self):
        """Delete record from Odoo."""
        if self.id:
            await self._client.unlink(self.odoo_name, [self.id])

Common Models

ResPartner

Partner/Customer model with comprehensive field definitions.

from zenoo_rpc.models.common import ResPartner
from zenoo_rpc.models.fields import Many2OneField

class ResPartner(OdooModel):
    odoo_name: ClassVar[str] = "res.partner"

    # Basic fields
    id: Optional[int] = None
    name: str
    email: Optional[str] = None
    phone: Optional[str] = None
    mobile: Optional[str] = None
    website: Optional[str] = None

    # Boolean fields
    is_company: bool = False
    active: bool = True

    # Ranking fields
    customer_rank: int = 0
    supplier_rank: int = 0

    # Address fields
    street: Optional[str] = None
    street2: Optional[str] = None
    city: Optional[str] = None
    zip: Optional[str] = None

    # Relationship fields
    country_id: Optional[Many2OneField["ResCountry"]] = None
    state_id: Optional[Many2OneField["ResCountryState"]] = None
    parent_id: Optional[Many2OneField["ResPartner"]] = None

    # Computed properties
    @property
    def is_customer(self) -> bool:
        """Check if partner is a customer."""
        return self.customer_rank > 0

    @property
    def is_supplier(self) -> bool:
        """Check if partner is a supplier."""
        return self.supplier_rank > 0

    @property
    def display_name(self) -> str:
        """Get display name for partner."""
        return self.name or f"Partner #{self.id}"

Usage Examples:

# Create partner instance
partner = ResPartner(
    name="ACME Corporation",
    email="contact@acme.com",
    is_company=True,
    customer_rank=1
)

# Access computed properties
if partner.is_customer:
    print(f"{partner.display_name} is a customer")

# Validate data
try:
    invalid_partner = ResPartner(name="", email="invalid-email")
except ValidationError as e:
    print(f"Validation failed: {e}")

ResCountry

Country model for address management.

from zenoo_rpc.models.common import ResCountry

class ResCountry(OdooModel):
    odoo_name: ClassVar[str] = "res.country"

    id: Optional[int] = None
    name: str
    code: str  # ISO country code
    phone_code: Optional[int] = None
    currency_id: Optional[Many2OneField["ResCurrency"]] = None

ResUsers

User model for authentication and permissions.

from zenoo_rpc.models.common import ResUsers

class ResUsers(OdooModel):
    odoo_name: ClassVar[str] = "res.users"

    id: Optional[int] = None
    name: str
    login: str
    email: Optional[str] = None
    active: bool = True

    # Relationship to partner
    partner_id: Optional[Many2OneField["ResPartner"]] = None

    # Groups and permissions
    groups_id: Optional[Many2ManyField["ResGroups"]] = None

Field Types

Many2OneField

Represents a many-to-one relationship.

from zenoo_rpc.models.fields import Many2OneField

class OrderLine(OdooModel):
    odoo_name: ClassVar[str] = "sale.order.line"

    order_id: Many2OneField["SaleOrder"]
    product_id: Many2OneField["ProductProduct"]

    async def get_order(self) -> "SaleOrder":
        """Get related order."""
        return await self.order_id

    async def get_product(self) -> "ProductProduct":
        """Get related product."""
        return await self.product_id

One2ManyField

Represents a one-to-many relationship.

from zenoo_rpc.models.fields import One2ManyField

class SaleOrder(OdooModel):
    odoo_name: ClassVar[str] = "sale.order"

    name: str
    order_line: One2ManyField["SaleOrderLine"]

    async def get_lines(self) -> List["SaleOrderLine"]:
        """Get all order lines."""
        return await self.order_line

Many2ManyField

Represents a many-to-many relationship.

from zenoo_rpc.models.fields import Many2ManyField

class ResPartner(OdooModel):
    odoo_name: ClassVar[str] = "res.partner"

    name: str
    category_id: Many2ManyField["ResPartnerCategory"]

    async def get_categories(self) -> List["ResPartnerCategory"]:
        """Get all partner categories."""
        return await self.category_id

SelectionField

Represents a selection field with predefined choices.

from zenoo_rpc.models.fields import SelectionField

class SaleOrder(OdooModel):
    odoo_name: ClassVar[str] = "sale.order"

    state: SelectionField = SelectionField(
        choices=[
            ("draft", "Draft"),
            ("sent", "Quotation Sent"),
            ("sale", "Sales Order"),
            ("done", "Locked"),
            ("cancel", "Cancelled")
        ],
        default="draft"
    )

DateField and DateTimeField

Date and datetime fields with proper Python types.

from zenoo_rpc.models.fields import DateField, DateTimeField
from datetime import date, datetime

class SaleOrder(OdooModel):
    odoo_name: ClassVar[str] = "sale.order"

    date_order: DateTimeField
    commitment_date: Optional[DateField] = None

    @property
    def is_recent(self) -> bool:
        """Check if order is from last 30 days."""
        if not self.date_order:
            return False
        return (datetime.now() - self.date_order).days <= 30

Model Registry

register_model()

Register a custom model with the registry.

from zenoo_rpc.models.registry import register_model

@register_model
class CustomModel(OdooModel):
    odoo_name: ClassVar[str] = "custom.model"

    name: str
    description: Optional[str] = None

get_model_class()

Get a model class by Odoo model name.

from zenoo_rpc.models.registry import get_model_class

# Get registered model class
PartnerClass = get_model_class("res.partner")
if PartnerClass:
    partner = PartnerClass(name="Test Partner")

Relationship Management

Lazy Loading

Relationships are loaded lazily when accessed.

# Partner with country relationship
partner = await client.model(ResPartner).filter(id=1).first()

# Country is loaded when accessed
country = await partner.country_id  # Triggers database query
print(country.name)

# Subsequent access uses cached value
country_again = await partner.country_id  # No database query

Prefetching

Load relationships efficiently with prefetching.

# Prefetch related data
partners = await client.model(ResPartner).filter(
    is_company=True
).prefetch_related("country_id", "state_id").all()

# No additional queries needed
for partner in partners:
    country = await partner.country_id  # Already loaded
    state = await partner.state_id      # Already loaded

Validation

Field Validation

Models automatically validate field values using Pydantic.

try:
    # This will raise ValidationError
    partner = ResPartner(
        name="",  # Empty name
        email="invalid-email",  # Invalid email format
        customer_rank=-1  # Negative rank
    )
except ValidationError as e:
    for error in e.errors():
        print(f"Field {error['loc']}: {error['msg']}")

Custom Validators

Add custom validation logic to models.

from pydantic import validator

class ResPartner(OdooModel):
    odoo_name: ClassVar[str] = "res.partner"

    name: str
    email: Optional[str] = None
    phone: Optional[str] = None

    @validator('name')
    def name_must_not_be_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('Name cannot be empty')
        return v.strip()

    @validator('email')
    def email_must_be_valid(cls, v):
        if v and '@' not in v:
            raise ValueError('Invalid email format')
        return v

    @validator('phone')
    def phone_must_be_valid(cls, v):
        if v and len(v) < 10:
            raise ValueError('Phone number too short')
        return v

Serialization

JSON Serialization

Models can be serialized to/from JSON.

# Create partner
partner = ResPartner(
    name="ACME Corp",
    email="contact@acme.com",
    is_company=True
)

# Serialize to JSON
json_data = partner.json()
print(json_data)

# Serialize to dict
dict_data = partner.model_dump()
print(dict_data)

# Create from JSON
partner_from_json = ResPartner.parse_raw(json_data)

# Create from dict
partner_from_dict = ResPartner(**dict_data)

Odoo Format

Convert between Zenoo models and Odoo data format.

# From Odoo data
odoo_data = {
    "id": 1,
    "name": "ACME Corp",
    "email": "contact@acme.com",
    "is_company": True,
    "country_id": [1, "United States"]  # Odoo format
}

partner = ResPartner.from_odoo_data(odoo_data)

# To Odoo format
odoo_format = partner.to_odoo_data()

Advanced Usage

Dynamic Models

Create models dynamically for custom Odoo models.

from zenoo_rpc.models.base import create_dynamic_model

# Create model for custom Odoo model
CustomModel = create_dynamic_model(
    "custom.model",
    {
        "name": str,
        "description": Optional[str],
        "active": bool
    }
)

# Use like any other model
instance = CustomModel(name="Test", active=True)

Model Inheritance

Extend existing models with additional functionality.

class ExtendedPartner(ResPartner):
    """Extended partner with additional methods."""

    def get_full_address(self) -> str:
        """Get formatted full address."""
        parts = [
            self.street,
            self.street2,
            self.city,
            self.zip
        ]
        return ", ".join(filter(None, parts))

    async def get_orders(self, client: ZenooClient) -> List["SaleOrder"]:
        """Get all orders for this partner."""
        if not self.id:
            return []

        return await client.model(SaleOrder).filter(
            partner_id=self.id
        ).all()

Performance Considerations

Field Selection

Only load needed fields to improve performance.

# Load only specific fields
partners = await client.model(ResPartner).only(
    "name", "email", "phone"
).filter(is_company=True).all()

Batch Loading

Use batch operations for multiple model instances.

# Batch create
partners_data = [
    {"name": "Company 1", "email": "c1@test.com"},
    {"name": "Company 2", "email": "c2@test.com"}
]

created_ids = await client.batch_manager.bulk_create(
    model="res.partner",
    records=partners_data
)

Caching

Cache frequently accessed model data.

# Cache model queries
countries = await client.model(ResCountry).cache(
    key="all_countries",
    ttl=3600
).all()

Next Steps