关于 frappe-syntax-hooks
面向 Frappe 开发者的 hooks.py 配置参考,按类别介绍应用配置、定时任务、权限、请求处理及其他扩展钩子。
主要功能
- 01应用元数据与前端资源钩子
- 02安装迁移与定时任务钩子
- 03会话、认证及请求处理中间件
- 04权限与 DocType 扩展钩子
- 05网站、文件、邮件和 PDF 配置
适用场景
- 01为应用添加前端资源
- 02配置定时任务与生命周期操作
- 03扩展权限、网站或邮件行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-hooks安装后在 Claude Code 中输入/frappe-syntax-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdbootinfo.mdexamples.mdhooks.mdoverrides.mdpermissions.mdrequest-lifecycle.mdscheduler-events.md详细介绍
Frappe 配置钩子(hooks.py)
hooks.py 中的配置钩子可让自定义应用扩展 Frappe/ERPNext 的行为。本技能涵盖所有非文档事件钩子。对于 doc_events(validate、on_submit、on_update 等),请参阅 frappe-syntax-hooks-events。
快速参考:钩子类别
| 类别 | 关键钩子 | 参考 |
|---|---|---|
| 应用元数据 | app_name、app_title、required_apps | 下文 |
| 前端资源 | app_include_js/css、web_include_js/css | 下文 |
| 安装/迁移 | before_install、after_install、after_migrate | 下文 |
| 调度器 | hourly、daily、cron、*_long | scheduler-events.md |
| 会话/身份验证 | on_login、on_logout、auth_hooks | bootinfo.md |
| 请求中间件 | before_request、after_request | request-lifecycle.md |
| 权限 | permission_query_conditions、has_permission | permissions.md |
| DocType 覆盖 | override_doctype_class、doctype_js | overrides.md |
| 网站/门户 | website_route_rules、portal_menu_items | request-lifecycle.md |
| 文件处理 | before_write_file、write_file | 下文 |
| 电子邮件 | override_email_send、default_mail_footer | 下文 |
pdf_header_html、pdf_footer_html | 下文 | |
| Jinja | jinja.methods、jinja.filters | 下文 |
| 启动/客户端数据 | extend_bootinfo、notification_config | bootinfo.md |
| 数据/固定数据 | fixtures、global_search_doctypes | 下文 |
| 方法覆盖 | override_whitelisted_methods、standard_queries | overrides.md |
决策树:我需要哪个钩子?
What do you want to achieve?
|
+-- ADD JS/CSS to desk or portal?
| +-- Desk --> app_include_js / app_include_css
| +-- Portal --> web_include_js / web_include_css
| +-- Specific form --> doctype_js
| +-- List view --> doctype_list_js
|
+-- RUN periodic background tasks?
| +-- < 5 min execution --> hourly / daily / weekly / monthly
| +-- 5-25 min execution --> hourly_long / daily_long / etc.
| +-- Exact time needed --> cron
| See: frappe-syntax-hooks > scheduler-events.md
|
+-- SEND data to client at page load?
| +-- extend_bootinfo
|
+-- MODIFY controller of existing DocType?
| +-- v16+ --> extend_doctype_class (RECOMMENDED)
| +-- v14/v15 --> override_doctype_class (last app wins)
|
+-- MODIFY API endpoint?
| +-- override_whitelisted_methods
|
+-- CUSTOMIZE permissions?
| +-- List filtering --> permission_query_conditions
| +-- Document-level --> has_permission
|
+-- REACT to document save/submit/delete?
| +-- See frappe-syntax-hooks-events skill
|
+-- EXPORT/IMPORT configuration?
| +-- fixtures
|
+-- SETUP on install or migrate?
| +-- after_install / after_migrate
|
+-- ADD custom Jinja functions?
| +-- jinja.methods / jinja.filters
|
+-- CUSTOMIZE website routing?
| +-- website_route_rules
| See: request-lifecycle.md for full routing pipeline
|
+-- INTERCEPT every request/response?
| +-- before_request / after_request
| See: request-lifecycle.md for lifecycle flow
|
+-- CUSTOM page rendering?
| +-- page_renderer hook
| See: request-lifecycle.md for renderer architecture
1. 应用元数据钩子
每个 hooks.py 中都必须包含以下内容:
app_name = "myapp"
app_title = "My App"
app_publisher = "My Company"
app_description = "Custom ERPNext extensions"
app_email = "[email protected]"
app_license = "MIT"
required_apps = ["erpnext"] # Declare dependencies
2. 前端资源注入
# Desk (backend UI) assets — loaded on EVERY desk page
app_include_js = "/assets/myapp/js/myapp.min.js" # string or list
app_include_css = "/assets/myapp/css/myapp.min.css"
# Website/portal assets — loaded on EVERY web page
web_include_js = "/assets/myapp/js/web.min.js"
web_include_css = "/assets/myapp/css/web.min.css"
# Web form specific assets
webform_include_js = {"My Web Form": "public/js/my_webform.js"}
webform_include_css = {"My Web Form": "public/css/my_webform.css"}
# Form script extensions (extend OTHER apps' forms)
doctype_js = {"Sales Invoice": "public/js/sales_invoice.js"}
# List view script extensions
doctype_list_js = {"Sales Invoice": "public/js/sales_invoice_list.js"}
# Custom sounds
sounds = [{"name": "alert", "src": "/assets/myapp/sounds/alert.mp3", "volume": 0.5}]
绝不要将大型库放入 app_include_js——它们会在每个页面加载。
3. 安装与迁移生命周期
before_install = "myapp.setup.before_install"
after_install = "myapp.setup.after_install"
after_sync = "myapp.setup.after_sync" # After fixture sync
before_migrate = "myapp.setup.before_migrate"
after_migrate = "myapp.setup.after_migrate"
before_uninstall = "myapp.setup.before_uninstall"
after_uninstall = "myapp.setup.after_uninstall"
before_tests = "myapp.setup.seed_test_data"
所有钩子都接受单个点分路径字符串。函数不会接收任何参数。
4. 调度器事件
完整参考请参阅 scheduler-events.md。
scheduler_events = {
"all": ["myapp.tasks.every_minute"], # ~60s interval
"hourly": ["myapp.tasks.hourly_check"], # default queue, 5 min timeout
"daily": ["myapp.tasks.daily_report"],
"weekly": ["myapp.tasks.weekly_cleanup"],
"monthly": ["myapp.tasks.monthly_summary"],
"daily_long": ["myapp.tasks.heavy_sync"], # long queue, 25 min timeout
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_morning"] # cron expression
}
}
更改 scheduler_events 后,务必运行 bench --site sitename migrate。
绝不要定义带参数的任务函数——它们不会接收任何参数。
5. 会话与身份验证钩子
on_login = "myapp.auth.on_login" # Receives login_manager
on_logout = "myapp.auth.on_logout" # No arguments
on_session_creation = "myapp.auth.on_session_creation" # No arguments
auth_hooks = ["myapp.auth.validate_request"] # List of validators
执行顺序:on_login --> 创建会话 --> on_session_creation --> extend_bootinfo。
6. 请求/响应中间件
完整的请求生命周期流程、页面渲染器架构和路由器 API,请参阅 request-lifecycle.md。
before_request = ["myapp.middleware.before_request"] # List of dotted paths
after_request = ["myapp.middleware.after_request"]
before_job = ["myapp.middleware.before_job"] # Before background job
after_job = ["myapp.middleware.after_job"] # After background job
7. 权限钩子
完整参考请参阅 permissions.md。
permission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query_conditions"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_has_permission"
}
在处理函数中,务必检查 if not user: user = frappe.session.user。
在 SQL 中务必使用 frappe.db.escape(user)——绝不要使用字符串插值。
permission_query_conditions 仅适用于 get_list,不适用于 get_all。
8. DocType 类覆盖
完整参考请参阅 overrides.md。
# v14+ — Full replacement (LAST installed app wins)
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
# v16+ — Mixin-based extension (ALL apps coexist) [RECOMMENDED]
extend_doctype_class = {
"Address": ["myapp.extensions.AddressMixin"]
}
覆盖方法中务必调用 super().method()。忘记调用 super() 会破坏核心逻辑。
9. 网站与门户钩子
# URL routing
website_route_rules = [
{"from_route": "/custom-page/<name>", "to_route": "Custom Page"}
]
website_redirects = [
{"source": "/old-url", "target": "/new-url"}
]
website_catch_all = "myapp.www.custom_404"
# Homepage
homepage = "my-custom-home"
role_home_page = {"Sales User": "sales-dashboard"}
get_website_user_home_page = "myapp.utils.get_home_page"
# Portal sidebar
portal_menu_items = [{"title": "My Orders", "route": "/orders", "role": "Customer"}]
standard_portal_menu_items = [{"title": "My Items", "route": "/my-items"}]
# Template overrides
base_template = "myapp/templates/base.html"
website_context = {"brand_html": "<b>My Brand</b>"}
update_website_context = "myapp.context.update_context"
10. 文件处理钩子
before_write_file = "myapp.files.before_write" # Pre-save hook
write_file = "myapp.files.custom_write" # Replace file storage (e.g., S3/CDN)
delete_file_data_content = "myapp.files.custom_delete" # Replace file deletion
使用 write_file 将文件存储重定向到云服务提供商(S3、GCS、Azure Blob)。
11. 电子邮件钩子
override_email_send = "myapp.email.custom_send" # Replace email backend
get_sender_details = "myapp.email.get_sender" # Override From address
default_mail_footer = "myapp.email.get_footer" # HTML footer for all emails
12. PDF 钩子
pdf_header_html = "myapp.pdf.get_header" # Custom PDF header
pdf_body_html = "myapp.pdf.get_body" # Custom PDF body wrapper
pdf_footer_html = "myapp.pdf.get_footer" # Custom PDF footer
# pdf_generator = "myapp.pdf.generate" # [v16+] Replace PDF engine
13. Jinja 钩子
# Add custom methods available in Jinja templates
jinja = {
"methods": ["myapp.jinja_utils.get_balance"],
"filters": ["myapp.jinja_utils.format_iban"]
}
# myapp/jinja_utils.py
def get_balance(customer):
"""Usage in template: {{ get_balance(doc.customer) }}"""
return frappe.db.get_value("Customer", customer, "outstanding_amount") or 0
def format_iban(value):
"""Usage in template: {{ bank_account|format_iban }}"""
if not value: return ""
return " ".join([value[i:i+4] for i in range(0, len(value), 4)])
14. 启动与客户端数据
完整参考请参阅 bootinfo.md。
extend_bootinfo = "myapp.boot.extend_boot"
notification_config = "myapp.notifications.get_config"
绝不要将密钥/API 密钥放入 bootinfo——它会被发送到浏览器。 绝不要在 bootinfo 中运行繁重的查询——它会在每次页面加载时运行。
15. 数据与 Fixtures
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My App"]]},
{"dt": "Role", "filters": [["name", "like", "MyApp%"]]}
]
global_search_doctypes = {"My DocType": {"index": 10}}
ignore_links_on_delete = ["Communication", "Activity Log"]
calendars = ["My Event DocType"]
clear_cache = "myapp.cache.clear_custom_cache"
Fixtures 中始终使用过滤条件——绝不导出未经过滤的数据(这会导出所有内容)。 绝不要将事务数据(Sales Invoice、Stock Entry)放入 Fixtures。
16. 方法重写
完整参考请参阅 overrides.md。
override_whitelisted_methods = {
"frappe.client.get_count": "myapp.overrides.custom_get_count"
}
standard_queries = {
"Customer": "myapp.queries.customer_query"
}
重写时始终确保与原始方法的签名完全一致。
版本差异
| Hook | v14 | v15 | v16+ |
|---|---|---|---|
extend_doctype_class | -- | -- | 新增 |
extend_bootinfo | 是 | 是 | 是 |
auth_hooks | 是 | 是 | 是 |
after_sync | 是 | 是 | 是 |
before_uninstall | -- | 是 | 是 |
after_uninstall | -- | 是 | 是 |
website_path_resolver | -- | 是 | 是 |
| 其他所有 Hook | 是 | 是 | 是 |
关键规则
- 修改
hooks.py后,始终运行bench --site sitename migrate - 绝不要在
hooks.py的模块级导入 frappe——它会在初始化之前运行 - 始终使用点分路径(
"myapp.module.function")——绝不要使用 lambda - 绝不要在 Hook 处理函数中提交事务——Frappe 会管理事务
- 部署前,始终在开发环境中测试 Hook
反模式摘要
| 错误做法 | 正确做法 |
|---|---|
| Fixtures 中没有过滤条件 | 始终按模块/应用进行过滤 |
| 在 bootinfo 中放入机密信息 | bootinfo 中仅放入公开配置 |
| 在 bootinfo 中执行耗时查询 | 缓存数据或尽量减少数据量 |
使用带有权限 Hook 的 get_all | 使用 get_list 进行权限过滤 |
重写时不调用 super() | 始终先调用 super().method() |
| 调度任务带有参数 | 任务不接收任何参数 |
跳过 bench migrate | 修改 Hook 后始终执行 migrate |
完整反模式列表:anti-patterns.md
参考文件
| 文件 | 内容 |
|---|---|
| hooks.md | 按类别整理的完整 Hook 目录 |
| scheduler-events.md | 调度频率、cron 语法、超时 |
| permissions.md | 权限 Hook 详解 |
| overrides.md | DocType 类重写模式 |
| bootinfo.md | extend_bootinfo、会话 Hook、notification_config |
| examples.md | 各类别可用的 hooks.py 示例 |
| request-lifecycle.md | 请求生命周期、路由流程、页面渲染器、路由器 API |
| anti-patterns.md | 常见 Hook 错误及修正方法 |
有关文档生命周期事件(doc_events),请参阅:frappe-syntax-hooks-events