Skip to main content

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.

Click to rate this post!
[Total: 0 Average: 0]