erpnext-impl-serverscripts
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Server Scripts - Implementation
ERPNext Server Scripts - 实现指南
This skill helps you determine HOW to implement server-side features. For exact syntax, see .
erpnext-syntax-serverscriptsVersion: 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 Name | Internal Hook | Best For |
|---|---|---|
| Before Validate | | Pre-validation setup |
| Before Save | | All validation + auto-calc |
| After Save | | Notifications, audit logs |
| Before Submit | | Submit-time validation |
| After Submit | | Post-submit automation |
| Before Cancel | | Cancel prevention |
| After Cancel | | Cleanup after cancel |
| After Insert | | Create related docs |
| Before Delete | | Delete prevention |
| UI名称 | 内部钩子 | 最佳适用场景 |
|---|---|---|
| 验证前 | | 验证前准备工作 |
| 保存前 | | 所有验证与自动计算 |
| 保存后 | | 通知、审计日志 |
| 提交前 | | 提交时验证 |
| 提交后 | | 提交后自动化操作 |
| 取消前 | | 阻止取消操作 |
| 取消后 | | 取消后的清理工作 |
| 插入后 | | 创建关联文档 |
| 删除前 | | 阻止删除操作 |
Implementation Workflows
实现工作流
Workflow 1: Validation with Conditional Logic
工作流1:带条件逻辑的验证
Scenario: Validate sales order based on customer credit limit.
python
undefined场景:根据客户信用额度验证销售订单。
python
undefinedConfiguration:
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"
)
undefinedtotal_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"
)
undefinedWorkflow 2: Auto-Calculate and Auto-Fill
工作流2:自动计算与自动填充
Scenario: Auto-calculate totals and set derived fields.
python
undefined场景:自动计算总计并设置派生字段。
python
undefinedConfiguration:
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")
undefinedif doc.supplier and not doc.supplier_name:
doc.supplier_name = frappe.db.get_value("Supplier", doc.supplier, "supplier_name")
undefinedWorkflow 3: Create Related Document
工作流3:创建关联文档
Scenario: Create ToDo when document is inserted.
python
undefined场景:插入文档时创建待办事项。
python
undefinedConfiguration:
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)
undefinedfrappe.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)
undefinedWorkflow 4: Custom API Endpoint
工作流4:自定义API端点
Scenario: Create API to fetch customer dashboard data.
python
undefined场景:创建用于获取客户仪表盘数据的API。
python
undefinedConfiguration:
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
}
undefinedorders = 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
}
undefinedWorkflow 5: Scheduled Task
工作流5:定时任务
Scenario: Daily reminder for overdue invoices.
python
undefined场景:逾期发票的每日提醒。
python
undefinedConfiguration:
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
undefinedtoday = 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
undefinedWorkflow 6: Permission Query
工作流6:权限查询
Scenario: Filter documents by user's territory.
python
undefined场景:按用户所属区域过滤文档。
python
undefinedConfiguration:
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".territory = {frappe.db.escape(user_territory)}"
else:
conditions = f".owner = {frappe.db.escape(user)}"
tabCustomertabCustomer
→ 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".territory = {frappe.db.escape(user_territory)}"
else:
conditions = f".owner = {frappe.db.escape(user)}"
tabCustomertabCustomer
→ 更多工作流模式请查看[references/workflows.md](references/workflows.md)。Integration: Client Script + Server Script
集成:客户端脚本 + 服务器脚本
| Client Script Calls | Server Script Provides |
|---|---|
| API type script |
| Direct DB (no script needed) |
| Controller method (not Server Script) |
| 客户端脚本调用 | 服务器脚本提供 |
|---|---|
| API类型脚本 |
| 直接调用数据库(无需脚本) |
| 控制器方法(非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
undefinedjavascript
// 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
undefinedSERVER: 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)
}
undefinedcustomer = 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)
}
undefinedChecklist: Implementation Steps
检查清单:实现步骤
New Server Script Feature
新Server Script功能
-
[ ] Determine script type
- Document lifecycle? → Document Event
- Custom API? → API
- Scheduled job? → Scheduler Event
- List filtering? → Permission Query
-
[ ] Check sandbox limitations
- No imports needed? → Proceed
- Need imports? → Use Controller instead
-
[ ] Implement core logic
- Use directly
frappe.utils.* - Use for database
frappe.db.*
- Use
-
[ ] Add validation & error handling
- for user errors
frappe.throw() - Input validation for API scripts
-
[ ] Test edge cases
- Empty values (null checks)
- Permission scenarios
- Large data volumes (add limits)
-
[ ] Scheduler-specific
- Add at end
frappe.db.commit() - Add to queries
limit - Batch process large datasets
- Add
-
[ ] 确定脚本类型
- 文档生命周期相关?→ 文档事件
- 自定义API?→ API
- 定时任务?→ 调度器事件
- 列表过滤?→ 权限查询
-
[ ] 检查沙箱限制
- 无需导入?→ 继续
- 需要导入?→ 改用控制器
-
[ ] 实现核心逻辑
- 直接使用
frappe.utils.* - 使用操作数据库
frappe.db.*
- 直接使用
-
[ ] 添加验证与错误处理
- 使用抛出用户可见错误
frappe.throw() - API脚本需添加输入验证
- 使用
-
[ ] 测试边缘场景
- 空值(空值检查)
- 权限场景
- 大数据量(添加限制)
-
[ ] 调度器脚本专属检查
- 在末尾添加
frappe.db.commit() - 查询中添加
limit - 大数据集需分批处理
- 在末尾添加
Critical Rules
重要规则
| Rule | Why |
|---|---|
NO | Sandbox blocks all imports |
| Changes not auto-committed |
NO | Framework handles save |
| Stops document operation |
| Always escape user input in SQL | Prevent SQL injection |
Add | Prevent memory issues |
| 规则 | 原因 |
|---|---|
禁止使用 | 沙箱会阻止所有导入操作 |
调度器脚本中必须调用 | 更改不会自动提交 |
保存前事件中禁止调用 | 框架会自动处理保存操作 |
验证时使用 | 会终止文档操作 |
| SQL中始终转义用户输入 | 防止SQL注入 |
查询中添加 | 避免内存问题 |
Related Skills
相关技能
- — Exact syntax and method signatures
erpnext-syntax-serverscripts - — Error handling patterns
erpnext-errors-serverscripts - — frappe.db.* operations
erpnext-database - — Permission system details
erpnext-permissions - — API design patterns
erpnext-api-patterns
→ See references/examples.md for 10+ complete implementation examples.
- — 具体语法与方法签名
erpnext-syntax-serverscripts - — 错误处理模式
erpnext-errors-serverscripts - — frappe.db.*操作
erpnext-database - — 权限系统详情
erpnext-permissions - — API设计模式
erpnext-api-patterns
→ 10+完整实现示例请查看references/examples.md。