mcpField
F
frappe-impl-hooks

frappe-impl-hooks

github.com/impertio-studio开发198118 次安装30 周前更新
代码仓库

关于 frappe-impl-hooks

面向 Frappe 开发者的 hooks.py 实现工作流指南,涵盖事件、调度、权限及资源注入等配置。按步骤说明选择、编写、部署和测试,并指出不同版本的差异。

主要功能

  1. 01配置文档生命周期事件
  2. 02设置定时任务与 Cron
  3. 03扩展 DocType 类与权限
  4. 04注入前端资源和网站钩子
  5. 05迁移并测试 Hook 配置

适用场景

  1. 01为其他应用的 DocType 添加事件处理
  2. 02安排定期清理或报告任务
  3. 03为 Sales Invoice 扩展逻辑
  4. 04配置权限过滤或页面资源

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-impl-hooks

安装后在 Claude Code 中输入/frappe-impl-hooks确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
decision-tree.md
examples.md
workflows.md

详细介绍

Frappe 钩子实现工作流

实现 hooks.py 配置的分步工作流。API 语法参考请参阅 frappe-syntax-hooks。

版本:v14/v15/v16(V16 专属功能会注明)


主要决策:你要实现什么?

WHAT DO YOU WANT TO ACHIEVE?
│
├─► React to document lifecycle events?
│   ├─► On OTHER app's DocTypes → doc_events in hooks.py
│   ├─► On YOUR OWN DocTypes → controller methods (preferred)
│   └─► On ALL DocTypes → doc_events with "*" wildcard
│
├─► Run code on a schedule?
│   └─► scheduler_events (daily, hourly, cron, etc.)
│
├─► Modify an existing DocType's behavior?
│   ├─► V16+: extend_doctype_class (RECOMMENDED)
│   └─► V14/V15: override_doctype_class (last app wins!)
│
├─► Override an existing API endpoint?
│   └─► override_whitelisted_methods
│
├─► Add custom permission logic?
│   ├─► List filtering → permission_query_conditions
│   └─► Document-level → has_permission
│
├─► Send config data to client on page load?
│   └─► extend_bootinfo
│
├─► Export/import configuration?
│   └─► fixtures
│
├─► Add JS/CSS to desk or portal?
│   ├─► Desk-wide → app_include_js / app_include_css
│   ├─► Portal-wide → web_include_js / web_include_css
│   └─► Specific form → doctype_js
│
├─► Customize website/portal behavior?
│   └─► website_context, portal_menu_items, website_route_rules
│
└─► Hook into session/auth lifecycle?
    └─► on_login, on_session_creation, on_logout

工作流 1:实现 doc_events

适用场景

如果需要响应由其他应用(ERPNext、Frappe 核心)拥有的 DocType 的文档生命周期事件,请使用 doc_events。对于你自己的 DocType,始终优先使用控制器方法。

分步说明

步骤 1:选择合适的事件(参见 references/decision-tree.md)

BEFORE save: validate (every save), before_insert (new only)
AFTER save:  after_insert (new only), on_update (every save), on_change (any change)
SUBMIT flow: before_submit → on_submit → on_change
CANCEL flow: before_cancel → on_cancel → on_change
DELETE:      on_trash (before), after_delete (after)
RENAME:      before_rename, after_rename

步骤 2:添加到 hooks.py

# myapp/hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales_invoice.validate",
        "on_submit": "myapp.events.sales_invoice.on_submit"
    }
}

步骤 3:创建处理程序模块

# myapp/events/sales_invoice.py
import frappe

def validate(doc, method=None):
    """Changes to doc ARE saved (before-save event)."""
    if doc.grand_total < 0:
        frappe.throw("Total cannot be negative")

def on_submit(doc, method=None):
    """Document already saved. Use db_set_value for changes."""
    frappe.db.set_value("Sales Invoice", doc.name,
                        "custom_external_id", create_external(doc))

步骤 4:部署

bench --site sitename migrate

步骤 5:测试

bench --site sitename execute myapp.events.sales_invoice.validate --kwargs '{"doc_name": "INV-001"}'
# Or in bench console:
# doc = frappe.get_doc("Sales Invoice", "INV-001"); doc.save()
doc_events 的关键规则
  • 绝不要在 doc_event 处理程序中调用 frappe.db.commit()——Frappe 会管理事务
  • 绝不要在 on_update 中修改 doc 字段——更改会丢失;请改用 frappe.db.set_value()
  • 始终在处理程序签名中将 method=None 作为第二个参数
  • 始终使用重命名签名:def handler(doc, method, old, new, merge)
  • 始终在更改 hooks.py 后运行 bench --site sitename migrate

工作流 2:实现 scheduler_events

分步说明

步骤 1:选择频率

频率短任务(< 5 分钟)长任务(5-25 分钟)
每次 tickall—
每小时hourlyhourly_long
每天dailydaily_long
每周weeklyweekly_long
每月monthlymonthly_long
自定义croncron(手动使用长任务队列)

步骤 2:添加到 hooks.py

scheduler_events = {
    "daily": ["myapp.tasks.daily_cleanup"],
    "daily_long": ["myapp.tasks.heavy_sync"],
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.weekday_report"]
    }
}

步骤 3:实现任务(不带参数)

# myapp/tasks.py
import frappe

def daily_cleanup():
    """Scheduler calls with NO arguments."""
    frappe.db.delete("Error Log", {
        "creation": ["<", frappe.utils.add_days(None, -30)]
    })
    frappe.db.commit()

def heavy_sync():
    """Long task — commit periodically."""
    records = get_records_to_sync()
    for i, record in enumerate(records):
        process(record)
        if i % 100 == 0:
            frappe.db.commit()
    frappe.db.commit()

