erpnext-syntax-hooks
0
总安装量
9
周安装量
安装命令
npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill erpnext-syntax-hooks
Agent 安装分布
claude-code
7
opencode
5
github-copilot
5
codex
5
amp
5
Skill 文档
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