erpnext-syntax-hooks

ERPNext Syntax: Hooks (hooks.py)

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-syntax-hooks" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-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

  1. 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)

  1. 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")

  1. 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"] }

  1. 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

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-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-database

No summary provided by upstream source.

Repository SourceNeeds Review