erpnext-syntax-hooks

📁 openaec-foundation/erpnext_anthropic_claude_development_skill_package 📅 8 days ago
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