步骤 4:部署并验证

bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status
# Test manually:
bench --site sitename execute myapp.tasks.daily_cleanup
Scheduler 的关键规则
  • 绝不要为 scheduler 任务函数添加参数——scheduler 不会传递任何参数
  • 始终为耗时超过 5 分钟的任务使用 _long 变体(默认队列超时时间为 5 分钟)
  • 始终在长任务中定期提交,以保存进度
  • 任务耗时超过 25 分钟:拆分成多个批次,或使用 frappe.enqueue()

工作流 3:实现 extend_doctype_class(V16+)

分步说明

步骤 1:添加到 hooks.py

extend_doctype_class = {
    "Sales Invoice": ["myapp.extensions.sales_invoice.SalesInvoiceMixin"]
}

步骤 2:创建混入类

# myapp/extensions/sales_invoice.py
import frappe
from frappe.model.document import Document

class SalesInvoiceMixin(Document):
    def validate(self):
        super().validate()  # ALWAYS call super() FIRST
        self.custom_validation()

    def custom_validation(self):
        if self.grand_total > 1000000:
            frappe.msgprint("High-value invoice", indicator="orange")

步骤 3:部署——bench --site sitename migrate

何时使用 extend 而非 override
  • 始终在 V16+ 优先使用 extend_doctype_class——多个应用可以安全地扩展
  • 仅在必须完全替换控制器逻辑时使用 override_doctype_class
  • 在 V14/V15 中,override_doctype_class 是唯一选项——最后安装的应用生效

工作流 4:实现权限钩子

分步说明

步骤 1:添加到 hooks.py

permission_query_conditions = {
    "Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
    "Sales Invoice": "myapp.permissions.si_permission"
}

步骤 2:实现处理程序

# myapp/permissions.py
import frappe

def si_query(user):
    """Returns SQL WHERE clause for list filtering."""
    if not user:
        user = frappe.session.user
    if "Sales Manager" in frappe.get_roles(user):
        return ""  # See all
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"

def si_permission(doc, user=None, permission_type=None):
    """Returns True (allow), False (deny), or None (use default)."""
    if not user:
        user = frappe.session.user
    if permission_type == "write" and doc.status == "Closed":
        return False
    return None
权限钩子的关键规则
  • permission_query_conditions 仅适用于 get_list,绝不适用于 get_all
  • has_permission 只能拒绝访问——返回 True 并不会授予额外权限
  • 始终处理 user=None 的情况,默认使用 frappe.session.user

工作流 5:资源注入和 doctype_js

添加全局 JS/CSS
# hooks.py
app_include_js = "/assets/myapp/js/myapp.min.js"       # Desk
app_include_css = "/assets/myapp/css/myapp.min.css"     # Desk
web_include_js = "/assets/myapp/js/portal.min.js"       # Portal
web_include_css = "/assets/myapp/css/portal.min.css"    # Portal
扩展特定表单
# hooks.py
doctype_js = {
    "Sales Invoice": "public/js/sales_invoice.js"
}
// myapp/public/js/sales_invoice.js
frappe.ui.form.on("Sales Invoice", {
    refresh(frm) {
        if (frm.doc.docstatus === 1) {
            frm.add_custom_button(__("Custom Action"), () => {
                frappe.call({
                    method: "myapp.api.custom_action",
                    args: { invoice: frm.doc.name },
                    freeze: true
                });
            }, __("Actions"));
        }
    }
});

更改 JS/CSS 文件后,始终运行 bench build --app myapp。


工作流 6:Fixtures、Boot Info 和网站钩子

Fixtures
fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "My App"]]}
]

绝不要在没有筛选条件的情况下导出 fixtures——这会捕获所有应用的自定义项。

extend_bootinfo
extend_bootinfo = "myapp.boot.extend_with_config"
def extend_with_config(bootinfo):
    bootinfo.my_app = {"feature_enabled": True}
    # NEVER send secrets — bootinfo is visible in browser DevTools
网站钩子
website_route_rules = [
    {"from_route": "/shop/<category>", "to_route": "shop"}
]
portal_menu_items = [
    {"title": "My Orders", "route": "/my-orders", "role": "Customer"}
]
on_login = "myapp.handlers.on_login"
on_logout = "myapp.handlers.on_logout"

迁移:在钩子、控制器和服务器脚本之间迁移逻辑

来源目标步骤
服务器脚本 → hooks.py1. 创建 Python 处理程序,2. 添加 doc_events,3. 禁用服务器脚本,4. 迁移
hooks.py → 控制器1. 将逻辑移至 doctype .py,2. 删除 doc_events 条目,3. 迁移
控制器 → hooks.py1. 创建事件模块,2. 添加 doc_events,3. 从控制器中移除,4. 迁移

务必在每次更改 hooks.py 后迁移:bench --site sitename migrate


处理程序签名速查

钩子签名
doc_eventsdef handler(doc, method=None):
重命名事件def handler(doc, method, old, new, merge):
scheduler_eventsdef handler():(无参数)
extend_bootinfodef handler(bootinfo):
permission_querydef handler(user): 返回 SQL 字符串
has_permissiondef handler(doc, user=None, permission_type=None): 返回 True/False/None
on_logindef handler(login_manager):
on_logoutdef handler():

版本差异

功能V14V15V16
doc_events是是是
scheduler_events是是是
override_doctype_class是是是
extend_doctype_class否否是
权限钩子是是是
Scheduler tick 间隔~4 分钟~4 分钟~60 秒
auth_hooks否是是

参考文件

文件内容
decision-tree.md完整的钩子选择流程图
workflows.md分步实现模式
examples.md所有钩子类型的可运行代码示例

评论

支持 Markdown