mcpField
F
frappe-impl-serverscripts

frappe-impl-serverscripts

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

关于 frappe-impl-serverscripts

面向 Frappe 开发者的 Server Script 实现指南,说明如何在受限沙箱中构建文档事件、API、计划任务和权限查询脚本,并指出何时应迁移到 Controller。

主要功能

  1. 01选择脚本类型并按步骤配置
  2. 02编写文档事件校验与计算
  3. 03创建 API 和计划任务脚本
  4. 04使用沙箱可用的 Frappe API
  5. 05判断何时迁移到 Controller

适用场景

  1. 01保存文档前校验字段或计算值
  2. 02为 Frappe 创建受认证的 API 端点
  3. 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.md
decision-tree.md
examples.md
workflows.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

  1. 启用 Server Scripts:bench set-config server_script_enabled true
  2. 前往 Setup > Server Script(或在 awesomebar 中输入“New Server Script”)
  3. 选择 Script Type(参见下方决策树)
  4. 配置特定类型的设置(DocType、事件、API 方法、cron)
  5. 在编辑器中编写脚本
  6. 保存 — 脚本立即生效
  7. 触发所配置的事件进行测试
  8. 使用“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 Validatebefore_validate验证前设置默认值
Before Savevalidate验证 + 计算(最常用)
After Saveon_update通知、审计日志
After Insertafter_insert创建关联文档(仅限新文档)
Before Submitbefore_submit提交时验证
After Submiton_submit提交后自动化
Before Cancelbefore_cancel阻止取消
After Cancelon_cancel取消后清理
Before Deleteon_trash阻止删除

沙盒安全 API 快速参考

需求使用(不要导入)
解析 JSONfrappe.parse_json() 或 json.loads()
序列化 JSONfrappe.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 GETfrappe.make_get_request(url)
HTTP POSTfrappe.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 个以上的完整示例。

评论

支持 Markdown