Skip to main content

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}")

Developer Reference / API

Commonly Used Hooks:

Method Description
validate() Data validation logic
before_save() Manipulate data before DB commit
after_save() Execute post-insert operations
on_submit() Run logic on submission (e.g., status updates, logs)
on_cancel() Reverse entries, prevent action
on_trash() Log deletion or perform backups

These are executed automatically by the Frappe Document API, found in:
frappe/model/document.py

Practical Use Case

Scenario: In a retail ERP, stock movement entries need to be recorded when a Sales Invoice is submitted.

  1. You override on_submit() in Sales Invoice DocType
  2. Write logic to create corresponding Stock Ledger Entries
  3. Use frappe.new_doc() and insert() within the controller

def on_submit(self):
sle = frappe.new_doc("Stock Ledger Entry")
sle.item_code = self.item
sle.qty = -self.qty
sle.save()

This automates real-time inventory tracking without writing separate scripts or manual triggers.

Advanced Features

  • Dynamic Method Injection: You can define custom methods and call them in client scripts or from REST API.
  • Multiple Inheritance: Use mixins or utility classes to manage complex controller logic.
  • Conditional Lifecycle Hooks: Add logic that checks workflow state, user roles, or permissions before executing.

Tips, Best Practices & Pitfalls

Best Practices:

  1. Keep each method single-responsibility (e.g., no DB writes in validate()).
  2. Use frappe.throw() for blocking validation errors; frappe.msgprint() for user messages.
  3. Use controller logic over client-side JS for security and consistency.

Common Pitfalls:

  • Forgetting to run bench migrate after adding the controller = no execution.
  • Calling frappe.db.commit() inside controller = breaks transaction chain.
  • Defining functions outside the class = they won’t auto-execute.

v15 Notes:

  • Enhanced internal error reporting inside lifecycle methods
  • Improved logging behavior across controller events
  • Controller inheritance patterns are better documented

Visual Suggestion

  • Diagram: Document Lifecycle (Draft → Save → Submit → Cancel → Delete)
  • Screenshot: System messages triggered by validate() and on_submit()
  • Code-map: book.py controller showing overridden methods

Conclusion

Controller methods in Frappe Framework v15 are essential for building intelligent, process-driven ERP solutions. They empower developers to tightly bind logic to document lifecycles, ensuring business rules are consistently enforced and actions are automated.

Click to rate this post!
[Total: 1 Average: 5]