- 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
- │ └──
- │
- ├─► Child table row added
- │ └── {tablename}_add
- │
- ├─► Child table field changed
- │ └── Child DocType event:
- │
- └─► 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 [ ] Add error handling try/catch around async calls frappe.throw for validation errors [ ] Test edge cases New document (frm.is_new()) Empty field (null checks) Child table empty/filled [ ] Translate strings 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