erpnext-impl-serverscripts

ERPNext Server Scripts - Implementation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-impl-serverscripts" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-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

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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-database

No summary provided by upstream source.

Repository SourceNeeds Review