Controller Methods in Frappe Framework (Version 15)
Introduction — Understanding Controller Methods in Frappe
In the Frappe Framework, every DocType is backed by a controller class in Python.
This controller defines how the document behaves — including validation, permission logic, event handling, and workflow automation.
These controller methods act as document event hooks, triggered automatically during various lifecycle stages (such as creation, saving, submission, or deletion).
Understanding these methods allows developers to customize document behavior and implement business logic efficiently within Frappe v15.
Purpose of Controller Methods
Controller methods let developers inject Python logic into the DocType’s life cycle.
When a user creates, saves, submits, or cancels a document, Frappe automatically executes specific methods from the controller file (<doctype_name>.py).
Typical use cases include:
- Validating input data before saving.
- Auto-generating linked records.
- Enforcing business rules before submission.
- Updating status fields when cancelling or amending documents.
Controller File Structure
When you create a DocType using:
bench make-doctype "Task Tracker"
Frappe automatically creates a Python controller file inside your app:
my_app/
└── my_app/
└── doctype/
└── task_tracker/
├── task_tracker.json
├── task_tracker.py ← Controller file
└── task_tracker.js
The controller defines a Python class that inherits from
frappe.model.document.Document:
import frappe
from frappe.model.document import Document
class TaskTracker(Document):
pass
You can override or extend built-in event methods in this class.
Commonly Used Controller Methods in Frappe v15
1. validate (self)
Executed before a document is saved (either new or updated).
Used for data validation or enforcing business logic.
def validate(self):
if self.status == "Completed" and not self.due_date:
frappe.throw("Please set a Due Date before marking as Completed.")
Use Case: Ensure that a required field is filled before saving.
2. before_insert(self)
Triggered before a new document is inserted into the database.
def before_insert(self):
frappe.msgprint("New record creation initiated.")
Use Case: Set default values or generate custom names before database insert.
3. after_insert(self)
Runs immediately after a new record has been added to the database.
def after_insert(self):
frappe.logger().info(f"Document {self.name} successfully created.")
Use Case: Send notifications or create dependent records after insert.
4. on_update(self)
Called after the document is updated and saved successfully.
def on_update(self):
frappe.msgprint(f"Record {self.name} has been updated.")
Use Case: Trigger updates in related DocTypes or refresh computed fields.
5. before_save(self)
Added in recent versions, it runs before saving (for both insert and update).
It executes after validate() but before the actual database operation.
def before_save(self):
frappe.logger().info("Preparing document for save.")
Use Case: Perform pre-save logic like recalculating totals.
6. before_submit(self)
Executed before the document is submitted.
def before_submit(self):
if self.total <= 0:
frappe.throw("Total value must be greater than zero before submission.")
Use Case: Validate mandatory business constraints before submission.
7. on_submit(self)
Runs when a document is successfully submitted (docstatus = 1).
def on_submit(self):
frappe.msgprint("Document successfully submitted.")
Use Case:
- Trigger accounting entries (in ERPNext).
- Lock records for further edits.
- Send workflow notifications.
8. on_cancel(self)
Triggered when a document is cancelled (docstatus = 2).
def on_cancel(self):
frappe.msgprint(f"Document {self.name} has been cancelled.")
Use Case:
- Reverse linked transactions.
- Free associated resources.
- Update status fields in related records.
9. on_trash(self)
Executed before a document is permanently deleted.
def on_trash(self):
frappe.logger().info(f"Deleting record {self.name}")
Use Case: Prevent deletion if dependencies exist.
10. after_delete(self)
Executed after the document is deleted from the database.
def after_delete(self):
frappe.logger().info(f"Record {self.name} deleted successfully.")
Use Case:
- Log deletions for audit trail.
- Remove external system references.
11. on_change(self)
Triggered when any field value is changed.
def on_change(self):
frappe.msgprint(f"Document {self.name} has changed.")
Use Case: Auto-update dependent records or track field modifications.
12. on_rename(self, old_name, new_name, merge=False)
Called when a document is renamed.
def on_rename(self, old_name, new_name, merge=False):
frappe.logger().info(f"Document renamed from {old_name} to {new_name}")
Use Case: Maintain external mappings or references after renaming.
Execution Flow of Controller Methods
The internal sequence for document events in Frappe is as follows:
before_insert → validate → before_save → after_insert → on_update → before_submit → on_submit → on_cancel → on_trash
This flow ensures that each event occurs in a predictable, traceable order.
Example — Custom Business Logic with Controller Methods
Let’s create a validation rule for a custom Expense Claim DocType:
import frappe
from frappe.model.document import Document
class ExpenseClaim(Document):
def validate(self):
if self.amount <= 0:
frappe.throw("Expense amount must be greater than zero.")
def on_submit(self):
frappe.msgprint(f"Expense Claim {self.name} submitted for approval.")
Behavior:
- Blocks saving when amount ≤ 0.
- Shows confirmation upon submission.
Integrating Controller Methods with Hooks
In addition to defining methods in the controller file, you can link external events via hooks.py.
doc_events = {
"Expense Claim": {
"on_submit": "my_app.expense.doctype.expense_claim.expense_claim.after_submit"
}
}
Benefit:
Keeps custom logic separate from the core app code.
Best Practices for Using Controller Methods
- Avoid writing large amounts of logic inside one method — separate functions for clarity.
- Always handle exceptions with frappe.throw() instead of raise.
- Log important operations using frappe.logger() for traceability.
- Maintain backward compatibility for overrides in future updates.
- Combine controller methods with Server Scripts or Hooks for modular automation.
Troubleshooting Common Issues
|
Error |
Cause |
Solution |
| AttributeError: ‘Document’ object has no attribute ‘on_submit’ | Method not defined in controller | Add method inside your custom DocType’s Python controller |
| frappe.throw not defined | Missing import | Add import frappe at the top of your script |
| Logic not executing | Incorrect method name or hook | Verify method signature matches Frappe event name |
| PermissionError | Custom code bypasses standard permissions | Use frappe.has_permission() checks in methods |
Cross-References and Related Topics
- Creating a DocType in Frappe Framework (v15)
- DocType Features in Frappe Framework (v15)
- Frappe Model and Document API (GitHub v15)
- Frappe Hooks and Event Triggers