Field Types API Reference¶
Specialized field types for Odoo models with proper validation, serialization, and relationship handling using Pydantic field types that correspond to Odoo field types.
Overview¶
Field types provide:
- Type Safety: Full type hints and validation
- Odoo Compatibility: Direct mapping to Odoo field types
- Relationship Support: Lazy loading and relationship descriptors
- Validation: Automatic data validation with Pydantic
- Serialization: JSON serialization/deserialization
Relationship Fields¶
Many2OneField¶
Creates a many-to-one relationship field for single record references.
def Many2OneField(
model_name: str,
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a Many2one field for Odoo relationships."""
Parameters:
model_name(str): Target Odoo model name (e.g., "res.partner")description(str): Field description**kwargs: Additional field parameters
Returns: FieldInfo configured for Many2one relationships
Example:
from zenoo_rpc.models.fields import Many2OneField
from typing import Optional, Union
class SaleOrder(OdooModel):
_odoo_name: ClassVar[str] = "sale.order"
# Many2one relationship to partner
partner_id: Optional[Union[int, "ResPartner"]] = Many2OneField(
"res.partner",
description="Customer"
)
# Many2one with additional constraints
company_id: Optional[Union[int, "ResCompany"]] = Many2OneField(
"res.company",
description="Company",
required=True
)
# Access relationship
async def get_customer(self) -> Optional["ResPartner"]:
"""Get the customer partner."""
return await self.partner_id
One2ManyField¶
Creates a one-to-many relationship field for collections of related records.
def One2ManyField(
model_name: str,
inverse_field: str,
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a One2many field for Odoo relationships."""
Parameters:
model_name(str): Target Odoo model nameinverse_field(str): Field name in target model that points backdescription(str): Field description**kwargs: Additional field parameters
Example:
from zenoo_rpc.models.fields import One2ManyField
from typing import List
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# One2many relationship to child partners
child_ids: List["ResPartner"] = One2ManyField(
"res.partner",
"parent_id",
description="Child companies"
)
# One2many to orders
sale_order_ids: List["SaleOrder"] = One2ManyField(
"sale.order",
"partner_id",
description="Sales orders"
)
# Access related records
async def get_children(self) -> List["ResPartner"]:
"""Get all child partners."""
return await self.child_ids
async def get_orders(self) -> List["SaleOrder"]:
"""Get all sales orders."""
return await self.sale_order_ids
Many2ManyField¶
Creates a many-to-many relationship field for multiple record references.
def Many2ManyField(
model_name: str,
relation_table: Optional[str] = None,
description: str = "",
**kwargs: Any,
) -> FieldInfo:
"""Create a Many2many field for Odoo relationships."""
Parameters:
model_name(str): Target Odoo model namerelation_table(str, optional): Relation table namedescription(str): Field description**kwargs: Additional field parameters
Example:
from zenoo_rpc.models.fields import Many2ManyField
from typing import List
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# Many2many relationship to categories
category_id: List["ResPartnerCategory"] = Many2ManyField(
"res.partner.category",
description="Partner categories"
)
# Many2many with custom relation table
tag_ids: List["ResPartnerTag"] = Many2ManyField(
"res.partner.tag",
relation_table="partner_tag_rel",
description="Partner tags"
)
# Access related records
async def get_categories(self) -> List["ResPartnerCategory"]:
"""Get all partner categories."""
return await self.category_id
async def add_category(self, category_id: int):
"""Add a category to this partner."""
current_categories = await self.category_id
if category_id not in [cat.id for cat in current_categories]:
# Update through client
await self._client.write(
"res.partner",
[self.id],
{"category_id": [(4, category_id)]}
)
Basic Field Types¶
CharField¶
Short text field with optional length constraint.
def CharField(
max_length: Optional[int] = None,
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a Char field for short text content."""
Example:
from zenoo_rpc.models.fields import CharField
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# Basic char field
name: str = CharField(description="Partner name")
# Char field with length constraint
ref: Optional[str] = CharField(
max_length=50,
description="Internal reference"
)
# Email field (special char field)
email: Optional[str] = CharField(
max_length=254,
description="Email address"
)
# Phone field
phone: Optional[str] = CharField(
max_length=32,
description="Phone number"
)
TextField¶
Long text field for multi-line content.
def TextField(description: str = "", **kwargs: Any) -> FieldInfo:
"""Create a Text field for long text content."""
Example:
from zenoo_rpc.models.fields import TextField
class ProductTemplate(OdooModel):
_odoo_name: ClassVar[str] = "product.template"
# Long text field
description: Optional[str] = TextField(
description="Product description"
)
# Notes field
note: Optional[str] = TextField(
description="Internal notes"
)
BooleanField¶
Boolean field for true/false values.
Example:
from zenoo_rpc.models.fields import BooleanField
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# Boolean fields
active: bool = BooleanField(description="Is record active")
is_company: bool = BooleanField(description="Is a company")
customer_rank: int = IntegerField(description="Customer rank")
# Computed property using boolean field
@property
def is_customer(self) -> bool:
"""Check if partner is a customer."""
return self.customer_rank > 0
IntegerField¶
Integer field for whole numbers.
Example:
from zenoo_rpc.models.fields import IntegerField
class ProductTemplate(OdooModel):
_odoo_name: ClassVar[str] = "product.template"
# Integer fields
sequence: int = IntegerField(description="Display sequence")
sale_delay: int = IntegerField(description="Customer lead time")
# Ranking fields
customer_rank: int = IntegerField(description="Customer rank")
supplier_rank: int = IntegerField(description="Vendor rank")
FloatField¶
Float field with optional precision control.
def FloatField(
digits: Optional[Tuple[int, int]] = None,
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a Float field with precision control."""
Parameters:
digits(Tuple[int, int], optional): (precision, scale) for decimal places
Example:
from zenoo_rpc.models.fields import FloatField
class ProductTemplate(OdooModel):
_odoo_name: ClassVar[str] = "product.template"
# Basic float field
list_price: float = FloatField(description="Sales price")
# Float field with precision
weight: float = FloatField(
digits=(16, 3),
description="Product weight in kg"
)
# Volume with high precision
volume: float = FloatField(
digits=(16, 6),
description="Product volume in m³"
)
Specialized Field Types¶
SelectionField¶
Selection field for predefined choices.
def SelectionField(
choices: List[Tuple[str, str]],
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a Selection field for Odoo choice fields."""
Parameters:
choices(List[Tuple[str, str]]): List of (value, label) tuples
Example:
from zenoo_rpc.models.fields import SelectionField
class SaleOrder(OdooModel):
_odoo_name: ClassVar[str] = "sale.order"
# Selection field with choices
state: str = SelectionField([
("draft", "Quotation"),
("sent", "Quotation Sent"),
("sale", "Sales Order"),
("done", "Locked"),
("cancel", "Cancelled")
], description="Order status")
# Priority selection
priority: str = SelectionField([
("0", "Normal"),
("1", "Low"),
("2", "High"),
("3", "Very High")
], description="Priority")
# Check state
@property
def is_confirmed(self) -> bool:
"""Check if order is confirmed."""
return self.state in ["sale", "done"]
DateField¶
Date field for date-only values.
def DateField(description: str = "", **kwargs: Any) -> FieldInfo:
"""Create a Date field with proper validation."""
Example:
from zenoo_rpc.models.fields import DateField
from datetime import date
class SaleOrder(OdooModel):
_odoo_name: ClassVar[str] = "sale.order"
# Date fields
date_order: Optional[date] = DateField(description="Order date")
commitment_date: Optional[date] = DateField(description="Delivery date")
# Computed properties
@property
def is_overdue(self) -> bool:
"""Check if order is overdue."""
if not self.commitment_date:
return False
return self.commitment_date < date.today()
DateTimeField¶
DateTime field for timestamp values.
def DateTimeField(description: str = "", **kwargs: Any) -> FieldInfo:
"""Create a DateTime field with proper validation."""
Example:
from zenoo_rpc.models.fields import DateTimeField
from datetime import datetime
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# DateTime fields
create_date: Optional[datetime] = DateTimeField(description="Created on")
write_date: Optional[datetime] = DateTimeField(description="Last updated")
# Computed properties
@property
def age_in_days(self) -> int:
"""Get age in days since creation."""
if not self.create_date:
return 0
return (datetime.now() - self.create_date).days
MonetaryField¶
Monetary field for currency amounts.
def MonetaryField(
currency_field: str = "currency_id",
description: str = "",
**kwargs: Any
) -> FieldInfo:
"""Create a Monetary field for currency amounts."""
Parameters:
currency_field(str): Name of the currency field (default: "currency_id")
Example:
from zenoo_rpc.models.fields import MonetaryField, Many2OneField
from decimal import Decimal
class SaleOrder(OdooModel):
_odoo_name: ClassVar[str] = "sale.order"
# Currency field
currency_id: Optional["ResCurrency"] = Many2OneField(
"res.currency",
description="Currency"
)
# Monetary fields
amount_untaxed: Decimal = MonetaryField(description="Untaxed amount")
amount_tax: Decimal = MonetaryField(description="Tax amount")
amount_total: Decimal = MonetaryField(description="Total amount")
# Computed properties
@property
def tax_percentage(self) -> float:
"""Calculate tax percentage."""
if self.amount_untaxed == 0:
return 0.0
return float(self.amount_tax / self.amount_untaxed * 100)
BinaryField¶
Binary field for file/image data.
def BinaryField(description: str = "", **kwargs: Any) -> FieldInfo:
"""Create a Binary field for file/image data."""
Example:
from zenoo_rpc.models.fields import BinaryField
from typing import Optional
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
# Image fields
image_1920: Optional[bytes] = BinaryField(description="Image")
image_1024: Optional[bytes] = BinaryField(description="Image 1024")
image_512: Optional[bytes] = BinaryField(description="Image 512")
# Document attachment
attachment: Optional[bytes] = BinaryField(description="Attachment")
# Helper methods
def has_image(self) -> bool:
"""Check if partner has an image."""
return self.image_1920 is not None
def get_image_size(self) -> int:
"""Get image size in bytes."""
return len(self.image_1920) if self.image_1920 else 0
Field Descriptors¶
RelationshipDescriptor¶
Base descriptor for relationship fields with lazy loading.
class RelationshipDescriptor:
"""Base descriptor for relationship fields with lazy loading."""
def __init__(self, field_name: str, comodel_name: str, field_info: FieldInfo):
self.field_name = field_name
self.comodel_name = comodel_name
self.field_info = field_info
self.is_collection = False
Features:
- Lazy loading of related records
- Caching of loaded relationships
- Automatic relationship resolution
Many2OneDescriptor¶
Descriptor for Many2One relationships.
class Many2OneDescriptor(RelationshipDescriptor):
"""Descriptor for Many2One relationships."""
def __init__(self, field_name: str, comodel_name: str, field_info: FieldInfo):
super().__init__(field_name, comodel_name, field_info)
self.is_collection = False
One2ManyDescriptor¶
Descriptor for One2Many relationships.
class One2ManyDescriptor(RelationshipDescriptor):
"""Descriptor for One2Many relationships."""
def __init__(self, field_name: str, comodel_name: str, field_info: FieldInfo):
super().__init__(field_name, comodel_name, field_info)
self.is_collection = True
Many2ManyDescriptor¶
Descriptor for Many2Many relationships.
class Many2ManyDescriptor(RelationshipDescriptor):
"""Descriptor for Many2Many relationships."""
def __init__(self, field_name: str, comodel_name: str, field_info: FieldInfo):
super().__init__(field_name, comodel_name, field_info)
self.is_collection = True
Field Validation¶
Automatic Validation¶
Fields automatically validate data using Pydantic validators.
from pydantic import ValidationError
class ResPartner(OdooModel):
_odoo_name: ClassVar[str] = "res.partner"
name: str = CharField(description="Name")
email: Optional[str] = CharField(description="Email")
# Custom validator
@field_validator('email')
@classmethod
def validate_email(cls, v):
if v and '@' not in v:
raise ValueError('Invalid email format')
return v
# Usage
try:
partner = ResPartner(
name="Test Partner",
email="invalid-email" # Will raise ValidationError
)
except ValidationError as e:
print(f"Validation failed: {e}")
Custom Field Validation¶
from pydantic import field_validator
class ProductTemplate(OdooModel):
_odoo_name: ClassVar[str] = "product.template"
list_price: float = FloatField(description="Sales price")
cost_price: float = FloatField(description="Cost price")
@field_validator('list_price')
@classmethod
def validate_list_price(cls, v):
if v < 0:
raise ValueError('Price cannot be negative')
return v
@field_validator('cost_price')
@classmethod
def validate_cost_price(cls, v):
if v < 0:
raise ValueError('Cost cannot be negative')
return v
# Cross-field validation
def model_post_init(self, __context):
if self.list_price < self.cost_price:
raise ValueError('List price cannot be less than cost price')
Field Serialization¶
JSON Serialization¶
Fields automatically handle JSON serialization.
# Model with various field types
partner = ResPartner(
name="ACME Corp",
email="contact@acme.com",
is_company=True,
create_date=datetime.now(),
amount_total=Decimal("1000.50")
)
# Serialize to JSON
json_data = partner.model_dump_json()
print(json_data)
# Serialize to dict
dict_data = partner.model_dump()
print(dict_data)
# Create from JSON
partner_from_json = ResPartner.model_validate_json(json_data)
# Create from dict
partner_from_dict = ResPartner.model_validate(dict_data)
Odoo Format Conversion¶
Convert between Zenoo models and Odoo data format.
# From Odoo data format
odoo_data = {
"id": 1,
"name": "ACME Corp",
"email": "contact@acme.com",
"is_company": True,
"country_id": [1, "United States"], # Odoo many2one format
"category_id": [1, 2, 3] # Odoo many2many format
}
# Convert to model using Pydantic validation
partner = ResPartner.model_validate(odoo_data)
# Convert back to dict format
odoo_format = partner.model_dump()
Best Practices¶
1. Use Appropriate Field Types¶
# ✅ Good: Use specific field types
class Product(OdooModel):
name: str = CharField(max_length=100)
price: Decimal = MonetaryField()
weight: float = FloatField(digits=(16, 3))
active: bool = BooleanField()
# ❌ Avoid: Generic types without validation
class Product(OdooModel):
name: str
price: float # Should be MonetaryField
weight: str # Should be FloatField
active: int # Should be BooleanField
2. Add Field Descriptions¶
# ✅ Good: Descriptive field definitions
partner_id: Optional["ResPartner"] = Many2OneField(
"res.partner",
description="Customer or vendor"
)
# ❌ Avoid: Missing descriptions
partner_id: Optional["ResPartner"] = Many2OneField("res.partner")
3. Use Type Hints¶
# ✅ Good: Proper type hints
from typing import Optional, List, Union
class SaleOrder(OdooModel):
partner_id: Optional[Union[int, "ResPartner"]] = Many2OneField("res.partner")
line_ids: List["SaleOrderLine"] = One2ManyField("sale.order.line", "order_id")
# ❌ Avoid: Missing type hints
class SaleOrder(OdooModel):
partner_id = Many2OneField("res.partner")
line_ids = One2ManyField("sale.order.line", "order_id")
Next Steps¶
- Learn about Relationship Management for advanced relationship handling
- Explore Model Validation for custom validation patterns
- Check Model Serialization for data conversion techniques