
erpnext-impl-serverscripts
by OpenAEC-Foundation
28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.
SKILL.md
name: erpnext-impl-serverscripts description: "Implementation workflows and decision trees for ERPNext Server Scripts. Use when determining HOW to implement server-side features: document validation, automated calculations, API endpoints, scheduled tasks, permission filtering. Triggers: how do I implement server-side, when to use server script vs controller, which script type, build custom API, automate validation, schedule task, filter documents per user."
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
- Use
-
[ ] 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
limitto queries - Batch process large datasets
- Add
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 |
Related Skills
erpnext-syntax-serverscripts— Exact syntax and method signatureserpnext-errors-serverscripts— Error handling patternserpnext-database— frappe.db.* operationserpnext-permissions— Permission system detailserpnext-api-patterns— API design patterns
→ See references/examples.md for 10+ complete implementation examples.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon

