Claude-skill-registry erpnext-impl-clientscripts
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/erpnext-impl-clientscripts" ~/.claude/skills/majiayu000-claude-skill-registry-erpnext-impl-clientscripts && rm -rf "$T"
skills/data/erpnext-impl-clientscripts/SKILL.mdERPNext 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?
: Sets correct state when form opensrefresh
: Responds to user interaction{fieldname}
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 | or whitelisted method |
| Call document method | in controller |
| Complex validation | Server Script or controller validation |
| Create document | 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
around async callstry/catch
for validation errorsfrappe.throw
-
[ ] 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 |
|---|---|
after child table change | UI synchronization |
in event | Consistent filter behavior |
for validation, not | Stops save action |
| Async/await for server calls | Prevent race conditions |
Check for buttons | Prevent errors on new doc |
Related Skills
— Exact syntax and method signatureserpnext-syntax-clientscripts
— Error handling patternserpnext-errors-clientscripts
— Server methods for frm.callerpnext-syntax-whitelisted
— frappe.db.* client-side APIerpnext-database
→ See references/examples.md for 10+ complete implementation examples.