erpnext-impl-serverscripts

安装

npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill erpnext-impl-serverscripts
ERPNext Server Scripts - Implementation
This skill helps you determine HOW to implement server-side features. For exact syntax, see
erpnext-syntax-serverscripts
.
Version
v14/v15/v16 compatible
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) │
└─────────────────────────────────────────────────────────────────────┘
Main Decision: Server Script vs Controller?
┌───────────────────────────────────────────────────────────────────┐
│ 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.
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.
Event Name Mapping (Document Event)
UI Name
Internal Hook
Best 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
Implementation Workflows
Workflow 1: Validation with Conditional Logic
Scenario
Validate sales order based on customer credit limit.

Configuration:

Type: Document Event

DocType Event: Before Save

Reference DocType: Sales Order

Get customer's credit limit

credit_limit

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

Check outstanding

outstanding

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

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"
)
Workflow 2: Auto-Calculate and Auto-Fill
Scenario
Auto-calculate totals and set derived fields.

Configuration:

Type: Document Event

DocType Event: Before Save

Reference DocType: Purchase Order

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 )

Set derived fields

if doc . total_amount

50000 : doc . requires_approval = 1 doc . approval_status = "Pending"

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"
)
Workflow 3: Create Related Document
Scenario
Create ToDo when document is inserted.

Configuration:

Type: Document Event

DocType Event: After Insert

Reference DocType: Lead

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
)
Workflow 4: Custom API Endpoint
Scenario
Create API to fetch customer dashboard data.

Configuration:

Type: API

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

Permission check

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

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
}
Workflow 5: Scheduled Task
Scenario
Daily reminder for overdue invoices.

Configuration:

Type: Scheduler Event

Event Frequency: Cron

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

Workflow 6: Permission Query
Scenario
Filter documents by user's territory.

Configuration:

Type: Permission Query

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 for more workflow patterns. Integration: Client Script + Server Script Client Script Calls Server 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) Combined Pattern // 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' ) ) ; } } } ) ;

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 ) } Checklist: Implementation Steps New Server Script Feature [ ] 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 frappe.utils. directly Use frappe.db. for database [ ] Add validation & error handling frappe.throw() for user errors Input validation for API scripts [ ] Test edge cases Empty values (null checks) Permission scenarios Large data volumes (add limits) [ ] Scheduler-specific Add frappe.db.commit() at end Add limit to queries Batch process large datasets Critical Rules Rule Why 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 SQL Prevent SQL injection Add limit to queries Prevent memory issues

返回排行榜