365 Logo365 Docs

🎨 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

  1. Open the Awesome Bar (search) at the top
  2. Type "Client Script" and select "Client Script List"
  3. 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.