erpnext-impl-controllers

安装

npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill 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

返回排行榜