🎨 Client Scripts
Add custom JavaScript functionality to enhance your ERPNext forms and user experience
🎨 Client Scripts Guide
Client Scripts in ERPNext allow you to add custom JavaScript functionality to forms and documents. This powerful feature lets you create interactive user experiences, validate data, and automate form behaviors without modifying the core system.
What are Client Scripts? Custom JavaScript code that runs in the browser to enhance form functionality, validate data, and create interactive user experiences.
🚀 Getting Started with Client Scripts
Step 1: Access Client Script Setup
- Open the Awesome Bar (search) at the top
- Type "Client Script" and select "Client Script List"
- Click the blue "+ Add Client Script" button
Step 2: Basic Configuration
Script Details:
- Name: Give your script a descriptive name (e.g., "Sales Order Auto-Calculate")
- Document Type: Select the form where the script will run (e.g., "Sales Order")
- Enabled: ✅ Check to activate the script
🎯 Common Client Script Examples
✅ Field Validation Scripts
Example: Validate Email Format
frappe.ui.form.on('Customer', {
email_id: function(frm) {
if (frm.doc.email_id) {
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(frm.doc.email_id)) {
frappe.msgprint(__('Please enter a valid email address'));
frm.set_value('email_id', '');
}
}
}
});Example: Validate Phone Number
frappe.ui.form.on('Customer', {
mobile_no: function(frm) {
if (frm.doc.mobile_no) {
// Remove all non-digits
let cleaned = frm.doc.mobile_no.replace(/\D/g, '');
if (cleaned.length !== 10) {
frappe.msgprint(__('Phone number must be 10 digits'));
frm.set_value('mobile_no', '');
} else {
// Format as (123) 456-7890
let formatted = `(\${cleaned.substr(0,3)}) \${cleaned.substr(3,3)}-\${cleaned.substr(6,4)}`;
frm.set_value('mobile_no', formatted);
}
}
}
});Best Practice: Always provide clear error messages to help users understand what went wrong.
🧮 Auto-Calculation Scripts
Example: Calculate Total with Discount
frappe.ui.form.on('Sales Order', {
quantity: function(frm) {
calculate_total(frm);
},
rate: function(frm) {
calculate_total(frm);
},
discount_percentage: function(frm) {
calculate_total(frm);
}
});
function calculate_total(frm) {
if (frm.doc.quantity && frm.doc.rate) {
let subtotal = frm.doc.quantity * frm.doc.rate;
let discount = subtotal * (frm.doc.discount_percentage || 0) / 100;
let total = subtotal - discount;
frm.set_value('subtotal', subtotal);
frm.set_value('discount_amount', discount);
frm.set_value('total', total);
}
}Example: Calculate Age from Date of Birth
frappe.ui.form.on('Employee', {
date_of_birth: function(frm) {
if (frm.doc.date_of_birth) {
let today = new Date();
let birthDate = new Date(frm.doc.date_of_birth);
let age = today.getFullYear() - birthDate.getFullYear();
// Adjust if birthday hasn't occurred this year
if (today.getMonth() < birthDate.getMonth() ||
(today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate())) {
age--;
}
frm.set_value('age', age);
}
}
});🎭 Dynamic Form Behavior
Example: Show/Hide Fields Based on Selection
frappe.ui.form.on('Customer', {
customer_type: function(frm) {
if (frm.doc.customer_type === 'Company') {
// Show company-specific fields
frm.set_df_property('company_registration', 'hidden', 0);
frm.set_df_property('tax_id', 'hidden', 0);
// Hide individual-specific fields
frm.set_df_property('date_of_birth', 'hidden', 1);
frm.set_df_property('gender', 'hidden', 1);
} else {
// Hide company-specific fields
frm.set_df_property('company_registration', 'hidden', 1);
frm.set_df_property('tax_id', 'hidden', 1);
// Show individual-specific fields
frm.set_df_property('date_of_birth', 'hidden', 0);
frm.set_df_property('gender', 'hidden', 0);
}
}
});Example: Filter Related Documents
frappe.ui.form.on('Sales Order', {
customer: function(frm) {
// Filter items based on customer
frm.set_query('item_code', 'items', function() {
return {
filters: {
'customer': frm.doc.customer
}
};
});
}
});✨ Form Enhancement Scripts
Example: Add Custom Buttons
frappe.ui.form.on('Sales Order', {
refresh: function(frm) {
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__('Send Email'), function() {
send_custom_email(frm);
}, __('Actions'));
frm.add_custom_button(__('Generate Report'), function() {
generate_custom_report(frm);
}, __('Actions'));
}
}
});
function send_custom_email(frm) {
frappe.call({
method: 'your_app.api.send_sales_order_email',
args: {
'sales_order': frm.doc.name
},
callback: function(r) {
if (r.message) {
frappe.msgprint(__('Email sent successfully!'));
}
}
});
}Example: Format Fields Automatically
frappe.ui.form.on('Customer', {
customer_name: function(frm) {
if (frm.doc.customer_name) {
// Auto-capitalize first letter of each word
let formatted = frm.doc.customer_name
.toLowerCase()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
frm.set_value('customer_name', formatted);
}
}
});🎯 Advanced Client Script Techniques
API Calls and Data Fetching
frappe.ui.form.on("Sales Order", {
customer: function (frm) {
if (frm.doc.customer) {
// Fetch customer details
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Customer",
name: frm.doc.customer,
},
callback: function (r) {
if (r.message) {
frm.set_value("billing_address", r.message.primary_address);
frm.set_value("territory", r.message.territory);
}
},
});
}
},
});Working with Child Tables
frappe.ui.form.on("Sales Order Item", {
item_code: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.item_code) {
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Item",
name: row.item_code,
},
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, "rate", r.message.standard_rate);
frappe.model.set_value(
cdt,
cdn,
"description",
r.message.description
);
}
},
});
}
},
});🛠️ Client Script Best Practices
🎯 Performance
Keep scripts lightweight and efficient
- Avoid heavy computations in frequently triggered events - Use debouncing for input validation - Cache API results when possible - Minimize DOM manipulations
🔒 Error Handling
Always handle errors gracefully
- Use try-catch blocks for critical operations - Provide meaningful error messages to users - Log errors for debugging purposes - Test scripts thoroughly before deployment
📝 Code Organization
Write maintainable and readable code
- Use descriptive function and variable names - Add comments to explain complex logic - Follow consistent coding style - Break large scripts into smaller functions
🔄 Testing
Ensure scripts work reliably
- Test with different user roles and permissions - Verify scripts work on mobile devices - Test edge cases and error scenarios - Use browser developer tools for debugging
🎮 Interactive Examples
Real-Time Form Calculator
Create a dynamic calculator that updates as users type:
frappe.ui.form.on("Quotation", {
onload: function (frm) {
// Add custom CSS for better styling
$("<style>")
.text(
".calculator-summary { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0; }"
)
.appendTo("head");
},
refresh: function (frm) {
// Add custom summary section
if (frm.doc.items && frm.doc.items.length > 0) {
update_calculator_summary(frm);
}
},
});
frappe.ui.form.on("Quotation Item", {
qty: function (frm, cdt, cdn) {
calculate_item_total(frm, cdt, cdn);
update_calculator_summary(frm);
},
rate: function (frm, cdt, cdn) {
calculate_item_total(frm, cdt, cdn);
update_calculator_summary(frm);
},
});
function calculate_item_total(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.qty && row.rate) {
let amount = row.qty * row.rate;
frappe.model.set_value(cdt, cdn, "amount", amount);
}
}
function update_calculator_summary(frm) {
let total_items = frm.doc.items.length;
let total_qty = frm.doc.items.reduce((sum, item) => sum + (item.qty || 0), 0);
let total_amount = frm.doc.items.reduce(
(sum, item) => sum + (item.amount || 0),
0
);
let summary_html = `
<div class="calculator-summary">
<h4>📊 Order Summary</h4>
<div class="row">
<div class="col-md-4">
<strong>Total Items:</strong> \${total_items}
</div>
<div class="col-md-4">
<strong>Total Quantity:</strong> \${total_qty}
</div>
<div class="col-md-4">
<strong>Total Amount:</strong> \${format_currency(total_amount)}
</div>
</div>
</div>
`;
frm.set_df_property("items", "description", summary_html);
}🚨 Troubleshooting Common Issues
Problem: Script not executing
Solutions:
- ✅ Check if "Enabled" is checked
- ✅ Verify the Document Type is correct
- ✅ Check browser console for JavaScript errors
- ✅ Ensure proper event names are used
Problem: Fields not updating
Solutions:
- ✅ Use
frm.set_value()instead of direct assignment - ✅ Check field names match exactly (case-sensitive)
- ✅ Verify field permissions allow editing
- ✅ Use
frm.refresh_field()if needed
Problem: Script conflicts
Solutions:
- ✅ Check for multiple scripts on the same DocType
- ✅ Use unique function names
- ✅ Test scripts individually
- ✅ Check execution order
🎉 Ready to enhance your forms? Start with simple validation scripts and gradually build more complex functionality as you become comfortable with the ERPNext client-side API.