Loading...
Loading...
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.
npx skill4agent add openaec-foundation/erpnext_anthropic_claude_development_skill_package erpnext-impl-controllerserpnext-syntax-controllers┌───────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└───────────────────────────────────────────────────────────────────┘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┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ 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") │
│ │
└─────────────────────────────────────────────────────────────────────┘| 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 |
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"))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_amountdef 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()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")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)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]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()# 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()# 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"))# 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):
pass# 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)before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert →
on_update → on_changebefore_validate → validate → before_save → [DB UPDATE] →
on_update → on_changevalidate → before_submit → [DB: docstatus=1] → on_update →
on_submit → on_change| ❌ Don't | ✅ Do Instead |
|---|---|
| |
| Let framework handle commits |
| Heavy operations in validate | Use |
| Causes infinite loop! |
| Assume hook order across docs | Each doc has its own cycle |