Back to list
OpenAEC-Foundation

erpnext-impl-serverscripts

by OpenAEC-Foundation

28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.

1🍴 1📅 Jan 23, 2026

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 NameInternal HookBest For
Before Validatebefore_validatePre-validation setup
Before SavevalidateAll validation + auto-calc
After Saveon_updateNotifications, audit logs
Before Submitbefore_submitSubmit-time validation
After Submiton_submitPost-submit automation
Before Cancelbefore_cancelCancel prevention
After Cancelon_cancelCleanup after cancel
After Insertafter_insertCreate related docs
Before Deleteon_trashDelete 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")

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

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

  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

Critical Rules

RuleWhy
NO import statementsSandbox blocks all imports
frappe.db.commit() in SchedulerChanges not auto-committed
NO doc.save() in Before SaveFramework handles save
frappe.throw() for validationStops document operation
Always escape user input in SQLPrevent SQL injection
Add limit to queriesPrevent memory issues
  • 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.

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon