erpnext-syntax-hooks

安装

npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill erpnext-syntax-hooks

ERPNext Syntax: Hooks (hooks.py) Hooks in hooks.py enable custom apps to extend Frappe/ERPNext functionality. Quick Reference doc_events - Document Lifecycle

In hooks.py

doc_events

{ "*" : { "after_insert" : "myapp.events.log_all_inserts" } , "Sales Invoice" : { "validate" : "myapp.events.si_validate" , "on_submit" : "myapp.events.si_on_submit" } }

In myapp/events.py

import frappe def si_validate ( doc , method = None ) : """doc = document object, method = event name""" if doc . grand_total < 0 : frappe . throw ( "Total cannot be negative" ) scheduler_events - Periodic Tasks

In hooks.py

scheduler_events

{ "daily" : [ "myapp.tasks.daily_cleanup" ] , "hourly_long" : [ "myapp.tasks.heavy_sync" ] , "cron" : { "0 9 * * 1-5" : [ "myapp.tasks.weekday_morning" ] } }

In myapp/tasks.py

def daily_cleanup ( ) : """No arguments - called automatically""" frappe . db . delete ( "Log" , { "creation" : [ "<" , one_month_ago ( ) ] } ) extend_bootinfo - Client Data Injection

In hooks.py

extend_bootinfo

"myapp.boot.extend_boot"

In myapp/boot.py

def
extend_boot
(
bootinfo
)
:
"""bootinfo = dict that goes to frappe.boot"""
bootinfo
.
my_setting
=
frappe
.
get_single
(
"My Settings"
)
.
value
// Client-side
console
.
log
(
frappe
.
boot
.
my_setting
)
;
Most Used doc_events
Event
When
Use Case
validate
Before every save
Validation, calculations
on_update
After every save
Notifications, sync
after_insert
After new doc
Creation-only actions
on_submit
After submit
Ledger entries
on_cancel
After cancel
Reverse entries
on_trash
Before delete
Cleanup
Complete list
See
doc-events.md
Scheduler Event Types
Event
Frequency
Queue/Timeout
hourly
Every hour
default / 5 min
daily
Every day
default / 5 min
weekly
Every week
default / 5 min
monthly
Every month
default / 5 min
hourly_long
Every hour
long / 25 min
daily_long
Every day
long / 25 min
cron
Custom timing
default / 5 min
Cron syntax and examples
See scheduler-events.md Critical Rules 1. bench migrate after scheduler changes

REQUIRED - otherwise changes won't be picked up

bench --site sitename migrate 2. No commits in doc_events

❌ WRONG

def on_update ( doc , method = None ) : frappe . db . commit ( )

Breaks transaction

✅ CORRECT - Frappe commits automatically

def on_update ( doc , method = None ) : update_related_docs ( doc ) 3. Changes after on_update via db_set

❌ WRONG - change is lost

def on_update ( doc , method = None ) : doc . status = "Processed"

✅ CORRECT

def on_update ( doc , method = None ) : frappe . db . set_value ( doc . doctype , doc . name , "status" , "Processed" ) 4. Heavy tasks to _long queue

❌ WRONG - timeout after 5 min

scheduler_events

{ "daily" : [ "myapp.tasks.process_all_records" ]

May take 20 min

}

✅ CORRECT - 25 min timeout

scheduler_events

{ "daily_long" : [ "myapp.tasks.process_all_records" ] } 5. Tasks receive no arguments

❌ WRONG

def my_task ( some_arg ) : pass

✅ CORRECT

def my_task ( ) :

Fetch data inside the function

pass Cron Syntax Cheatsheet


│ │ │ │ │ │ │ │ │ └── Day of week (0-6, Sun=0) │ │ │ └──── Month (1-12) │ │ └────── Day of month (1-31) │ └──────── Hour (0-23) └────────── Minute (0-59) Pattern Meaning */5 * * * * Every 5 minutes 0 9 * * * Daily at 09:00 0 9 * * 1-5 Weekdays at 09:00 0 0 1 * * First day of month 0 17 * * 5 Friday at 17:00 doc_events vs Controller Hooks Aspect doc_events (hooks.py) Controller Methods Location hooks.py doctype/xxx/xxx.py Scope Hook OTHER doctypes Only OWN doctype Multiple handlers ✅ Yes (list) ❌ No Priority After controller First Wildcard ( * ) ✅ Yes ❌ No Use doc_events when : Hooking other apps' DocTypes from your custom app Reacting to ALL DocTypes (wildcard) Registering multiple handlers Use controller methods when : Working on your own DocType You want full lifecycle control Reference Files File Contents doc-events.md All document events, signatures, execution order scheduler-events.md Scheduler types, cron syntax, timeouts bootinfo.md extend_bootinfo, session hooks overrides.md Override and extend patterns permissions.md Permission hooks fixtures.md Fixtures configuration examples.md Complete hooks.py examples anti-patterns.md Mistakes and corrections Configuration Hooks Override DocType Controller

