关于 frappe-impl-serverscripts
面向 Frappe 开发者的 Server Script 实现指南,说明如何在受限沙箱中构建文档事件、API、计划任务和权限查询脚本,并指出何时应迁移到 Controller。
主要功能
- 01选择脚本类型并按步骤配置
- 02编写文档事件校验与计算
- 03创建 API 和计划任务脚本
- 04使用沙箱可用的 Frappe API
- 05判断何时迁移到 Controller
适用场景
- 01保存文档前校验字段或计算值
- 02为 Frappe 创建受认证的 API 端点
- 03设置定时提醒或权限查询条件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-serverscripts安装后在 Claude Code 中输入/frappe-impl-serverscripts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mddecision-tree.mdexamples.mdworkflows.md详细介绍
服务端脚本 — 实现工作流
无需自定义应用即可构建服务端功能的分步工作流。如需查看准确语法,请参阅 frappe-syntax-serverscripts。
版本:v14/v15/v16 | v15+ 注意:Server Scripts 默认处于禁用状态 — 使用 bench set-config server_script_enabled true 启用
重要:沙箱限制
ALL IMPORTS BLOCKED — RestrictedPython sandbox
import json → ImportError: __import__ not found
from frappe.utils → ImportError
import requests → ImportError
SOLUTION: Use pre-loaded namespace:
frappe.utils.nowdate() frappe.utils.flt()
frappe.parse_json(data) json.loads() (json IS available)
frappe.as_json(obj) json.dumps()
frappe.make_get_request(url) (replaces requests.get)
规则:如果需要使用 json 以外的 import 语句,请务必改用 Controller。
工作流 1:创建 Server Script
- 启用 Server Scripts:
bench set-config server_script_enabled true - 前往 Setup > Server Script(或在 awesomebar 中输入“New Server Script”)
- 选择 Script Type(参见下方决策树)
- 配置特定类型的设置(DocType、事件、API 方法、cron)
- 在编辑器中编写脚本
- 保存 — 脚本立即生效
- 触发所配置的事件进行测试
- 使用“Compare Versions”按钮比较更改
工作流 2:选择脚本类型
WHAT DO YOU NEED?
│
├── React to document save/submit/cancel?
│ └── Document Event
│ └── Select DocType + Event (Before Save, After Save, etc.)
│
├── Create a REST API endpoint?
│ └── API
│ └── Set method name + guest access setting
│ └── Endpoint: /api/method/{method_name}
│
├── Run task on schedule (daily/hourly/cron)?
│ └── Scheduler Event
│ └── Set cron pattern or frequency
│
└── Filter list views per user/role?
└── Permission Query
└── Select DocType — set `conditions` variable
完整决策树请参阅 references/decision-tree.md。
工作流 3:文档事件:验证
目标:保存前验证 Sales Order。
步骤 1:选择事件 — “Before Save”对应 validate 钩子。
步骤 2:编写沙箱安全的脚本:
# Type: Document Event | Event: Before Save | DocType: Sales Order
errors = []
if not doc.customer:
errors.append("Customer is required")
if doc.delivery_date and doc.delivery_date < frappe.utils.today():
errors.append("Delivery date cannot be in the past")
for item in doc.items:
if item.qty <= 0:
errors.append(f"Row {item.idx}: Quantity must be positive")
if errors:
frappe.throw("<br>".join(errors), title="Validation Error")
规则:
- 务必收集所有错误后一次性抛出(比多次抛出错误的用户体验更好)
- 切勿在 Before Save 中调用
doc.save()— 框架会处理 - 务必使用
frappe.throw()—msgprint不会阻止保存
工作流 4:文档事件:自动计算
目标:自动计算总计并设置派生字段。
# Type: Document Event | Event: Before Save | DocType: Purchase Order
doc.total_qty = sum(item.qty or 0 for item in doc.items)
doc.total_amount = sum((item.qty or 0) * (item.rate or 0) for item in doc.items)
if doc.total_amount > 50000:
doc.requires_approval = 1
doc.approval_status = "Pending"
if doc.supplier and not doc.supplier_name:
doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name")
规则:务必在 Before Save 中直接修改 doc 字段 — 这些字段会自动持久化。
工作流 5:文档事件:创建关联文档
目标:插入新 Lead 时创建 ToDo。
# Type: Document Event | Event: After Insert | DocType: Lead
frappe.get_doc({
"doctype": "ToDo",
"allocated_to": doc.lead_owner or doc.owner,
"reference_type": "Lead",
"reference_name": doc.name,
"description": f"Follow up with new lead: {doc.lead_name}",
"date": frappe.utils.add_days(frappe.utils.today(), 1),
"priority": "High" if doc.status == "Hot" else "Medium"
}).insert(ignore_permissions=True)
规则:
- 创建关联文档时务必使用 After Insert 或 After Save
- 切勿在 Before Save 中创建文档 —
doc.name可能尚不存在 - 创建系统生成的文档时务必使用
ignore_permissions=True
工作流 6:API 端点
目标:创建经过身份验证、可返回客户数据的 REST API。
# Type: API | Method: get_customer_dashboard | Allow Guest: No
# Endpoint: /api/method/get_customer_dashboard
customer = frappe.form_dict.get("customer")
if not customer:
frappe.throw("Parameter 'customer' is required")
# ALWAYS check permissions
if not frappe.has_permission("Customer", "read", customer):
frappe.throw("Access denied", frappe.PermissionError)
orders = frappe.db.count("Sales Order", {"customer": customer, "docstatus": 1})
revenue = frappe.db.get_value("Sales Invoice",
filters={"customer": customer, "docstatus": 1},
fieldname="sum(grand_total)") or 0
frappe.response["message"] = {
"customer": customer,
"total_orders": orders,
"total_revenue": revenue
}
规则:
- 务必验证输入参数
- 务必检查权限(即使 Allow Guest 为 No)
- 务必限制查询上限:
min(frappe.utils.cint(limit), 100) - 切勿公开完整文档 — 仅返回所需字段
工作流 7:调度器事件
目标:每天提醒逾期发票。
# Type: Scheduler Event | Cron: 0 9 * * * (daily at 9:00)
BATCH_SIZE = 50
today = frappe.utils.today()
overdue = frappe.get_all("Sales Invoice",
filters={
"status": "Unpaid",
"due_date": ["<", today],
"docstatus": 1
},
fields=["name", "customer", "owner", "due_date", "grand_total"],
limit=BATCH_SIZE
)
for inv in overdue:
days = frappe.utils.date_diff(today, inv.due_date)
if not frappe.db.exists("ToDo", {
"reference_type": "Sales Invoice",
"reference_name": inv.name,
"status": "Open"
}):
frappe.get_doc({
"doctype": "ToDo",
"allocated_to": inv.owner,
"reference_type": "Sales Invoice",
"reference_name": inv.name,
"description": f"Invoice {inv.name} is {days} days overdue"
}).insert(ignore_permissions=True)
frappe.db.commit() # REQUIRED in scheduler scripts
规则:
- 调度器脚本末尾务必添加
frappe.db.commit() - 查询务必添加
limit— 防止内存耗尽 - 循环中务必使用
try/except+frappe.log_error() - 切勿运行处理无限条记录的调度器脚本
工作流 8:权限查询
目标:用户只能看到其所属区域的客户。
# Type: Permission Query | DocType: Customer
user_territory = frappe.db.get_value("User", user, "territory")
user_roles = frappe.get_roles(user)
if "System Manager" in user_roles:
conditions = "" # Full access
elif user_territory:
conditions = f"`tabCustomer`.territory = {frappe.db.escape(user_territory)}"
else:
conditions = f"`tabCustomer`.owner = {frappe.db.escape(user)}"
规则:
- 务必为 System Manager 提供完整访问权限(
conditions = "") - 在 SQL 中使用用户输入时务必使用
frappe.db.escape() - 务必设置
conditions变量 — 它就是输出 - 权限查询仅影响
frappe.db.get_list,不影响frappe.db.get_all
事件名称映射
| UI 名称 | 内部钩子 | 最适用场景 |
|---|---|---|
| Before Validate | before_validate | 验证前设置默认值 |
| Before Save | validate | 验证 + 计算(最常用) |
| After Save | on_update | 通知、审计日志 |
| After Insert | after_insert | 创建关联文档(仅限新文档) |
| Before Submit | before_submit | 提交时验证 |
| After Submit | on_submit | 提交后自动化 |
| Before Cancel | before_cancel | 阻止取消 |
| After Cancel | on_cancel | 取消后清理 |
| Before Delete | on_trash | 阻止删除 |
沙盒安全 API 快速参考
| 需求 | 使用(不要导入) |
|---|---|
| 解析 JSON | frappe.parse_json() 或 json.loads() |
| 序列化 JSON | frappe.as_json() 或 json.dumps() |
| 今天的日期 | frappe.utils.today() |
| 当前时间(日期时间) | frappe.utils.now() |
| 增加天数 | frappe.utils.add_days(date, n) |
| 日期差 | frappe.utils.date_diff(d1, d2) |
| 转换为浮点数 | frappe.utils.flt(val) |
| 转换为整数 | frappe.utils.cint(val) |
| HTTP GET | frappe.make_get_request(url) |
| HTTP POST | frappe.make_post_request(url, data) |
| 渲染模板 | frappe.render_template(tmpl, ctx) |
| 记录错误 | frappe.log_error(msg, title) |
| 发送邮件 | frappe.sendmail(recipients, subject, message) |
何时迁移到控制器
出现以下情况时,务必迁移到文档控制器:
- 需要
import语句(json除外) - 脚本超过 100 行
- 需要使用 try/except 并回滚
- 需要通过
frappe.enqueue()执行后台任务 - 需要扩展现有的 ERPNext DocType
- 同一 DocType 上的多个脚本变得难以管理
迁移路径:请参阅 frappe-impl-controllers,了解控制器实现。
相关技能
frappe-syntax-serverscripts— 精确的沙盒 API 参考frappe-errors-serverscripts— 错误处理和反模式frappe-core-database—frappe.db.*操作frappe-core-permissions— 权限系统详情frappe-impl-controllers— 何时从 Server Script 迁移
请参阅 references/decision-tree.md,了解完整的决策树。 请参阅 references/workflows.md,了解扩展模式。 请参阅 references/examples.md,查看 10 个以上的完整示例。