erpnext-impl-controllers

ERPNext Controllers - Implementation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-impl-controllers" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-impl-controllers

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_amount

Pattern 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): pass

Flags 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_change

SAVE (Existing Document)

before_validate → validate → before_save → [DB UPDATE] → on_update → on_change

SUBMIT

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-database

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-customapp

No summary provided by upstream source.

Repository SourceNeeds Review