关于 frappe-syntax-serverscripts
面向 Frappe Server Scripts 的实用参考技能,说明如何在 RestrictedPython 沙箱中编写脚本,并介绍脚本类型、可用 API、生命周期事件及其限制。
主要功能
- 01Document Event、API、Scheduler 与 Permission Query 脚本
- 02RestrictedPython 沙箱规则和可用方法
- 03文档生命周期事件映射
- 04各版本的启用说明
- 05安全与查询安全指南
适用场景
- 01为文档事件添加校验逻辑
- 02在 Frappe 中创建 API 或定时脚本
- 03根据用户权限筛选文档列表
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-serverscripts安装后在 Claude Code 中输入/frappe-syntax-serverscripts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdevents.mdexamples.mdhooks.mdmethods.mdpatterns.mdsyntax.md详细介绍
Frappe 服务端脚本 — 完整参考
服务端脚本是通过 Frappe/ERPNext 界面中的 Setup > Server Script 管理的 Python 脚本。它们在 RestrictedPython 沙箱中运行。
关键:沙箱规则
┌──────────────────────────────────────────────────────────────────┐
│ ALL import STATEMENTS ARE BLOCKED │
│ │
│ import json → ImportError: __import__ not found │
│ from datetime import * → ImportError: __import__ not found │
│ import frappe → ImportError (even frappe itself!) │
│ │
│ EVERYTHING you need is pre-loaded in the frappe namespace. │
│ NEVER write an import line. ALWAYS use frappe.utils.*, etc. │
└──────────────────────────────────────────────────────────────────┘
始终使用预加载的命名空间,而不是导入:
| 被禁止的导入 | 改用 |
|---|---|
import json | frappe.parse_json() / frappe.as_json() |
from datetime import date | frappe.utils.today() / frappe.utils.now_datetime() |
from frappe.utils import cint | frappe.utils.cint()(已加载) |
import requests | frappe.make_get_request() / frappe.make_post_request() |
import re | 不可用 — 不使用正则表达式,重新组织逻辑 |
import os / import sys | 不可用 — 改用自定义应用 |
启用服务端脚本
# v14: enabled by default
# v15+: DISABLED by default — you MUST enable explicitly:
bench set-config -g server_script_enabled 1
# Or set server_script_enabled: true in site_config.json
切勿指望服务端脚本在 Frappe Cloud 共享 bench 上运行 — 它们需要专用 bench。
脚本类型
| 类型 | 触发条件 | 关键变量 |
|---|---|---|
| Document Event | 文档生命周期(保存、提交、取消) | doc |
| API | 对 /api/method/{name} 的 HTTP 请求 | frappe.form_dict |
| Scheduler Event | Cron 计划 | (无) |
| Permission Query | 文档列表筛选 | user、conditions |
事件名称映射(Document Events)
关键:界面中的名称与内部钩子名称不同:
| 服务端脚本界面 | 内部钩子 | 触发时机 |
|---|---|---|
| Before Insert | before_insert | 新文档保存到数据库之前 |
| After Insert | after_insert | 首次插入数据库之后 |
| Before Validate | before_validate | 框架验证之前 |
| Before Save | validate | 保存之前(新建 + 更新) |
| After Save | on_update | 保存成功之后 |
| Before Submit | before_submit | 提交之前(docstatus 0→1) |
| After Submit | on_submit | 提交完成之后 |
| Before Cancel | before_cancel | 取消之前(docstatus 1→2) |
| After Cancel | on_cancel | 取消完成之后 |
| Before Delete | on_trash | 永久删除之前 |
| After Delete | after_delete | 永久删除之后 |
切勿将“Before Save”与 before_save 混淆 — 界面标签“Before Save”对应的是 validate 钩子。实际的 before_save 钩子在 validate 之后运行。
决策树:服务端脚本还是文档控制器
Need custom Python logic for a DocType?
│
├─► Can you install a custom Frappe app?
│ ├─► YES: Use a Document Controller when you need:
│ │ • import statements (any Python library)
│ │ • File system access
│ │ • Complex class inheritance
│ │ • autoname / before_naming hooks
│ │ • Unit-testable code
│ │
│ └─► NO: Use a Server Script when:
│ • You only have UI access (no bench CLI)
│ • Logic is simple validation / field calculation
│ • You need a quick API endpoint
│ • You need dynamic permission filtering
│
└─► Is logic > 50 lines or needs external libraries?
├─► YES → Document Controller in a custom app
└─► NO → Server Script is fine
快速参考:沙箱中可用的内容
预加载对象
doc # Current document (Document Event only)
frappe # Core namespace — ALWAYS available
frappe.db # Database operations
frappe.utils # Date, number, string utilities
frappe.session # Current session (user, csrf_token)
frappe.form_dict # Request parameters (API scripts)
frappe.response # Response object (API scripts)
frappe.request # Werkzeug request object
frappe.qb # Query Builder (v14+)
json # Python json module (pre-loaded)
核心方法
# Documents
frappe.get_doc(doctype, name) # Fetch document
frappe.new_doc(doctype) # Create new document
frappe.get_cached_doc(doctype, name) # Cached fetch (read-only)
frappe.get_last_doc(doctype) # Most recent document
frappe.get_mapped_doc(...) # Map fields between DocTypes
frappe.delete_doc(doctype, name) # Delete document
frappe.rename_doc(doctype, old, new) # Rename document
# Querying
frappe.get_all(doctype, filters, fields, order_by, limit) # No permission check
frappe.get_list(doctype, filters, fields, order_by, limit) # With permission check
frappe.db.get_value(doctype, name, fieldname)
frappe.db.get_single_value(doctype, fieldname)
frappe.db.set_value(doctype, name, fieldname, value)
frappe.db.exists(doctype, name_or_filters)
frappe.db.count(doctype, filters)
frappe.db.sql(query, values, as_dict) # ALWAYS parameterize!
frappe.db.escape(value) # SQL escape
frappe.db.commit() # ONLY in Scheduler scripts
frappe.db.rollback() # ONLY in Scheduler scripts
# Messaging
frappe.throw(msg, exc, title) # Stop execution + show error
frappe.msgprint(msg, title, indicator) # User notification
frappe.log_error(message, title) # Error Log entry
# HTTP (yes, these work in sandbox!)
frappe.make_get_request(url, params, headers)
frappe.make_post_request(url, data, headers)
frappe.make_put_request(url, data, headers)
# Email
frappe.sendmail(recipients, sender, subject, message)
# Utilities
frappe.utils.today() # "2024-01-15"
frappe.utils.now() # "2024-01-15 10:30:00"
frappe.utils.now_datetime() # datetime object
frappe.utils.add_days(date, n) # Date arithmetic
frappe.utils.add_months(date, n)
frappe.utils.date_diff(d1, d2) # Days between dates
frappe.utils.flt(val) # Safe float (None → 0.0)
frappe.utils.cint(val) # Safe int (None → 0)
frappe.utils.cstr(val) # Safe string (None → "")
frappe.parse_json(string) # JSON string → dict/list
frappe.as_json(obj) # dict/list → JSON string
frappe.render_template(template, ctx) # Jinja rendering
frappe.get_url() # Site URL
frappe.get_hooks(hook) # Read app hooks
run_script(script_name, **kwargs) # Call another Server Script
# Session / Permissions
frappe.session.user # Current user email
frappe.get_roles(user) # User's roles list
frappe.has_permission(doctype, ptype, doc)
frappe.get_fullname(user) # User's display name
_("translatable string") # Translation function
可用的 Python 内置函数
str, int, float, bool, list, dict, tuple, set # Types
range, enumerate, zip, map, filter # Iteration
sum, min, max, len, sorted, reversed # Aggregation
isinstance, type, hasattr, getattr # Introspection
all, any, abs, round, divmod # Math/logic
print # → server log
True, False, None # Constants
被禁止的 Python 内置函数
open, file # No file I/O
eval, exec, compile # No dynamic code execution
__import__ # No imports (this is the root cause)
globals, locals # No scope introspection
各脚本类型的语法
Document Event
# Config: Reference DocType = Sales Invoice, Event = Before Save
if doc.grand_total < 0:
frappe.throw("Total MUST NOT be negative")
doc.requires_approval = 1 if doc.grand_total > 10000 else 0
API
# Config: API Method = get_customer_orders, Allow Guest = No
# Endpoint: /api/method/get_customer_orders
customer = frappe.form_dict.get("customer")
if not customer:
frappe.throw("Parameter 'customer' is required")
orders = frappe.get_all("Sales Order",
filters={"customer": customer, "docstatus": 1},
fields=["name", "grand_total", "status"],
order_by="creation desc",
limit=20
)
frappe.response["message"] = {"orders": orders, "count": len(orders)}
Scheduler Event
# Config: Event Frequency = Cron, Cron Format = 0 9 * * *
overdue = frappe.get_all("Sales Invoice",
filters={"status": "Unpaid", "due_date": ["<", frappe.utils.today()], "docstatus": 1},
fields=["name", "customer", "grand_total"]
)
for inv in overdue:
frappe.log_error(f"Overdue: {inv.name} ({inv.customer})", "Invoice Reminder")
frappe.db.commit() # ALWAYS commit in Scheduler scripts
Permission Query
# Config: Reference DocType = Sales Invoice
# Variables available: user, conditions
roles = frappe.get_roles(user)
if "System Manager" in roles:
conditions = ""
elif "Sales User" in roles:
conditions = f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
else:
conditions = "1=0"
版本差异
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
| 服务端脚本已启用 | 默认启用 | 默认禁用 | 默认禁用 |
| 启用命令 | 无需设置 | bench set-config -g server_script_enabled 1 | 与 v15 相同 |
frappe.qb(查询构建器) | 可用 | 可用 | 可用 |
用于库的 run_script() | v13+ | 可用 | 可用 |
frappe.make_get_request() | 可用 | 可用 | 可用 |
| Frappe Cloud 共享 bench | 支持 | 不支持 | 不支持 |
5 条首要规则
- 切勿编写
import— 所有内容都在frappe命名空间中 - 切勿在 Before Save 脚本中调用
doc.save()— 会导致无限循环 - 切勿在 Document Event 脚本中调用
frappe.db.commit()— 框架会处理 - 始终在 Scheduler 脚本末尾调用
frappe.db.commit() - 始终使用参数化查询:将
%(var)s与字典搭配使用,SQL 中切勿使用 f-string
参考资料
- references/methods.md — 完整的沙盒 API 参考
- references/events.md — 文档生命周期和执行顺序
- references/examples.md — 每种脚本类型的可运行示例
- references/anti-patterns.md — 沙盒违规行为和常见错误
- references/syntax.md — 快速语法速查表
- references/patterns.md — 常见模式(验证、自动填充、API)
- references/hooks.md — Server Scripts 与 hooks.py 的交互
交叉参考
- frappe-syntax-api — Frappe REST API 和白名单方法
- frappe-syntax-doctype — DocType 字段类型和架构
- frappe-core-database — 深入了解 frappe.db
- frappe-core-permissions — 权限系统架构
- frappe-errors-common — 错误处理模式
评论
支持 Markdown