mcpField
F
frappe-impl-clientscripts

frappe-impl-clientscripts

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

关于 frappe-impl-clientscripts

指导 Frappe 开发者实现表单客户端行为,涵盖事件选择、字段逻辑、服务器调用、调试及迁移到自定义应用,并区分界面逻辑与需服务端验证的逻辑。

主要功能

  1. 01为客户端行为选择表单事件
  2. 02切换字段显示并设置链接筛选
  3. 03计算子表行与文档数值
  4. 04从表单调用服务端方法
  5. 05添加自定义按钮和异步验证
  6. 06调试并迁移脚本

适用场景

  1. 01根据所选国家筛选城市选项
  2. 02按条件显示表单字段
  3. 03行变更后重新计算发票总额
  4. 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.md
examples.md
workflows.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 创建客户端脚本

  1. 前往 Setup > Client Script(或在 awesomebar 中输入“New Client Script”)
  2. 选择目标 DocType
  3. 始终勾选 Enabled
  4. 使用 frappe.ui.form.on 模式编写脚本
  5. 保存——脚本立即生效(无需重启)
  6. 打开目标 DocType 表单 → 测试行为
  7. 打开浏览器 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:在浏览器中调试

  1. 打开 F12 DevTools > Console
  2. 在事件处理程序中添加 console.log(frm.doc)
  3. 在 Console 中使用 cur_frm 检查当前表单状态
  4. 在 Network 标签页中检查失败的 frappe.call 请求
  5. 使用 frappe.ui.form.handlers 查看已注册的事件处理程序

调试模式:

frappe.ui.form.on('MyDocType', {
    my_field(frm) {
        console.log('Field changed:', frm.doc.my_field);
        // ... actual logic
    }
});

工作流 10:将客户端脚本迁移到自定义应用

  1. 创建 JS 文件:myapp/myapp/public/js/sales_order.js
  2. 将脚本内容移入该文件(保留 frappe.ui.form.on 包装)
  3. 在 hooks.py 中注册:
    doctype_js = {
        "Sales Order": "public/js/sales_order.js"
    }
    
  4. 运行 bench build(开发时使用 bench watch)
  5. 从 Setup 中删除客户端脚本文档
  6. 在表单上测试——行为必须完全相同

始终在以下情况下迁移:需要版本控制、部署到多个环境、脚本超过 50 行或需要团队协作。

性能规则

规则原因
仅在 setup 中使用 set_query避免每次 refresh 时重复注册
批量调用 set_valuefrm.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.* 客户端 API
  • frappe-impl-serverscripts — 何时将逻辑移至服务器端

有关事件选择,请参阅 references/decision-tree.md。 有关扩展模式,请参阅 references/workflows.md。 有关 10+ 个完整示例,请参阅 references/examples.md。

评论

支持 Markdown