erpnext-impl-clientscripts
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Client Scripts - Implementation (EN)
ERPNext Client Scripts - 实现说明
This skill helps you determine HOW to implement client-side features. For exact syntax, see .
erpnext-syntax-clientscriptsVersion: v14/v15/v16 compatible
本指南将帮助你确定如何实现客户端功能。如需了解具体语法,请参考 。
erpnext-syntax-clientscripts版本兼容性:支持v14/v15/v16
Main Decision: Client or Server?
核心决策:选择客户端还是服务器端?
┌─────────────────────────────────────────────────────────┐
│ Must the logic ALWAYS execute? │
│ (including imports, API calls, Server Scripts) │
├─────────────────────────────────────────────────────────┤
│ YES → Server-side (Controller or Server Script) │
│ NO → What is the primary goal? │
│ ├── UI feedback/UX improvement → Client Script │
│ ├── Show/hide fields → Client Script │
│ ├── Link filters → Client Script │
│ ├── Data validation → BOTH (client + server) │
│ └── Calculations → Depends on criticality │
└─────────────────────────────────────────────────────────┘Rule of thumb: Client Scripts for UX, Server for integrity.
┌─────────────────────────────────────────────────────────┐
│ Must the logic ALWAYS execute? │
│ (including imports, API calls, Server Scripts) │
├─────────────────────────────────────────────────────────┤
│ YES → Server-side (Controller or Server Script) │
│ NO → What is the primary goal? │
│ ├── UI feedback/UX improvement → Client Script │
│ ├── Show/hide fields → Client Script │
│ ├── Link filters → Client Script │
│ ├── Data validation → BOTH (client + server) │
│ └── Calculations → Depends on criticality │
└─────────────────────────────────────────────────────────┘经验法则:客户端脚本用于优化UX(用户体验),服务器端用于保障数据完整性。
Decision Tree: Which Event?
决策树:选择哪个事件?
WHAT DO YOU WANT TO ACHIEVE?
│
├─► Set link field filters
│ └── setup (once, early in lifecycle)
│
├─► Add custom buttons
│ └── refresh (after each form load/save)
│
├─► Show/hide fields based on condition
│ └── refresh + {fieldname} (both needed)
│
├─► Validation before save
│ └── validate (use frappe.throw on error)
│
├─► Action after successful save
│ └── after_save
│
├─► Calculation on field change
│ └── {fieldname}
│
├─► Child table row added
│ └── {tablename}_add
│
├─► Child table field changed
│ └── Child DocType event: {fieldname}
│
└─► One-time initialization
└── setup or onload→ See references/decision-tree.md for complete decision tree.
WHAT DO YOU WANT TO ACHIEVE?
│
├─► Set link field filters
│ └── setup (once, early in lifecycle)
│
├─► Add custom buttons
│ └── refresh (after each form load/save)
│
├─► Show/hide fields based on condition
│ └── refresh + {fieldname} (both needed)
│
├─► Validation before save
│ └── validate (use frappe.throw on error)
│
├─► Action after successful save
│ └── after_save
│
├─► Calculation on field change
│ └── {fieldname}
│
├─► Child table row added
│ └── {tablename}_add
│
├─► Child table field changed
│ └── Child DocType event: {fieldname}
│
└─► One-time initialization
└── setup or onload→ 完整决策树请参考 references/decision-tree.md。
Implementation Workflows
实现工作流
Workflow 1: Dynamic Field Visibility
工作流1:动态字段显示/隐藏
Scenario: Show "delivery_date" only when "requires_delivery" is checked.
javascript
frappe.ui.form.on('Sales Order', {
refresh(frm) {
// Initial state on form load
frm.trigger('requires_delivery');
},
requires_delivery(frm) {
// Toggle on checkbox change AND refresh
frm.toggle_display('delivery_date', frm.doc.requires_delivery);
frm.toggle_reqd('delivery_date', frm.doc.requires_delivery);
}
});Why both events?
- : Sets correct state when form opens
refresh - : Responds to user interaction
{fieldname}
场景:仅当勾选"requires_delivery"时显示"delivery_date"字段。
javascript
frappe.ui.form.on('Sales Order', {
refresh(frm) {
// Initial state on form load
frm.trigger('requires_delivery');
},
requires_delivery(frm) {
// Toggle on checkbox change AND refresh
frm.toggle_display('delivery_date', frm.doc.requires_delivery);
frm.toggle_reqd('delivery_date', frm.doc.requires_delivery);
}
});为什么需要这两个事件?
- :表单打开时设置正确的初始状态
refresh - :响应用户交互操作
{fieldname}
Workflow 2: Cascading Dropdowns
工作流2:级联下拉菜单
Scenario: Filter "city" based on selected "country".
javascript
frappe.ui.form.on('Customer', {
setup(frm) {
// Filter MUST be in setup for consistency
frm.set_query('city', () => ({
filters: {
country: frm.doc.country || ''
}
}));
},
country(frm) {
// Clear city when country changes
frm.set_value('city', '');
}
});场景:根据选中的"country"筛选"city"选项。
javascript
frappe.ui.form.on('Customer', {
setup(frm) {
// Filter MUST be in setup for consistency
frm.set_query('city', () => ({
filters: {
country: frm.doc.country || ''
}
}));
},
country(frm) {
// Clear city when country changes
frm.set_value('city', '');
}
});Workflow 3: Automatic Calculations
工作流3:自动计算
Scenario: Calculate total in child table with discount.
javascript
frappe.ui.form.on('Sales Invoice', {
discount_percentage(frm) {
calculate_totals(frm);
}
});
frappe.ui.form.on('Sales Invoice Item', {
qty(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
rate(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
amount(frm) {
// Recalculate document total on row change
calculate_totals(frm);
}
});
function calculate_row_amount(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate);
}
function calculate_totals(frm) {
let total = 0;
(frm.doc.items || []).forEach(row => {
total += row.amount || 0;
});
let discount = total * (frm.doc.discount_percentage || 0) / 100;
frm.set_value('grand_total', total - discount);
}场景:计算子表中包含折扣的总计金额。
javascript
frappe.ui.form.on('Sales Invoice', {
discount_percentage(frm) {
calculate_totals(frm);
}
});
frappe.ui.form.on('Sales Invoice Item', {
qty(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
rate(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
amount(frm) {
// Recalculate document total on row change
calculate_totals(frm);
}
});
function calculate_row_amount(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate);
}
function calculate_totals(frm) {
let total = 0;
(frm.doc.items || []).forEach(row => {
total += row.amount || 0;
});
let discount = total * (frm.doc.discount_percentage || 0) / 100;
frm.set_value('grand_total', total - discount);
}Workflow 4: Fetching Server Data
工作流4:获取服务器数据
Scenario: Populate customer details on customer selection.
javascript
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) {
// Clear fields if customer cleared
frm.set_value({
customer_name: '',
territory: '',
credit_limit: 0
});
return;
}
// Fetch customer details
let r = await frappe.db.get_value('Customer',
frm.doc.customer,
['customer_name', 'territory', 'credit_limit']
);
if (r.message) {
frm.set_value({
customer_name: r.message.customer_name,
territory: r.message.territory,
credit_limit: r.message.credit_limit
});
}
}
});场景:选择客户后自动填充客户详情。
javascript
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) {
// Clear fields if customer cleared
frm.set_value({
customer_name: '',
territory: '',
credit_limit: 0
});
return;
}
// Fetch customer details
let r = await frappe.db.get_value('Customer',
frm.doc.customer,
['customer_name', 'territory', 'credit_limit']
);
if (r.message) {
frm.set_value({
customer_name: r.message.customer_name,
territory: r.message.territory,
credit_limit: r.message.credit_limit
});
}
}
});Workflow 5: Validation with Server Check
工作流5:带服务器校验的验证
Scenario: Check credit limit before save.
javascript
frappe.ui.form.on('Sales Order', {
async validate(frm) {
if (frm.doc.customer && frm.doc.grand_total) {
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]));
}
}
}
});→ See references/workflows.md for more workflow patterns.
场景:保存前检查客户信用额度。
javascript
frappe.ui.form.on('Sales Order', {
async validate(frm) {
if (frm.doc.customer && frm.doc.grand_total) {
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]));
}
}
}
});→ 更多工作流模式请参考 references/workflows.md。
Integration Matrix
集成矩阵
| Client Script Action | Requires Server-side |
|---|---|
| Link filters | Optional: custom query |
| Fetch server data | |
| Call document method | |
| Complex validation | Server Script or controller validation |
| Create document | |
| 客户端脚本操作 | 是否需要服务器端支持 |
|---|---|
| 链接筛选 | 可选:自定义查询 |
| 获取服务器数据 | |
| 调用文档方法 | 控制器中需添加 |
| 复杂验证 | 服务器脚本或控制器验证 |
| 创建文档 | |
Client + Server Combination
客户端与服务器端结合示例
javascript
// CLIENT: frm.call invokes controller method
frm.call('calculate_taxes')
.then(() => frm.reload_doc());
// SERVER (controller): MUST have @frappe.whitelist
class SalesInvoice(Document):
@frappe.whitelist()
def calculate_taxes(self):
# complex calculation
self.tax_amount = self.grand_total * 0.21
self.save()javascript
// CLIENT: frm.call invokes controller method
frm.call('calculate_taxes')
.then(() => frm.reload_doc());
// SERVER (controller): MUST have @frappe.whitelist
class SalesInvoice(Document):
@frappe.whitelist()
def calculate_taxes(self):
// complex calculation
self.tax_amount = self.grand_total * 0.21
self.save()Checklist: Implementation Steps
检查清单:实施步骤
New Client Script Feature
新客户端脚本功能开发
-
[ ] Determine scope
- UI/UX only? → Client script only
- Data integrity? → Also server validation
-
[ ] Choose events
- Use decision tree above
- Combine refresh + fieldname for visibility
-
[ ] Implement basics
- Start with
frappe.ui.form.on - Test with console.log first
- Start with
-
[ ] Add error handling
- around async calls
try/catch - for validation errors
frappe.throw
-
[ ] Test edge cases
- New document (frm.is_new())
- Empty field (null checks)
- Child table empty/filled
-
[ ] Translate strings
- All UI text in
__()
- All UI text in
-
[ ] 确定范围
- 仅涉及UI/UX?→ 仅使用客户端脚本
- 涉及数据完整性?→ 同时需要服务器端验证
-
[ ] 选择事件
- 使用上述决策树
- 显示/隐藏字段需结合refresh + 字段名事件
-
[ ] 基础实现
- 从 开始
frappe.ui.form.on - 先使用console.log测试
- 从
-
[ ] 添加错误处理
- 异步调用需包裹
try/catch - 验证错误使用
frappe.throw
- 异步调用需包裹
-
[ ] 测试边缘情况
- 新建文档(frm.is_new())
- 空字段(空值检查)
- 子表为空/已填充
-
[ ] 字符串翻译
- 所有UI文本需包裹在中
__()
- 所有UI文本需包裹在
Critical Rules
关键规则
| Rule | Why |
|---|---|
| UI synchronization |
| Consistent filter behavior |
| Stops save action |
| Async/await for server calls | Prevent race conditions |
Check | Prevent errors on new doc |
| 规则 | 原因 |
|---|---|
子表变更后调用 | 保证UI同步 |
| 保证筛选行为一致 |
验证使用 | 阻止保存操作 |
| 服务器调用使用Async/await | 避免竞态条件 |
按钮需检查 | 避免在新建文档时出错 |
Related Skills
相关技能
- — Exact syntax and method signatures
erpnext-syntax-clientscripts - — Error handling patterns
erpnext-errors-clientscripts - — Server methods for frm.call
erpnext-syntax-whitelisted - — frappe.db.* client-side API
erpnext-database
→ See references/examples.md for 10+ complete implementation examples.
- — 具体语法与方法签名
erpnext-syntax-clientscripts - — 错误处理模式
erpnext-errors-clientscripts - — 用于frm.call的服务器端方法
erpnext-syntax-whitelisted - — frappe.db.* 客户端API
erpnext-database
→ 10+完整实现示例请参考 references/examples.md。