"Implementation workflows and decision trees for Frappe Document Controllers. Use when determining HOW to implement server-side DocType logic: lifecycle hooks, validation patterns, autoname, submittable workflows, controller override. Triggers: how do I implement controller, which hook to use, validate vs on_update, override controller, submittable document, autoname pattern, flags system."
Resources
1Install
npx skillscat add openaec-foundation/erpnext-anthropic-claude-development-skill-package/erpnext-impl-controllers Install via the SkillsCat registry.
ERPNext Controllers - Implementation
This skill helps you determine HOW to implement server-side DocType logic. For exact syntax, see erpnext-syntax-controllers.
Version: v14/v15/v16 compatible
Main Decision: Controller vs Server Script?
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WHAT DO YOU NEED? โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โบ Import external libraries (requests, pandas, numpy) โ
โ โโโ Controller โ โ
โ โ
โ โบ Complex multi-document transactions with rollback โ
โ โโโ Controller โ โ
โ โ
โ โบ Full Python power (try/except, classes, generators) โ
โ โโโ Controller โ โ
โ โ
โ โบ Extend/override standard ERPNext DocType โ
โ โโโ Controller (override_doctype_class in hooks.py) โ
โ โ
โ โบ Quick validation without custom app โ
โ โโโ Server Script โ
โ โ
โ โบ Simple auto-fill or calculation โ
โ โโโ Server Script โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโRule: Controllers for custom apps with full Python power. Server Scripts for quick no-code solutions.
Decision Tree: Which Hook?
WHAT DO YOU WANT TO DO?
โ
โโโบ Validate data or calculate fields before save?
โ โโโบ validate
โ NOTE: Changes to self ARE saved
โ
โโโบ Action AFTER save (emails, linked docs, logs)?
โ โโโบ on_update
โ โ ๏ธ Changes to self are NOT saved! Use db_set instead
โ
โโโบ Only for NEW documents?
โ โโโบ after_insert
โ
โโโบ Only for SUBMIT (docstatus 0โ1)?
โ โโโบ Check before submit? โ before_submit
โ โโโบ Action after submit? โ on_submit
โ
โโโบ Only for CANCEL (docstatus 1โ2)?
โ โโโบ Prevent cancel? โ before_cancel
โ โโโบ Cleanup after cancel? โ on_cancel
โ
โโโบ Before DELETE?
โ โโโบ on_trash
โ
โโโบ Custom document naming?
โ โโโบ autoname
โ
โโโบ Detect any change (including db_set)?
โโโบ on_changeโ See references/decision-tree.md for complete decision tree with all hooks.
CRITICAL: Changes After on_update
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ ๏ธ CHANGES TO self AFTER on_update ARE NOT SAVED โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โ WRONG - This does NOTHING: โ
โ def on_update(self): โ
โ self.status = "Completed" # NOT SAVED! โ
โ โ
โ โ
CORRECT - Use db_set: โ
โ def on_update(self): โ
โ frappe.db.set_value(self.doctype, self.name, โ
โ "status", "Completed") โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโHook Comparison: validate vs on_update
| Aspect | validate | on_update |
|---|---|---|
| When | Before DB write | After DB write |
| Changes to self | โ Saved | โ NOT saved |
| Can throw error | โ Aborts save | โ ๏ธ Already saved |
| Use for | Validation, calculations | Notifications, linked docs |
| get_doc_before_save() | โ Available | โ Available |
Common Implementation Patterns
Pattern 1: Validation with Error
def validate(self):
if not self.items:
frappe.throw(_("At least one item is required"))
if self.from_date > self.to_date:
frappe.throw(_("From Date cannot be after To Date"))Pattern 2: Auto-Calculate Fields
def validate(self):
self.total = sum(item.amount for item in self.items)
self.tax_amount = self.total * 0.1
self.grand_total = self.total + self.tax_amountPattern 3: Detect Field Changes
def validate(self):
old_doc = self.get_doc_before_save()
if old_doc and old_doc.status != self.status:
self.flags.status_changed = True
def on_update(self):
if self.flags.get('status_changed'):
self.notify_status_change()Pattern 4: Post-Save Actions
def on_update(self):
# Update linked document
if self.linked_doc:
frappe.db.set_value("Other DocType", self.linked_doc,
"status", "Updated")
# Send notification (never fails the save)
try:
self.send_notification()
except Exception:
frappe.log_error("Notification failed")Pattern 5: Custom Naming
from frappe.model.naming import getseries
def autoname(self):
# Format: CUST-ABC-001
prefix = f"CUST-{self.customer[:3].upper()}-"
self.name = getseries(prefix, 3)โ See references/workflows.md for more implementation patterns.
Submittable Documents Workflow
DRAFT (docstatus=0)
โ
โโโ save() โ validate โ on_update
โ
โโโ submit()
โ
โโโ validate
โโโ before_submit โ Last chance to abort
โโโ [DB: docstatus=1]
โโโ on_update
โโโ on_submit โ Post-submit actions
SUBMITTED (docstatus=1)
โ
โโโ cancel()
โ
โโโ before_cancel โ Last chance to abort
โโโ [DB: docstatus=2]
โโโ on_cancel โ Reverse actions
โโโ [check_no_back_links]Submittable Implementation
def before_submit(self):
# Validation that only applies on submit
if self.total > 50000 and not self.manager_approval:
frappe.throw(_("Manager approval required for orders over 50,000"))
def on_submit(self):
# Actions after submit
self.update_stock_ledger()
self.make_gl_entries()
def before_cancel(self):
# Prevent cancel if linked docs exist
if self.has_linked_invoices():
frappe.throw(_("Cannot cancel - linked invoices exist"))
def on_cancel(self):
# Reverse submitted actions
self.reverse_stock_ledger()
self.reverse_gl_entries()Controller Override (hooks.py)
Method 1: Full Override
# hooks.py
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
# myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # ALWAYS call parent
self.custom_validation()Method 2: Add Event Handler (Safer)
# hooks.py
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.validate_sales_invoice",
}
}
# myapp/events.py
def validate_sales_invoice(doc, method=None):
if doc.grand_total < 0:
frappe.throw(_("Invalid total"))V16: extend_doctype_class (New)
# hooks.py (v16+)
extend_doctype_class = {
"Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}
# myapp/extends.py - Only methods to add/override
class SalesInvoiceExtend:
def custom_method(self):
passFlags System
# Document-level flags
doc.flags.ignore_permissions = True # Bypass permissions
doc.flags.ignore_validate = True # Skip validate()
doc.flags.ignore_mandatory = True # Skip required fields
# Custom flags for inter-hook communication
def validate(self):
if self.is_urgent:
self.flags.needs_notification = True
def on_update(self):
if self.flags.get('needs_notification'):
self.notify_team()
# Insert/save with flags
doc.insert(ignore_permissions=True, ignore_mandatory=True)
doc.save(ignore_permissions=True)Execution Order Reference
INSERT (New Document)
before_insert โ before_naming โ autoname โ before_validate โ
validate โ before_save โ [DB INSERT] โ after_insert โ
on_update โ on_changeSAVE (Existing Document)
before_validate โ validate โ before_save โ [DB UPDATE] โ
on_update โ on_changeSUBMIT
validate โ before_submit โ [DB: docstatus=1] โ on_update โ
on_submit โ on_changeโ See references/decision-tree.md for all execution orders.
Quick Anti-Pattern Check
| โ Don't | โ Do Instead |
|---|---|
self.x = y in on_update |
frappe.db.set_value(...) |
frappe.db.commit() in hooks |
Let framework handle commits |
| Heavy operations in validate | Use frappe.enqueue() in on_update |
self.save() in on_update |
Causes infinite loop! |
| Assume hook order across docs | Each doc has its own cycle |
โ See references/anti-patterns.md for complete list.
References
- decision-tree.md - Complete hook selection with all execution orders
- workflows.md - Extended implementation patterns
- examples.md - Complete working examples
- anti-patterns.md - Common mistakes to avoid