mcpField
F
frappe-syntax-serverscripts

frappe-syntax-serverscripts

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

关于 frappe-syntax-serverscripts

面向 Frappe Server Scripts 的实用参考技能,说明如何在 RestrictedPython 沙箱中编写脚本,并介绍脚本类型、可用 API、生命周期事件及其限制。

主要功能

  1. 01Document Event、API、Scheduler 与 Permission Query 脚本
  2. 02RestrictedPython 沙箱规则和可用方法
  3. 03文档生命周期事件映射
  4. 04各版本的启用说明
  5. 05安全与查询安全指南

适用场景

  1. 01为文档事件添加校验逻辑
  2. 02在 Frappe 中创建 API 或定时脚本
  3. 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.md
events.md
examples.md
hooks.md
methods.md
patterns.md
syntax.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 jsonfrappe.parse_json() / frappe.as_json()
from datetime import datefrappe.utils.today() / frappe.utils.now_datetime()
from frappe.utils import cintfrappe.utils.cint()(已加载)
import requestsfrappe.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 EventCron 计划(无)
Permission Query文档列表筛选user、conditions

事件名称映射(Document Events)

关键:界面中的名称与内部钩子名称不同:

服务端脚本界面内部钩子触发时机
Before Insertbefore_insert新文档保存到数据库之前
After Insertafter_insert首次插入数据库之后
Before Validatebefore_validate框架验证之前
Before Savevalidate保存之前(新建 + 更新)
After Saveon_update保存成功之后
Before Submitbefore_submit提交之前(docstatus 0→1)
After Submiton_submit提交完成之后
Before Cancelbefore_cancel取消之前(docstatus 1→2)
After Cancelon_cancel取消完成之后
Before Deleteon_trash永久删除之前
After Deleteafter_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"

版本差异

功能v14v15v16
服务端脚本已启用默认启用默认禁用默认禁用
启用命令无需设置bench set-config -g server_script_enabled 1与 v15 相同
frappe.qb(查询构建器)可用可用可用
用于库的 run_script()v13+可用可用
frappe.make_get_request()可用可用可用
Frappe Cloud 共享 bench支持不支持不支持

5 条首要规则

  1. 切勿编写 import — 所有内容都在 frappe 命名空间中
  2. 切勿在 Before Save 脚本中调用 doc.save() — 会导致无限循环
  3. 切勿在 Document Event 脚本中调用 frappe.db.commit() — 框架会处理
  4. 始终在 Scheduler 脚本末尾调用 frappe.db.commit()
  5. 始终使用参数化查询:将 %(var)s 与字典搭配使用,SQL 中切勿使用 f-string

参考资料

交叉参考

  • frappe-syntax-api — Frappe REST API 和白名单方法
  • frappe-syntax-doctype — DocType 字段类型和架构
  • frappe-core-database — 深入了解 frappe.db
  • frappe-core-permissions — 权限系统架构
  • frappe-errors-common — 错误处理模式

评论

支持 Markdown