erpnext-impl-clientscripts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Client Scripts - Implementation (EN)

ERPNext Client Scripts - 实现说明

This skill helps you determine HOW to implement client-side features. For exact syntax, see
erpnext-syntax-clientscripts
.
Version: 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?
  • refresh
    : Sets correct state when form opens
  • {fieldname}
    : Responds to user interaction
场景:仅当勾选"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 ActionRequires Server-side
Link filtersOptional: custom query
Fetch server data
frappe.db.*
or whitelisted method
Call document method
@frappe.whitelist()
in controller
Complex validationServer Script or controller validation
Create document
frappe.db.insert
or whitelisted method
客户端脚本操作是否需要服务器端支持
链接筛选可选:自定义查询
获取服务器数据
frappe.db.*
或白名单方法
调用文档方法控制器中需添加
@frappe.whitelist()
复杂验证服务器脚本或控制器验证
创建文档
frappe.db.insert
或白名单方法

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

新客户端脚本功能开发

  1. [ ] Determine scope
    • UI/UX only? → Client script only
    • Data integrity? → Also server validation
  2. [ ] Choose events
    • Use decision tree above
    • Combine refresh + fieldname for visibility
  3. [ ] Implement basics
    • Start with
      frappe.ui.form.on
    • Test with console.log first
  4. [ ] Add error handling
    • try/catch
      around async calls
    • frappe.throw
      for validation errors
  5. [ ] Test edge cases
    • New document (frm.is_new())
    • Empty field (null checks)
    • Child table empty/filled
  6. [ ] Translate strings
    • All UI text in
      __()
  1. [ ] 确定范围
    • 仅涉及UI/UX?→ 仅使用客户端脚本
    • 涉及数据完整性?→ 同时需要服务器端验证
  2. [ ] 选择事件
    • 使用上述决策树
    • 显示/隐藏字段需结合refresh + 字段名事件
  3. [ ] 基础实现
    • frappe.ui.form.on
      开始
    • 先使用console.log测试
  4. [ ] 添加错误处理
    • 异步调用需包裹
      try/catch
    • 验证错误使用
      frappe.throw
  5. [ ] 测试边缘情况
    • 新建文档(frm.is_new())
    • 空字段(空值检查)
    • 子表为空/已填充
  6. [ ] 字符串翻译
    • 所有UI文本需包裹在
      __()

Critical Rules

关键规则

RuleWhy
refresh_field()
after child table change
UI synchronization
set_query
in
setup
event
Consistent filter behavior
frappe.throw()
for validation, not
msgprint
Stops save action
Async/await for server callsPrevent race conditions
Check
frm.is_new()
for buttons
Prevent errors on new doc
规则原因
子表变更后调用
refresh_field()
保证UI同步
set_query
需在
setup
事件中定义
保证筛选行为一致
验证使用
frappe.throw()
而非
msgprint
阻止保存操作
服务器调用使用Async/await避免竞态条件
按钮需检查
frm.is_new()
避免在新建文档时出错

Related Skills

相关技能

  • erpnext-syntax-clientscripts
    — Exact syntax and method signatures
  • erpnext-errors-clientscripts
    — Error handling patterns
  • erpnext-syntax-whitelisted
    — Server methods for frm.call
  • erpnext-database
    — frappe.db.* client-side API
→ See references/examples.md for 10+ complete implementation examples.
  • erpnext-syntax-clientscripts
    — 具体语法与方法签名
  • erpnext-errors-clientscripts
    — 错误处理模式
  • erpnext-syntax-whitelisted
    — 用于frm.call的服务器端方法
  • erpnext-database
    — frappe.db.* 客户端API
→ 10+完整实现示例请参考 references/examples.md