Defining DocTypes
Advanced patterns for defining DocTypes in Framework M.
Introduction to DocTypes
In Framework M, a DocType is both a data schema (Pydantic model) and a database table definition. All DocTypes should inherit from DocType (an alias for BaseDocType).
from framework_m import DocType, Field
class MyDocType(DocType):
"""DocType definition."""
# fields...
Field Patterns
Required vs Optional
# Required - no default
name: str = Field(description="Customer name")
# Optional - with default
email: str = Field(default="", description="Email")
# Nullable optional
phone: str | None = Field(default=None, description="Phone")
Unique Fields
To enforce a unique constraint at the database level:
# Email must be unique
email: str = Field(json_schema_extra={"unique": True})
# Code must be unique
code: str = Field(min_length=3, json_schema_extra={"unique": True})
Field Validation
from framework_m import Field
# String constraints
code: str = Field(min_length=3, max_length=20)
# Number constraints
quantity: int = Field(ge=0, le=1000) # >= 0, <= 1000
price: Decimal = Field(gt=0) # > 0
# Pattern matching
email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
Common Types
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID
# Dates
due_date: date = Field(description="Due date")
created_at: datetime = Field(description="Created timestamp")
# Money
amount: Decimal = Field(decimal_places=2, description="Amount")
# References
related_id: UUID = Field(description="Related document ID")
Child Tables
For one-to-many relationships:
class InvoiceItem(DocType):
"""Invoice line item."""
__doctype_name__: ClassVar[str] = "InvoiceItem"
item_code: str = Field(description="Item code")
quantity: int = Field(default=1, ge=1)
rate: Decimal = Field(description="Unit price")
amount: Decimal = Field(default=Decimal("0"))
class Meta:
is_child_table: ClassVar[bool] = True
class Invoice(DocType):
"""Invoice with line items."""
__doctype_name__: ClassVar[str] = "Invoice"
customer: str = Field(description="Customer name")
items: list[InvoiceItem] = Field(default_factory=list)
total: Decimal = Field(default=Decimal("0"))
Calculate totals in controller:
from framework_m import Controller
class InvoiceController(Controller[Invoice]):
async def before_save(self, context=None):
for item in self.doc.items:
item.amount = item.quantity * item.rate
self.doc.total = sum(i.amount for i in self.doc.items)
Meta Options
class Invoice(DocType):
# ... fields ...
class Meta:
# Naming
naming_rule: ClassVar[str] = "autoincrement"
# Options: "autoincrement", "uuid", "field:customer"
# Behavior
is_submittable: ClassVar[bool] = True
is_child_table: ClassVar[bool] = False
# API exposure
api_resource: ClassVar[bool] = True
# Database
table_name: ClassVar[str] = "invoice" # Custom table name
# Permissions
permissions: ClassVar[dict] = {
"read": ["Employee", "Manager"],
"write": ["Manager"],
"submit": ["Manager"],
"delete": ["Administrator"],
}
Submittable Documents
For documents with approval workflow:
class PurchaseOrder(DocType):
__doctype_name__: ClassVar[str] = "PurchaseOrder"
supplier: str = Field(description="Supplier")
total: Decimal = Field(description="Total amount")
docstatus: int = Field(default=0) # 0=Draft, 1=Submitted, 2=Cancelled
class Meta:
is_submittable: ClassVar[bool] = True
Controller hooks for submission:
from framework_m import Controller
class PurchaseOrderController(Controller[PurchaseOrder]):
async def on_submit(self, context=None):
"""Called when document is submitted."""
# Create ledger entries, reserve inventory, etc.
if self.doc.total > 10000:
raise ValueError("Orders over 10000 require approval")
async def on_cancel(self, context=None):
"""Called when document is cancelled."""
# Reverse ledger entries, release inventory, etc.
pass
Custom Validators
For complex validation:
from pydantic import field_validator, model_validator
class Order(DocType):
start_date: date
end_date: date
quantity: int
@field_validator("quantity")
@classmethod
def validate_quantity(cls, v):
if v <= 0:
raise ValueError("Quantity must be positive")
return v
@model_validator(mode="after")
def validate_dates(self):
if self.end_date < self.start_date:
raise ValueError("End date must be after start date")
return self
Database Indexes
For query performance:
class Meta:
indexes: ClassVar[list] = [
("status",), # Single column
("customer", "date"), # Composite
("owner", "creation"), # For RLS queries
]
Linking DocTypes
Reference other DocTypes:
class Invoice(DocType):
# Simple reference (stores ID)
customer_id: UUID = Field(description="Customer")
# Reference with name (for display)
customer_name: str = Field(default="", description="Customer name")
In controller, fetch related data:
async def before_save(self, context=None):
if self.doc.customer_id:
customer = await customer_repo.get(self.doc.customer_id)
if customer:
self.doc.customer_name = customer.name
Next Steps
- Creating Apps - Build complete applications
- Architecture - System overview