Creating DocTypes
DocTypes are the core building blocks of Framework M applications. Each DocType defines a data model with fields, validation, and business logic.
File Structure
Every DocType lives in its own directory under the app's namespace:
src/<app_name>/doctypes/invoice/
├── __init__.py # Exports
├── doctype.py # Schema (fields, types)
└── controller.py # Business logic
tests/doctypes/invoice/
└── test_invoice.py # Tests
Defining a DocType
A DocType is a Pydantic model inheriting from BaseDocType:
# doctype.py
from __future__ import annotations
from datetime import date
from decimal import Decimal
from typing import ClassVar
from pydantic import Field
from framework_m import DocType
class Invoice(DocType):
"""Invoice DocType."""
__doctype_name__: ClassVar[str] = "Invoice"
# Required fields
customer: str = Field(description="Customer name")
invoice_date: date = Field(description="Invoice date")
# Optional fields with defaults
total: Decimal = Field(default=Decimal("0"), description="Total amount")
status: str = Field(default="Draft", description="Invoice status")
notes: str = Field(default="", description="Additional notes")
class Meta:
"""DocType configuration."""
naming_rule: ClassVar[str] = "autoincrement"
is_submittable: ClassVar[bool] = True
Field Types
Framework M supports these Python types:
| Python Type | Description | Example |
|---|---|---|
str | Text | name: str |
int | Integer | quantity: int |
float | Decimal number | price: float |
Decimal | Precise decimal | amount: Decimal |
bool | Boolean | is_active: bool |
date | Date only | due_date: date |
datetime | Date and time | created_at: datetime |
list[str] | List of strings | tags: list[str] |
Optional Fields
Use | None for optional fields:
email: str | None = Field(default=None, description="Email address")
due_date: date | None = Field(default=None, description="Due date")
Field Options
The Field() function supports these options:
from pydantic import Field
name: str = Field(
description="Customer name", # Help text
min_length=1, # Minimum length
max_length=100, # Maximum length
)
quantity: int = Field(
default=1, # Default value
ge=0, # Greater than or equal
le=1000, # Less than or equal
)
Meta Options
The Meta class configures DocType behavior:
class Meta:
# Naming
naming_rule: ClassVar[str] = "autoincrement" # or "uuid", "field:customer"
# Behavior
is_submittable: ClassVar[bool] = True # Can be submitted/cancelled
is_child_table: ClassVar[bool] = False # Is a child table
# Permissions
permissions: ClassVar[dict] = {
"read": ["Employee", "Manager"],
"write": ["Manager"],
"submit": ["Manager"],
}
Naming Rules
| Rule | Description | Example |
|---|---|---|
autoincrement | Sequential numbers | 1, 2, 3 |
uuid | Random UUID | abc123-def456 |
field:customer | Use field value | Customer name |
Controller Hooks
Business logic goes in controller.py. Hooks run at specific lifecycle points:
# controller.py
from __future__ import annotations
from framework_m import BaseController
from .doctype import Invoice
class InvoiceController(BaseController[Invoice]):
"""Invoice business logic."""
async def validate(self, context=None):
"""Validate before save. Raise exception to abort."""
if self.doc.total < 0:
raise ValueError("Total cannot be negative")
async def before_save(self, context=None):
"""Called before saving to database."""
self.doc.status = "Pending" if self.doc.total > 0 else "Draft"
async def after_save(self, context=None):
"""Called after saving to database."""
# Send notification, update related records, etc.
pass
async def on_submit(self, context=None):
"""Called when document is submitted (for submittable DocTypes)."""
if self.doc.total == 0:
raise ValueError("Cannot submit zero-value invoice")
# Create ledger entries, send emails, etc.
async def on_cancel(self, context=None):
"""Called when document is cancelled."""
# Reverse ledger entries, cleanup, etc.
pass
Hook Order
On Create:
validate → before_save → [DB INSERT] → after_save
On Update:
validate → before_save → [DB UPDATE] → after_save
On Submit (submittable only):
validate → before_save → [DB UPDATE] → after_save → on_submit
On Cancel (submittable only):
on_cancel → before_save → [DB UPDATE] → after_save
Child Tables
For one-to-many relationships, use child tables:
# doctype.py
class InvoiceItem(BaseDocType):
"""Invoice line item (child table)."""
__doctype_name__: ClassVar[str] = "InvoiceItem"
item_name: str = Field(description="Item name")
quantity: int = Field(default=1, description="Quantity")
rate: Decimal = Field(description="Unit price")
amount: Decimal = Field(default=Decimal("0"), description="Line total")
class Meta:
is_child_table: ClassVar[bool] = True
class Invoice(BaseDocType):
"""Invoice with line items."""
__doctype_name__: ClassVar[str] = "Invoice"
customer: str = Field(description="Customer")
items: list[InvoiceItem] = Field(default_factory=list, description="Line items")
total: Decimal = Field(default=Decimal("0"), description="Total")
Calculate totals in the controller:
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)
Indexes
Add database indexes for frequently queried fields:
class Meta:
indexes: ClassVar[list] = [
("customer",), # Single column
("status", "date"), # Composite
]
Next Steps
- Using the Desk - Work with DocTypes in the UI
- Getting Started - Create your first app