Skip to main content

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 TypeDescriptionExample
strTextname: str
intIntegerquantity: int
floatDecimal numberprice: float
DecimalPrecise decimalamount: Decimal
boolBooleanis_active: bool
dateDate onlydue_date: date
datetimeDate and timecreated_at: datetime
list[str]List of stringstags: 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

RuleDescriptionExample
autoincrementSequential numbers1, 2, 3
uuidRandom UUIDabc123-def456
field:customerUse field valueCustomer 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