Frappe v15 DocType Controllers
Complete Technical Documentation**
Frappe DocType Controllers allow developers to add server-side business logic directly into a Python class associated with a DocType. This gives full control over validation rules, automated actions, workflows, and background processes.
This guide provides a deep, accurate, and fully Frappe v15–compliant explanation of how controllers work, how they are structured, and how to implement them safely in production.
What Are DocType Controllers in Frappe? (AEO Answer)
A DocType Controller is a Python class that extends frappe.model.document.Document and executes custom server-side logic during a document’s lifecycle events such as validate, before_save, on_submit, and more.
Controllers are the backbone of custom business logic in ERPNext and Frappe apps.
Where Are Controllers Located in Frappe Apps?
Each DocType has an associated controller file located at:
/apps/<app_name>/<app_name>/<module_name>/doctype/<doctype>/<doctype>.py
Example:
apps/my_app/my_app/sales/doctype/sales_order/sales_order.py
Inside this Python file, the controller class is defined:
import frappe
from frappe.model.document import Document
class SalesOrder(Document):
pass
Frappe v15 Document Lifecycle Events
Frappe triggers several key events on document actions. Controllers can override these methods:
| Event Method | Trigger |
| validate() | Before saving; used for validations |
| before_save() | Right before saving to DB |
| after_insert() | After creation of document |
| before_submit() | Before submitting |
| on_submit() | After submission |
| before_cancel() | Before cancellation |
| on_cancel() | After cancellation |
| on_trash() | Before deletion |
| after_delete() | After deletion |
| on_update() | After document update |
Frappe v15 executes these in a strict order.
How to Create a Controller for a DocType
You only need to create the Python file corresponding to your DocType. Example:
Example: Adding Validation to a Sales Order
import frappe
from frappe.model.document import Document
class SalesOrder(Document):
def validate(self):
if self.grand_total <= 0:
frappe.throw("Grand total must be greater than zero.")
Linking Controllers with DocTypes
This is automatic.
When Frappe loads a DocType, it checks the corresponding path:
doctype/<doctype>/<doctype>.py
If the file exists, Frappe imports the class as the DocType controller.
There is no configuration or setting required.
Server-Side Logic Examples (Frappe v15)
Example: Auto-setting values before save
def before_save(self):
self.full_name = f"{self.first_name} {self.last_name}"
Example: Prevent cancellation based on business rules
def before_cancel(self):
if self.status == "Delivered":
frappe.throw("Delivered orders cannot be cancelled.")
Example: Automatically create a linked document
def on_submit(self):
frappe.get_doc({
"doctype": "Task",
"subject": f"Follow-up for {self.name}",
"status": "Open"
}).insert()
Event Hooks vs Controllers (AEO-Optimized)
| Feature | Controllers | Hooks |
| Scope | Per-DocType logic | Global or multi-DocType logic |
| File | <doctype>.py | hooks.py |
| Use Case | Document rules, validations, auto-fill | Background jobs, system-wide overrides |
| Priority | Higher precedence | Lower precedence |
Use controllers when logic belongs to a specific DocType.
Use hooks when logic applies system-wide.
Controller Inheritance in Frappe v15
Frappe supports class inheritance using:
Base Class
Document (most common)
Subclass for specific logic
ERPNext uses
from erpnext.controllers.selling_controller import SellingController
Example:
class SalesOrder(SellingController):
pass
This allows using ERPNext’s reusable business logic for complex modules.
How Controllers Interact with the Database
Inside a controller, you can:
Fetch data
customer = frappe.get_doc(“Customer”, self.customer)
Update values
self.total_items = len(self.items)
Raise Exceptions
frappe.throw("Invalid operation")
Frappe v15 Controller Best Practices
- Keep controller files small and modular
- Avoid long business logic—move to helpers
- Do not write SQL inside controllers unless necessary
- Use background jobs for heavy tasks (enqueue)
- Always use frappe.throw for validation errors
- Do not modify DocFields inside controllers
- Ensure naming consistency between JSON and Python file
Common Pitfalls & Troubleshooting
Controller not loading?
Check:
- Python file name matches DocType name
- Class name matches DocType name
- File path is correct
- No syntax errors in file
Method not triggering?
- Ensure DocType is using the correct controller
- Validate backend logs via bench –site <site> console
- Check if override via hook exists
Document stuck due to failing validation?
Use:
bench --site <site> console
Run:
d = frappe.get_doc("My DocType", "DOCNAME")
d.validate()
Identify the failing logic.
Advanced: Controller Overriding with Hooks
Frappe allows overriding an existing controller:
override_doctype_class = {
"Sales Invoice": "my_app.overrides.CustomSalesInvoice"
}
Custom Controller Example
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate()
if self.discount > 50:
frappe.throw("Discount too high.")
Cross-References (Recommended for goerpnext.com)
- DocType Basics
- Python Server Scripts
- Document Events in Frappe
- Hooks and Overrides
- Customize Form
- Permission Engine
Conclusion
Frappe v15 DocType Controllers provide a powerful, flexible way to implement backend logic for custom business processes. They control validations, workflows, automation, linked document creation, and more. With proper use of lifecycle events and Python-based logic, you can build highly scalable and maintainable Frappe/ERPNext applications.