erpnext-impl-clientscripts
npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-impl-clientscripts --openclawERPNext Client Scripts - Implementation (EN)
This skill helps you determine HOW to implement client-side features. For exact syntax, see erpnext-syntax-clientscripts.
Version: v14/v15/v16 compatible
Main Decision: Client or Server?
┌─────────────────────────────────────────────────────────┐
│ Must the logic ALWAYS execute? │
│ (including imports, API calls, Server Scripts) │
├─────────────────────────────────────────────────────────┤
│ YES → Server-side (Controller or Server Script) │
│ NO → What is the primary goal? │
│ ├── UI feedback/UX improvement → Client Script │
│ ├── Show/hide fields → Client Script │
│ ├── Link filters → Client Script │
│ ├── Data validation → BOTH (client + server) │
│ └── Calculations → Depends on criticality │
└─────────────────────────────────────────────────────────┘
Rule of thumb: Client Scripts for UX, Server for integrity.
Decision Tree: Which Event?
WHAT DO YOU WANT TO ACHIEVE?
│
├─► Set link field filters
│ └── setup (once, early in lifecycle)
│
├─► Add custom buttons
│ └── refresh (after each form load/save)
│
├─► Show/hide fields based on condition
│ └── refresh + {fieldname} (both needed)
│
├─► Validation before save
│ └── validate (use frappe.throw on error)
│
├─► Action after successful save
│ └── after_save
│
├─► Calculation on field change
│ └── {fieldname}
│
├─► Child table row added
│ └── {tablename}_add
│
├─► Child table field changed
│ └── Child DocType event: {fieldname}
│
└─► One-time initialization
└── setup or onload
→ See references/decision-tree.md for complete decision tree.
Implementation Workflows
Workflow 1: Dynamic Field Visibility
Scenario: Show "delivery_date" only when "requires_delivery" is checked.
frappe.ui.form.on('Sales Order', {
refresh(frm) {
// Initial state on form load
frm.trigger('requires_delivery');
},
requires_delivery(frm) {
// Toggle on checkbox change AND refresh
frm.toggle_display('delivery_date', frm.doc.requires_delivery);
frm.toggle_reqd('delivery_date', frm.doc.requires_delivery);
}
});
Why both events?
refresh: Sets correct state when form opens{fieldname}: Responds to user interaction
Workflow 2: Cascading Dropdowns
Scenario: Filter "city" based on selected "country".
frappe.ui.form.on('Customer', {
setup(frm) {
// Filter MUST be in setup for consistency
frm.set_query('city', () => ({
filters: {
country: frm.doc.country || ''
}
}));
},
country(frm) {
// Clear city when country changes
frm.set_value('city', '');
}
});
Workflow 3: Automatic Calculations
Scenario: Calculate total in child table with discount.
frappe.ui.form.on('Sales Invoice', {
discount_percentage(frm) {
calculate_totals(frm);
}
});
frappe.ui.form.on('Sales Invoice Item', {
qty(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
rate(frm, cdt, cdn) {
calculate_row_amount(frm, cdt, cdn);
},
amount(frm) {
// Recalculate document total on row change
calculate_totals(frm);
}
});
function calculate_row_amount(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate);
}
function calculate_totals(frm) {
let total = 0;
(frm.doc.items || []).forEach(row => {
total += row.amount || 0;
});
let discount = total * (frm.doc.discount_percentage || 0) / 100;
frm.set_value('grand_total', total - discount);
}
Workflow 4: Fetching Server Data
Scenario: Populate customer details on customer selection.
frappe.ui.form.on('Sales Order', {
async customer(frm) {
if (!frm.doc.customer) {
// Clear fields if customer cleared
frm.set_value({
customer_name: '',
territory: '',
credit_limit: 0
});
return;
}
// Fetch customer details
let r = await frappe.db.get_value('Customer',
frm.doc.customer,
['customer_name', 'territory', 'credit_limit']
);
if (r.message) {
frm.set_value({
customer_name: r.message.customer_name,
territory: r.message.territory,
credit_limit: r.message.credit_limit
});
}
}
});
Workflow 5: Validation with Server Check
Scenario: Check credit limit before save.
frappe.ui.form.on('Sales Order', {
async validate(frm) {
if (frm.doc.customer && frm.doc.grand_total) {
let r = await frappe.call({
method: 'myapp.api.check_credit',
args: {
customer: frm.doc.customer,
amount: frm.doc.grand_total
}
});
if (r.message && !r.message.allowed) {
frappe.throw(__('Credit limit exceeded. Available: {0}',
[r.message.available]));
}
}
}
});
→ See references/workflows.md for more workflow patterns.
Integration Matrix
| Client Script Action | Requires Server-side |
|---|---|
| Link filters | Optional: custom query |
| Fetch server data | frappe.db.* or whitelisted method |
| Call document method | @frappe.whitelist() in controller |
| Complex validation | Server Script or controller validation |
| Create document | frappe.db.insert or whitelisted method |
Client + Server Combination
// CLIENT: frm.call invokes controller method
frm.call('calculate_taxes')
.then(() => frm.reload_doc());
// SERVER (controller): MUST have @frappe.whitelist
class SalesInvoice(Document):
@frappe.whitelist()
def calculate_taxes(self):
# complex calculation
self.tax_amount = self.grand_total * 0.21
self.save()
Checklist: Implementation Steps
New Client Script Feature
-
[ ] Determine scope
- UI/UX only? → Client script only
- Data integrity? → Also server validation
-
[ ] Choose events
- Use decision tree above
- Combine refresh + fieldname for visibility
-
[ ] Implement basics
- Start with
frappe.ui.form.on - Test with console.log first
- Start with
-
[ ] Add error handling
try/catcharound async callsfrappe.throwfor validation errors
-
[ ] Test edge cases
- New document (frm.is_new())
- Empty field (null checks)
- Child table empty/filled
-
[ ] Translate strings
- All UI text in
__()
- All UI text in
Critical Rules
| Rule | Why |
|---|---|
refresh_field() after child table change | UI synchronization |
set_query in setup event | Consistent filter behavior |
frappe.throw() for validation, not msgprint | Stops save action |
| Async/await for server calls | Prevent race conditions |
Check frm.is_new() for buttons | Prevent errors on new doc |
Related Skills
erpnext-syntax-clientscripts— Exact syntax and method signatureserpnext-errors-clientscripts— Error handling patternserpnext-syntax-whitelisted— Server methods for frm.callerpnext-database— frappe.db.* client-side API
→ See references/examples.md for 10+ complete implementation examples.
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/impl/erpnext-impl-clientscripts/SKILL.mdView on GitHub Overview
This skill helps you decide how to implement client-side features in ERPNext, such as form validations, dynamic UI, server integration, and child-table logic. It emphasizes the main decision: client-side UX versus server-side data integrity, and provides practical workflows for common UI and calculation tasks.
How This Skill Works
Start with the core decision: should the logic always execute on the server or primarily enhance UI on the client? Use the event-based decision tree to map goals (filters, buttons, show/hide, validation, calculations, and child table events). Implement practical workflows like dynamic field visibility, cascading dropdowns, and automatic calculations to build robust client scripts while keeping server-side scripts for data integrity.
When to Use It
- Set link field filters
- Add custom buttons
- Show/hide fields based on condition
- Validation before save
- Calculation on field change
Quick Start
- Step 1: Identify the UX goal and choose the appropriate ERPNext event (setup, refresh, field event, validate, after_save)
- Step 2: Implement with a client script that targets the relevant doctype and fields, focusing on UI behavior and lightweight logic
- Step 3: Test scenarios (visibility, cascading filters, and on-change calculations) and iterate based on results
Best Practices
- Align client-script usage with your UX goals before coding
- Use setup for early lifecycle configuration (initial filters/state)
- Leverage refresh and field-specific events to respond to user actions
- Use client-side validation (e.g., frappe.throw) where appropriate, mirroring critical checks on the server
- Keep client calculations/lightweight and delegate data integrity to server-side logic
Example Use Cases
- Dynamic field visibility: show delivery_date only when requires_delivery is checked
- Cascading dropdowns: filter city options based on selected country
- Automatic calculations: update totals in a Sales Invoice when quantity or rate changes
- Validation before save: enforce business rules prior to form submission
- One-time initialization: run setup/onload to establish initial field states