关于 frappe-impl-clientscripts
指导 Frappe 开发者实现表单客户端行为,涵盖事件选择、字段逻辑、服务器调用、调试及迁移到自定义应用,并区分界面逻辑与需服务端验证的逻辑。
主要功能
- 01为客户端行为选择表单事件
- 02切换字段显示并设置链接筛选
- 03计算子表行与文档数值
- 04从表单调用服务端方法
- 05添加自定义按钮和异步验证
- 06调试并迁移脚本
适用场景
- 01根据所选国家筛选城市选项
- 02按条件显示表单字段
- 03行变更后重新计算发票总额
- 04将较长的 Client Script 移入自定义应用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-clientscripts安装后在 Claude Code 中输入/frappe-impl-clientscripts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/decision-tree.mdexamples.mdworkflows.md详细介绍
客户端脚本——实现工作流
用于构建客户端表单功能的分步工作流。有关准确的 API 语法,请参阅 frappe-syntax-clientscripts。
版本:v14/v15/v16 | 注意:v13 将“Custom Script”重命名为“Client Script”
快速判断:客户端还是服务器端?
MUST the logic ALWAYS execute (imports, API, Data Import)?
├── YES → Server Script or Controller
└── NO → What is the goal?
├── UI feedback / UX → Client Script
├── Show/hide fields → Client Script
├── Link filters → Client Script
├── Data validation → BOTH (client for UX, server for integrity)
└── Calculations → Client for display, server for critical
规则:始终使用客户端脚本改善用户体验。关键逻辑始终要由服务器端验证兜底。
工作流 1:通过 UI 创建客户端脚本
- 前往 Setup > Client Script(或在 awesomebar 中输入“New Client Script”)
- 选择目标 DocType
- 始终勾选 Enabled
- 使用
frappe.ui.form.on模式编写脚本 - 保存——脚本立即生效(无需重启)
- 打开目标 DocType 表单 → 测试行为
- 打开浏览器 DevTools Console(F12)进行调试
何时迁移到自定义应用:脚本超过 50 行、需要版本控制,或必须部署到多个环境时,始终应进行迁移。
工作流 2:选择合适的事件
WHAT DO YOU WANT?
├── Set link filters → setup (once, earliest lifecycle)
├── Add custom buttons → refresh (re-added after each render)
├── Show/hide fields → refresh + {fieldname} (BOTH needed)
├── Validate before save → validate (frappe.throw stops save)
├── Action after save → after_save
├── Calculate on change → {fieldname} handler
├── Child row added → {tablename}_add
├── Child row removed → {tablename}_remove
├── Child field changed → Child DocType: {fieldname}
├── One-time init → setup or onload
└── After full DOM render → onload_post_render
有关完整的事件时机矩阵,请参阅 references/decision-tree.md。
工作流 3:切换字段可见性
目标:仅当勾选“requires_delivery”时显示“delivery_date”。
步骤 1:同时实现 refresh 和字段名事件:
frappe.ui.form.on('Sales Order', {
refresh(frm) {
frm.trigger('requires_delivery'); // Set initial state
},
requires_delivery(frm) {
frm.toggle_display('delivery_date', frm.doc.requires_delivery);
frm.toggle_reqd('delivery_date', frm.doc.requires_delivery);
}
});
为什么要同时使用? refresh 在表单加载时设置状态。{fieldname} 响应用户交互。切勿只使用其中一个——否则表单加载或字段变更时会显示错误状态。
工作流 4:级联链接过滤器
目标:根据选定的“country”过滤“city”。
frappe.ui.form.on('Customer', {
setup(frm) {
// ALWAYS set filters in setup — ensures consistency
frm.set_query('city', () => ({
filters: { country: frm.doc.country || '' }
}));
},
country(frm) {
frm.set_value('city', ''); // ALWAYS clear dependent field
}
});
规则:始终将 set_query 放在 setup 中。父字段变更时,始终清空子字段。
工作流 5:计算字段(子表)
目标:计算行金额和单据总计。
frappe.ui.form.on('Invoice Item', {
qty(frm, cdt, cdn) { calculate_row(frm, cdt, cdn); },
rate(frm, cdt, cdn) { calculate_row(frm, cdt, cdn); },
amount(frm) { calculate_totals(frm); }
});
frappe.ui.form.on('Invoice', {
items_remove(frm) { calculate_totals(frm); }
});
function calculate_row(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'amount',
flt(row.qty) * flt(row.rate));
}
function calculate_totals(frm) {
let total = (frm.doc.items || []).reduce(
(sum, row) => sum + flt(row.amount), 0);
frm.set_value('grand_total', flt(total, 2));
}
规则:
- 数值运算始终使用
flt()(可处理 null/undefined) - 始终处理
items_remove——删除行时必须重新计算总计 - 调用
set_value后切勿调用refresh_field——它会自动触发
工作流 6:服务器调用:使用哪种方法
NEED TO CALL THE SERVER?
├── Fetch a single value?
│ └── frappe.db.get_value(doctype, name, fields)
│ Returns: Promise — lightweight, no whitelist needed
│
├── Call a document's controller method?
│ └── frm.call(method, args)
│ Requires: @frappe.whitelist() on controller method
│ Auto-includes: doctype, docname, doc context
│
├── Call a standalone whitelisted function?
│ └── frappe.call({method: 'dotted.path', args: {}})
│ Requires: @frappe.whitelist() decorator
│ Returns: Promise with r.message
│
└── Need Promise-only (no callback)?
└── frappe.xcall('dotted.path', args)
Same as frappe.call but returns clean Promise
示例——frm.call:
frm.call('calculate_taxes').then(r => {
frm.reload_doc(); // Refresh after server-side changes
});
示例——frappe.xcall:
let result = await frappe.xcall(
'myapp.api.check_credit', { customer: frm.doc.customer });
工作流 7:自定义按钮实现
frappe.ui.form.on('Sales Order', {
refresh(frm) {
// ALWAYS check conditions before adding buttons
if (!frm.is_new() && frm.doc.docstatus === 1) {
frm.add_custom_button(__('Create Invoice'), () => {
create_invoice(frm);
}, __('Create')); // Group label
}
}
});
规则:
- 始终在
refresh中添加按钮——每次重新渲染时按钮都会被清除 - 始终检查
frm.is_new()——在未保存的文档上添加按钮会导致错误 - 按钮标签始终使用
__()包装,以便翻译 - 切勿在
setup或onload中添加按钮——此时 UI 尚未就绪
工作流 8:通过服务器检查进行异步验证
frappe.ui.form.on('Sales Order', {
async validate(frm) {
if (!frm.doc.customer || !frm.doc.grand_total) return;
let r = await frappe.call({
method: 'myapp.api.check_credit',
args: {
customer: frm.doc.customer,
amount: frm.doc.grand_total
}
});
if (r.message && !r.message.allowed) {
frappe.throw(__('Credit limit exceeded. Available: {0}',
[r.message.available]));
}
}
});
规则:
- 在
validate中进行服务器调用时始终使用async/await - 始终使用
frappe.throw()阻止保存——msgprint不会阻止保存 - 如果用户未预期到耗时操作,切勿在
validate中进行较慢的服务器调用
工作流 9:在浏览器中调试
- 打开 F12 DevTools > Console
- 在事件处理程序中添加
console.log(frm.doc) - 在 Console 中使用
cur_frm检查当前表单状态 - 在 Network 标签页中检查失败的
frappe.call请求 - 使用
frappe.ui.form.handlers查看已注册的事件处理程序
调试模式:
frappe.ui.form.on('MyDocType', {
my_field(frm) {
console.log('Field changed:', frm.doc.my_field);
// ... actual logic
}
});
工作流 10:将客户端脚本迁移到自定义应用
- 创建 JS 文件:
myapp/myapp/public/js/sales_order.js - 将脚本内容移入该文件(保留
frappe.ui.form.on包装) - 在
hooks.py中注册:doctype_js = { "Sales Order": "public/js/sales_order.js" } - 运行
bench build(开发时使用bench watch) - 从 Setup 中删除客户端脚本文档
- 在表单上测试——行为必须完全相同
始终在以下情况下迁移:需要版本控制、部署到多个环境、脚本超过 50 行或需要团队协作。
性能规则
| 规则 | 原因 |
|---|---|
仅在 setup 中使用 set_query | 避免每次 refresh 时重复注册 |
批量调用 set_value | frm.set_value({a: 1, b: 2})——一次更新,而不是两次 |
| 缓存服务器响应 | 存储在 frm._cache_key 中,避免重复调用 |
| 切勿在循环中查询 | 一次获取所有数据,然后构建查找映射 |
使用 frappe.db.get_value | 对于简单查找,它比 frappe.call 更轻量 |
相关技能
frappe-syntax-clientscripts— 精确的 API 语法和方法签名frappe-errors-clientscripts— 错误处理和常见陷阱frappe-syntax-whitelisted— 可从客户端调用的服务器方法frappe-core-database—frappe.db.*客户端 APIfrappe-impl-serverscripts— 何时将逻辑移至服务器端
有关事件选择,请参阅 references/decision-tree.md。 有关扩展模式,请参阅 references/workflows.md。 有关 10+ 个完整示例,请参阅 references/examples.md。