erpnext-impl-serverscripts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Server Scripts - Implementation

ERPNext Server Scripts - 实现指南

This skill helps you determine HOW to implement server-side features. For exact syntax, see
erpnext-syntax-serverscripts
.
Version: v14/v15/v16 compatible
本技能可帮助你确定如何实现服务器端功能。如需具体语法,请参考
erpnext-syntax-serverscripts
版本:兼容v14/v15/v16

CRITICAL: Sandbox Limitation

重要提示:沙箱限制

┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️  ALL IMPORTS BLOCKED IN SERVER SCRIPTS                          │
├─────────────────────────────────────────────────────────────────────┤
│ import json              → ImportError: __import__ not found       │
│ from frappe.utils import → ImportError                             │
│                                                                     │
│ SOLUTION: Use pre-loaded namespace directly:                       │
│   frappe.utils.nowdate()      frappe.parse_json(data)              │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️  ALL IMPORTS BLOCKED IN SERVER SCRIPTS                          │
├─────────────────────────────────────────────────────────────────────┤
│ import json              → ImportError: __import__ not found       │
│ from frappe.utils import → ImportError                             │
│                                                                     │
│ SOLUTION: Use pre-loaded namespace directly:                       │
│   frappe.utils.nowdate()      frappe.parse_json(data)              │
└─────────────────────────────────────────────────────────────────────┘

Main Decision: Server Script vs Controller?

核心决策:使用Server Script还是控制器?

┌───────────────────────────────────────────────────────────────────┐
│ WHAT DO YOU NEED?                                                 │
├───────────────────────────────────────────────────────────────────┤
│                                                                   │
│ ► No custom app / Quick prototyping                               │
│   └── Server Script ✓                                             │
│                                                                   │
│ ► Import external libraries (requests, pandas, etc.)              │
│   └── Controller (in custom app)                                  │
│                                                                   │
│ ► Complex multi-document transactions                             │
│   └── Controller (full Python, try/except/rollback)               │
│                                                                   │
│ ► Simple validation / auto-fill / notifications                   │
│   └── Server Script ✓                                             │
│                                                                   │
│ ► Create REST API without custom app                              │
│   └── Server Script API type ✓                                    │
│                                                                   │
│ ► Scheduled background job                                        │
│   └── Server Script Scheduler type ✓ (simple)                     │
│   └── hooks.py scheduler_events (complex)                         │
│                                                                   │
│ ► Dynamic list filtering per user                                 │
│   └── Server Script Permission Query type ✓                       │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘
Rule of thumb: Server Scripts for no-code/low-code solutions within Frappe's sandbox. Controllers for full Python power.
┌───────────────────────────────────────────────────────────────────┐
│ 你的需求是什么?                                                 │
├───────────────────────────────────────────────────────────────────┤
│                                                                   │
│ ► 无需自定义应用 / 快速原型开发                                   │
│   └── Server Script ✓                                             │
│                                                                   │
│ ► 需要导入外部库(requests、pandas等)                            │
│   └── 控制器(在自定义应用中)                                    │
│                                                                   │
│ ► 复杂的多文档事务                                               │
│   └── 控制器(完整Python支持,try/except/回滚)                   │
│                                                                   │
│ ► 简单验证 / 自动填充 / 通知                                     │
│   └── Server Script ✓                                             │
│                                                                   │
│ ► 无需自定义应用即可创建REST API                                  │
│   └── Server Script(API类型)✓                                    │
│                                                                   │
│ ► 定时后台任务                                                   │
│   └── Server Script(调度器事件类型)✓(简单场景)                 │
│   └── hooks.py scheduler_events(复杂场景)                         │
│                                                                   │
│ ► 按用户过滤列表视图                                             │
│   └── Server Script(权限查询类型)✓                               │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘
经验法则:在Frappe沙箱环境中,Server Script适用于无代码/低代码解决方案;控制器则适用于需要完整Python能力的场景。

Decision Tree: Which Script Type?

决策树:选择哪种脚本类型?

WHAT DO YOU WANT TO ACHIEVE?
├─► React to document lifecycle (save/submit/cancel)?
│   └── Document Event
│       └── Which event? See event mapping below
├─► Create REST API endpoint?
│   └── API
│       ├── Public endpoint? → Allow Guest: Yes
│       └── Authenticated? → Allow Guest: No
├─► Run task on schedule (daily/hourly)?
│   └── Scheduler Event
│       └── Define cron pattern
└─► Filter list view per user/role/territory?
    └── Permission Query
        └── Return conditions string for WHERE clause
