
erpnext-impl-clientscripts
by OpenAEC-Foundation
28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.
SKILL.md
name: erpnext-impl-clientscripts description: "Implementation workflows and decision trees for ERPNext Client Scripts. Use when determining HOW to implement client-side features: form validations, dynamic UI, server integration, child table logic. Triggers: how do I implement, when to use, which approach, client script workflow, build form logic, make UI dynamic, calculate fields."
ERPNext 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.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon

