Introduction
In Frappe Framework v15, controller methods are Python functions defined inside a DocType’s backend class that allow developers to hook into key document events. These methods enable powerful business logic execution at lifecycle events like validation, saving, submission, cancellation, and deletion.
This tutorial explains how to define and use controller methods to automate processes, enforce validations, and extend standard behaviors. It is especially valuable for Python developers, ERP consultants, and system integrators working on custom Frappe or ERPNext applications.
Core Sections
Overview
A DocType controller is a Python class (inheriting from frappe.model.document.Document) that contains methods which execute automatically during specific document events such as:
Event | Triggered When… |
---|---|
validate | Before saving a document |
before_save | Before database insert/update |
after_save | After database insert/update |
before_submit | Before the submit action completes |
on_submit | When a document is submitted |
on_cancel | When a submitted document is canceled |
on_trash | When a document is deleted |
Each of these methods can be overridden in your custom DocType’s controller file.
Installation / Setup
1. Navigate to your DocType
Suppose you have a custom DocType named Book inside your app library_management.
Your folder structure will look like:
apps/library_management/library_management/library_management/doctype/book/
├── book.py # Controller file
├── book.json # DocType definition
└── __init__.py
2. Define the Class in book.py
import frappe
from frappe.model.document import Document
class Book(Document):
def validate(self):
if not self.isbn:
frappe.throw("ISBN must be provided for all books.")
def before_save(self):
frappe.msgprint("Book is about to be saved!")
def on_submit(self):
frappe.msgprint(f"The book '{self.title}' has been submitted.")
def on_cancel(self):
frappe.msgprint(f"The book '{self.title}' has been canceled.")
After saving, run:
bench --site site1.local migrate
Configuration
No additional configuration is required in the UI. Simply ensure that:
- Your controller class is named the same as your DocType’s internal name.
- It inherits from Document.
- You place the file in the correct path.
Each method corresponds to a specific lifecycle event in the document’s flow.
Functional Use
Let’s say you want to prevent duplicate book entries by ISBN:
def validate(self):
if frappe.db.exists("Book", {"isbn": self.isbn, "name": ["!=", self.name]}):
frappe.throw("A book with this ISBN already exists.")
You could also auto-populate a field during submission:
def on_submit(self):
self.status = "Issued"
And to track deleted logs:
def on_trash(self):
frappe.logger().info(f"Book {self.name} deleted by {frappe.session.user}")