→ See references/decision-tree.md for complete decision tree.
你想要实现什么功能?
├─► 响应文档生命周期(保存/提交/取消)?
│   └── 文档事件
│       └── 选择哪个事件?请查看下方的事件映射
├─► 创建REST API端点?
│   └── API
│       ├── 公开端点?→ 允许访客:是
│       └── 需要认证?→ 允许访客:否
├─► 按计划运行任务(每日/每小时)?
│   └── 调度器事件
│       └── 定义cron表达式
└─► 按用户/角色/区域过滤列表视图?
    └── 权限查询
        └── 返回WHERE子句的条件字符串
→ 完整决策树请查看references/decision-tree.md

Event Name Mapping (Document Event)

事件名称映射(文档事件)

UI NameInternal HookBest For
Before Validate
before_validate
Pre-validation setup
Before Save
validate
All validation + auto-calc
After Save
on_update
Notifications, audit logs
Before Submit
before_submit
Submit-time validation
After Submit
on_submit
Post-submit automation
Before Cancel
before_cancel
Cancel prevention
After Cancel
on_cancel
Cleanup after cancel
After Insert
after_insert
Create related docs
Before Delete
on_trash
Delete prevention
UI名称内部钩子最佳适用场景
验证前
before_validate
验证前准备工作
保存前
validate
所有验证与自动计算
保存后
on_update
通知、审计日志
提交前
before_submit
提交时验证
提交后
on_submit
提交后自动化操作
取消前
before_cancel
阻止取消操作
取消后
on_cancel
取消后的清理工作
插入后
after_insert
创建关联文档
删除前
on_trash
阻止删除操作

Implementation Workflows

实现工作流

Workflow 1: Validation with Conditional Logic

工作流1:带条件逻辑的验证

Scenario: Validate sales order based on customer credit limit.
python
undefined
场景:根据客户信用额度验证销售订单。
python
undefined

Configuration:

Configuration:

Type: Document Event

Type: Document Event

DocType Event: Before Save

DocType Event: Before Save

Reference DocType: Sales Order

Reference DocType: Sales Order

Get customer's credit limit

Get customer's credit limit

credit_limit = frappe.db.get_value("Customer", doc.customer, "credit_limit") or 0
credit_limit = frappe.db.get_value("Customer", doc.customer, "credit_limit") or 0

Check outstanding

Check outstanding

outstanding = frappe.db.get_value( "Sales Invoice", filters={"customer": doc.customer, "docstatus": 1, "status": "Unpaid"}, fieldname="sum(outstanding_amount)" ) or 0
outstanding = frappe.db.get_value( "Sales Invoice", filters={"customer": doc.customer, "docstatus": 1, "status": "Unpaid"}, fieldname="sum(outstanding_amount)" ) or 0

Validate

Validate

total_exposure = outstanding + doc.grand_total if credit_limit > 0 and total_exposure > credit_limit: frappe.throw( f"Credit limit exceeded. Limit: {credit_limit}, Exposure: {total_exposure}", title="Credit Limit Error" )
undefined
total_exposure = outstanding + doc.grand_total if credit_limit > 0 and total_exposure > credit_limit: frappe.throw( f"Credit limit exceeded. Limit: {credit_limit}, Exposure: {total_exposure}", title="Credit Limit Error" )
undefined

Workflow 2: Auto-Calculate and Auto-Fill

工作流2:自动计算与自动填充

Scenario: Auto-calculate totals and set derived fields.
python
undefined
场景:自动计算总计并设置派生字段。
python
undefined

Configuration:

Configuration:

Type: Document Event

Type: Document Event

DocType Event: Before Save

DocType Event: Before Save

Reference DocType: Purchase Order

Reference DocType: Purchase Order

Calculate from child table

Calculate from child table

doc.total_qty = sum(item.qty or 0 for item in doc.items) doc.total_amount = sum(item.amount or 0 for item in doc.items)
doc.total_qty = sum(item.qty or 0 for item in doc.items) doc.total_amount = sum(item.amount or 0 for item in doc.items)

Set derived fields

Set derived fields

if doc.total_amount > 50000: doc.requires_approval = 1 doc.approval_status = "Pending"
if doc.total_amount > 50000: doc.requires_approval = 1 doc.approval_status = "Pending"

Auto-fill from linked document