In hooks.py

override_doctype_class

{ "Sales Invoice" : "myapp.overrides.CustomSalesInvoice" }

In myapp/overrides.py

from erpnext . accounts . doctype . sales_invoice . sales_invoice import SalesInvoice class CustomSalesInvoice ( SalesInvoice ) : def validate ( self ) : super ( ) . validate ( )

CRITICAL: always call super()!

self
.
custom_validation
(
)
Warning
Last installed app wins when multiple apps override the same DocType. Override Whitelisted Methods

In hooks.py

override_whitelisted_methods

{ "frappe.client.get_count" : "myapp.overrides.custom_get_count" }

Method signature MUST be identical to original!

def custom_get_count ( doctype , filters = None , debug = False , cache = False ) :

Custom implementation

return frappe . db . count ( doctype , filters ) Permission Hooks

In hooks.py

permission_query_conditions

{ "Sales Invoice" : "myapp.permissions.si_query_conditions" } has_permission = { "Sales Invoice" : "myapp.permissions.si_has_permission" }

In myapp/permissions.py

def si_query_conditions ( user ) : """Returns SQL WHERE fragment for list filtering""" if not user : user = frappe . session . user if "Sales Manager" in frappe . get_roles ( user ) : return ""

No restrictions

return f"tabSales Invoice.owner = { frappe . db . escape ( user ) } " def si_has_permission ( doc , user = None , permission_type = None ) : """Document-level permission check""" if permission_type == "write" and doc . status == "Closed" : return False return None

Fallback to default

Note : permission_query_conditions only works with get_list , NOT with get_all ! Fixtures

In hooks.py

fixtures

[ { "dt" : "Custom Field" , "filters" : [ [ "module" , "=" , "My App" ] ] } , { "dt" : "Property Setter" , "filters" : [ [ "module" , "=" , "My App" ] ] } , { "dt" : "Role" , "filters" : [ [ "name" , "like" , "MyApp%" ] ] } ]

Export fixtures to JSON

bench --site sitename export-fixtures Asset Includes

In hooks.py

Desk (backend) assets

app_include_js

"/assets/myapp/js/myapp.min.js" app_include_css = "/assets/myapp/css/myapp.min.css"

Website/Portal assets

web_include_js

"/assets/myapp/js/web.min.js" web_include_css = "/assets/myapp/css/web.min.css"

Form script extensions

doctype_js

{ "Sales Invoice" : "public/js/sales_invoice.js" } Install/Migrate Hooks

In hooks.py

after_install

"myapp.setup.after_install" after_migrate = "myapp.setup.after_migrate"

In myapp/setup.py

def
after_install
(
)
:
create_default_roles
(
)
def
after_migrate
(
)
:
clear_custom_cache
(
)
Complete Decision Tree
What do you want to achieve?
├─► REACT to document events from OTHER apps?
│ └─► doc_events
├─► Run PERIODIC tasks?
│ └─► scheduler_events
│ ├─► < 5 min → hourly/daily/weekly/monthly
│ ├─► > 5 min → hourly_long/daily_long/etc.
│ └─► Specific time → cron
├─► Send DATA to CLIENT at page load?
│ └─► extend_bootinfo
├─► Modify CONTROLLER of existing DocType?
│ ├─► Frappe v16+ → extend_doctype_class (recommended)
│ └─► Frappe v14/v15 → override_doctype_class
├─► Modify API ENDPOINT?
│ └─► override_whitelisted_methods
├─► Customize PERMISSIONS?
│ ├─► List filtering → permission_query_conditions
│ └─► Document-level → has_permission
├─► EXPORT/IMPORT configuration?
│ └─► fixtures
├─► ADD JS/CSS to desk or portal?
│ ├─► Desk → app_include_js/css
│ ├─► Portal → web_include_js/css
│ └─► Form specific → doctype_js
└─► SETUP on install/migrate?
└─► after_install, after_migrate
Version Differences
Feature
v14
v15
v16
doc_events
scheduler_events
extend_bootinfo
override_doctype_class
extend_doctype_class
permission_query_conditions
has_permission
fixtures
Anti-Patterns Summary
❌ Wrong
✅ Correct
frappe.db.commit()
in handler
Frappe commits automatically
doc.field = x
in on_update
frappe.db.set_value()
Heavy task in
daily
Use
daily_long
Change scheduler without migrate
Always
bench migrate
Sensitive data in bootinfo
Only public config
Override without
super()
Always
super().method()
first
get_all
with permission_query
Use
get_list
Fixtures without filters
Filter by module/app
Full anti-patterns
See anti-patterns.md
返回排行榜