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¶
- Explore Query Building for advanced querying
- Learn about Field Types in detail
- Check Relationship Management patterns