Auto-fill from linked document

if doc.supplier and not doc.supplier_name: doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name")
undefined
if doc.supplier and not doc.supplier_name: doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name")
undefined

Workflow 3: Create Related Document

工作流3:创建关联文档

Scenario: Create ToDo when document is inserted.
python
undefined
场景:插入文档时创建待办事项。
python
undefined

Configuration:

Configuration:

Type: Document Event

Type: Document Event

DocType Event: After Insert

DocType Event: After Insert

Reference DocType: Lead

Reference DocType: Lead

Create follow-up task

Create follow-up task

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)
undefined
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)
undefined

Workflow 4: Custom API Endpoint

工作流4:自定义API端点

Scenario: Create API to fetch customer dashboard data.
python
undefined
场景:创建用于获取客户仪表盘数据的API。
python
undefined

Configuration:

Configuration:

Type: API

Type: API

API Method: get_customer_dashboard

API Method: get_customer_dashboard

Allow Guest: No

Allow Guest: No

Endpoint: /api/method/get_customer_dashboard

Endpoint: /api/method/get_customer_dashboard

customer = frappe.form_dict.get("customer") if not customer: frappe.throw("Parameter 'customer' is required")
customer = frappe.form_dict.get("customer") if not customer: frappe.throw("Parameter 'customer' is required")

Permission check

Permission check

if not frappe.has_permission("Customer", "read", customer): frappe.throw("Access denied", frappe.PermissionError)
if not frappe.has_permission("Customer", "read", customer): frappe.throw("Access denied", frappe.PermissionError)

Aggregate data

Aggregate data

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 }
undefined
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 }
undefined

Workflow 5: Scheduled Task

工作流5:定时任务

Scenario: Daily reminder for overdue invoices.
python
undefined
场景:逾期发票的每日提醒。
python
undefined

Configuration:

Configuration:

Type: Scheduler Event

Type: Scheduler Event

Event Frequency: Cron

Event Frequency: Cron

Cron Format: 0 9 * * * (daily at 9:00)

Cron Format: 0 9 * * * (daily at 9:00)

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=100 )
for inv in overdue: days_overdue = frappe.utils.date_diff(today, inv.due_date)
# Create ToDo if not exists
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_overdue} days overdue (${inv.grand_total})"
    }).insert(ignore_permissions=True)
frappe.db.commit() # REQUIRED in scheduler scripts
undefined
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=100 )
for inv in overdue: days_overdue = frappe.utils.date_diff(today, inv.due_date)
# Create ToDo if not exists
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_overdue} days overdue (${inv.grand_total})"
    }).insert(ignore_permissions=True)
frappe.db.commit() # REQUIRED in scheduler scripts
undefined

Workflow 6: Permission Query

工作流6:权限查询

Scenario: Filter documents by user's territory.
python
undefined
场景:按用户所属区域过滤文档。
python
undefined

Configuration:

Configuration:

Type: Permission Query

Type: Permission Query

Reference DocType: Customer

Reference 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)}"

→ See [references/workflows.md](references/workflows.md) for more workflow patterns.
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)}"

→ 更多工作流模式请查看[references/workflows.md](references/workflows.md)。

Integration: Client Script + Server Script

集成:客户端脚本 + 服务器脚本

Client Script CallsServer Script Provides
frappe.call({method: 'api_name'})
API type script
frappe.db.get_value()
Direct DB (no script needed)
frm.call('method')
Controller method (not Server Script)
客户端脚本调用服务器脚本提供
frappe.call({method: 'api_name'})
API类型脚本
frappe.db.get_value()
直接调用数据库(无需脚本)
frm.call('method')
控制器方法(非Server Script)

Combined Pattern

组合模式

javascript
// CLIENT: Call server API
frappe.call({
    method: 'check_credit_limit',
    args: {
        customer: frm.doc.customer,
        amount: frm.doc.grand_total
    },
    callback: function(r) {
        if (!r.message.allowed) {
            frappe.throw(__('Credit limit exceeded'));
        }
    }
});
python
undefined
javascript
// CLIENT: Call server API
frappe.call({
    method: 'check_credit_limit',
    args: {
        customer: frm.doc.customer,
        amount: frm.doc.grand_total
    },
    callback: function(r) {
        if (!r.message.allowed) {
            frappe.throw(__('Credit limit exceeded'));
        }
    }
});
python
undefined

SERVER: API script 'check_credit_limit'

SERVER: API script 'check_credit_limit'

