Skip to main content

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