customer = frappe.form_dict.get("customer") amount = frappe.utils.flt(frappe.form_dict.get("amount"))
credit_limit = frappe.db.get_value("Customer", customer, "credit_limit") or 0 outstanding = frappe.db.get_value( "Sales Invoice", {"customer": customer, "docstatus": 1, "status": "Unpaid"}, "sum(outstanding_amount)" ) or 0
frappe.response["message"] = { "allowed": (outstanding + amount) <= credit_limit or credit_limit == 0, "available": max(0, credit_limit - outstanding) }
undefined
customer = frappe.form_dict.get("customer") amount = frappe.utils.flt(frappe.form_dict.get("amount"))
credit_limit = frappe.db.get_value("Customer", customer, "credit_limit") or 0 outstanding = frappe.db.get_value( "Sales Invoice", {"customer": customer, "docstatus": 1, "status": "Unpaid"}, "sum(outstanding_amount)" ) or 0
frappe.response["message"] = { "allowed": (outstanding + amount) <= credit_limit or credit_limit == 0, "available": max(0, credit_limit - outstanding) }
undefined

Checklist: Implementation Steps

检查清单:实现步骤

New Server Script Feature

新Server Script功能

  1. [ ] Determine script type
    • Document lifecycle? → Document Event
    • Custom API? → API
    • Scheduled job? → Scheduler Event
    • List filtering? → Permission Query
  2. [ ] Check sandbox limitations
    • No imports needed? → Proceed
    • Need imports? → Use Controller instead
  3. [ ] Implement core logic
    • Use
      frappe.utils.*
      directly
    • Use
      frappe.db.*
      for database
  4. [ ] Add validation & error handling
    • frappe.throw()
      for user errors
    • Input validation for API scripts
  5. [ ] Test edge cases
    • Empty values (null checks)
    • Permission scenarios
    • Large data volumes (add limits)
  6. [ ] Scheduler-specific
    • Add
      frappe.db.commit()
      at end
    • Add
      limit
      to queries
    • Batch process large datasets
  1. [ ] 确定脚本类型
    • 文档生命周期相关?→ 文档事件
    • 自定义API?→ API
    • 定时任务?→ 调度器事件
    • 列表过滤?→ 权限查询
  2. [ ] 检查沙箱限制
    • 无需导入?→ 继续
    • 需要导入?→ 改用控制器
  3. [ ] 实现核心逻辑
    • 直接使用
      frappe.utils.*
    • 使用
      frappe.db.*
      操作数据库
  4. [ ] 添加验证与错误处理
    • 使用
      frappe.throw()
      抛出用户可见错误
    • API脚本需添加输入验证
  5. [ ] 测试边缘场景
    • 空值(空值检查)
    • 权限场景
    • 大数据量(添加限制)
  6. [ ] 调度器脚本专属检查
    • 在末尾添加
      frappe.db.commit()
    • 查询中添加
      limit
    • 大数据集需分批处理

Critical Rules

重要规则

RuleWhy
NO
import
statements
Sandbox blocks all imports
frappe.db.commit()
in Scheduler
Changes not auto-committed
NO
doc.save()
in Before Save
Framework handles save
frappe.throw()
for validation
Stops document operation
Always escape user input in SQLPrevent SQL injection
Add
limit
to queries
Prevent memory issues
规则原因
禁止使用
import
语句
沙箱会阻止所有导入操作
调度器脚本中必须调用
frappe.db.commit()
更改不会自动提交
保存前事件中禁止调用
doc.save()
框架会自动处理保存操作
验证时使用
frappe.throw()
会终止文档操作
SQL中始终转义用户输入防止SQL注入
查询中添加
limit
避免内存问题

Related Skills

相关技能

  • erpnext-syntax-serverscripts
    — Exact syntax and method signatures
  • erpnext-errors-serverscripts
    — Error handling patterns
  • erpnext-database
    — frappe.db.* operations
  • erpnext-permissions
    — Permission system details
  • erpnext-api-patterns
    — API design patterns
→ See references/examples.md for 10+ complete implementation examples.
  • erpnext-syntax-serverscripts
    — 具体语法与方法签名
  • erpnext-errors-serverscripts
    — 错误处理模式
  • erpnext-database
    — frappe.db.*操作
  • erpnext-permissions
    — 权限系统详情
  • erpnext-api-patterns
    — API设计模式
→ 10+完整实现示例请查看references/examples.md