ERPNext Server and Client Scripts: Making things work better and making the user experience better
ERPNext is a strong open-source enterprise resource planning (ERP) system that gives companies a full set of tools to run their businesses. One of the best things about ERPNext is that you can use server and client scripts to change and add to its features. These scripts let users customise the system to fit their needs, automate boring tasks, and make the experience better for everyone. This article will talk about the basics of server and client scripts in ERPNext, give examples of scripts for both types, and explain how Zikpro, a certified Frappe partner, helps clients write scripts for ERPNext.
How to Understand Server and Client Scripts
Server Scripts:
- Validating data
- Making custom workflows
- Following business rules
- Connections to systems outside of the company
Python is used to write server scripts, which take advantage of the Frappe Framework. In ERPNext v16 you create them from the Server Script list: choose a Script Type (DocType Event, Scheduler Event, Permission Query, API or Workflow Task) and, for a DocType Event, the Reference Document Type and the event. The script is plain Python that works on doc, with no function definition or decorator. Server Scripts are switched off by default; a system administrator turns them on with bench set-config -g server_script_enabled 1.
Client Scripts:
- Improvements to the user interface
- Validations of forms
- Visibility of dynamic fields
- Calculations on the client side
Client scripts are written in JavaScript and change the form dynamically through the Frappe form API (frm). In ERPNext v16 you add them from the Client Script list: pick the DocType, set Apply To to Form, tick Enabled and paste the code.
Advantages of Using Server and Client Scripts in ERPNext
- Customisation: Companies can customise ERPNext to fit their own processes and workflows, making sure that the system meets their operational needs.
- Automation: Scripts can do the same tasks over and over again, which cuts down on the work that needs to be done by hand and lowers the chance of making mistakes.
- Better User Experience: Client scripts make ERPNext easier to use by adding dynamic features that make the interface easier to understand.
- Flexibility: Businesses can quickly adapt to new needs or connect with new systems by making their own scripts.
Server Scripts in Action
Here are ten server scripts, each tested on ERPNext v16. The first comment line of each one shows the settings to use:
- Create Serial Numbers Automatically
# Script Type: DocType Event | Reference Document Type: Item | Event: Before Save # Give every serialised item a serial number series, so ERPNext # creates the serial numbers automatically on receipt (e.g. ITEM-00001). if doc.has_serial_no and not doc.serial_no_series: doc.serial_no_series = doc.item_code + "-.#####" - Check Payment Terms
# Script Type: DocType Event | Reference Document Type: Sales Order | Event: Before Save allowed_terms = ["Net 30", "Net 60"] if doc.payment_terms_template and doc.payment_terms_template not in allowed_terms: frappe.throw("Invalid payment terms. Pick either Net 30 or Net 60.") - Set the Default Warehouse
# Script Type: DocType Event | Reference Document Type: Purchase Order | Event: Before Validate # Use the default warehouse from Stock Settings when none is picked. if not doc.set_warehouse: default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") if default_warehouse and frappe.db.get_value("Warehouse", default_warehouse, "company") == doc.company: doc.set_warehouse = default_warehouse for item in doc.items: if not item.warehouse: item.warehouse = default_warehouse - Change the levels of stock
# Script Type: API | API Method: add_stock # Stock levels change only through stock transactions, so this creates # and submits a Material Receipt. Call it with a POST request to # /api/method/add_stock with item_code, qty and warehouse. args = frappe.form_dict stock_entry = frappe.get_doc({ "doctype": "Stock Entry", "stock_entry_type": "Material Receipt", "company": frappe.db.get_value("Warehouse", args.warehouse, "company"), "items": [{ "item_code": args.item_code, "qty": frappe.utils.flt(args.qty), "t_warehouse": args.warehouse, }], }) stock_entry.insert() stock_entry.submit() frappe.response["message"] = stock_entry.name - Send an email alert
# Script Type: DocType Event | Reference Document Type: Lead | Event: After Insert frappe.sendmail( recipients=["sales@example.com"], subject="New Lead Created", message=f"New lead created: {doc.lead_name}", ) - Figure Out How Much Tax
# Script Type: DocType Event | Reference Document Type: Sales Invoice | Event: Before Validate # Add a 5% VAT row when no taxes are set; ERPNext then calculates # the tax amount and the grand total itself. if not doc.taxes: tax_account = frappe.db.get_value( "Account", {"company": doc.company, "account_name": "VAT", "is_group": 0} ) if tax_account: doc.append("taxes", { "charge_type": "On Net Total", "account_head": tax_account, "description": "VAT 5%", "rate": 5, }) - Check to make sure that the stock is available
# Script Type: DocType Event | Reference Document Type: Sales Order | Event: Before Submit for item in doc.items: if not frappe.db.get_value("Item", item.item_code, "is_stock_item"): continue available = frappe.db.get_value( "Bin", {"item_code": item.item_code, "warehouse": item.warehouse}, "actual_qty" ) or 0 if available < item.stock_qty: frappe.throw(f"Insufficient stock for {item.item_code} in {item.warehouse}. Available: {available}") - Automatically Assign Tasks
# Script Type: DocType Event | Reference Document Type: Task | Event: After Insert # Assign each new task to the first user with the Projects Manager role. managers = frappe.get_all( "Has Role", filters={"role": "Projects Manager", "parenttype": "User", "parent": ["!=", "Administrator"]}, pluck="parent", ) if managers: frappe.get_doc({ "doctype": "ToDo", "allocated_to": managers[0], "reference_type": "Task", "reference_name": doc.name, "description": doc.subject, }).insert(ignore_permissions=True) - Make entries in the accounting system
# Script Type: DocType Event | Reference Document Type: Sales Invoice | Event: After Submit # Book a 2% sales commission as a Journal Entry. commission = frappe.utils.flt(doc.base_net_total * 0.02, 2) if commission: company = frappe.db.get_value("Company", doc.company, ["abbr", "cost_center"], as_dict=True) journal_entry = frappe.get_doc({ "doctype": "Journal Entry", "voucher_type": "Journal Entry", "company": doc.company, "posting_date": doc.posting_date, "user_remark": f"2% sales commission on {doc.name}", "accounts": [ {"account": "Commission on Sales - " + company.abbr, "cost_center": company.cost_center, "debit_in_account_currency": commission}, {"account": "Cash - " + company.abbr, "credit_in_account_currency": commission}, ], }) journal_entry.insert() journal_entry.submit() - Tell Customers
# Script Type: DocType Event | Reference Document Type: Delivery Note | Event: After Submit customer_email = doc.contact_email or frappe.db.get_value("Customer", doc.customer, "email_id") if customer_email: frappe.sendmail( recipients=[customer_email], subject="Order Shipped", message=f"Your order has been shipped with delivery note {doc.name}.", )
Client Scripts in Action
Here are ten client scripts that can make using ERPNext better for users, each tested on ERPNext v16:
- Hide Field Based on What You Choose
// Client Script | DocType: Sales Order | Apply To: Form // Maintenance orders ship no stock, so hide the Source Warehouse. frappe.ui.form.on('Sales Order', { refresh(frm) { frm.trigger('order_type'); }, order_type(frm) { frm.toggle_display('set_warehouse', frm.doc.order_type !== 'Maintenance'); } }); - Show Alert on Form Submission
// Client Script | DocType: Quotation | Apply To: Form frappe.ui.form.on('Quotation', { on_submit(frm) { frappe.msgprint(__('Quotation {0} has been submitted.', [frm.doc.name])); } }); - Auto-Fill Fields
// Client Script | DocType: Task | Apply To: Form // When a project is picked, fill the task's Expected End Date // from the project's Expected End Date. frappe.ui.form.on('Task', { project(frm) { if (!frm.doc.project || frm.doc.exp_end_date) return; frappe.db.get_value('Project', frm.doc.project, 'expected_end_date').then(r => { if (r.message.expected_end_date) { frm.set_value('exp_end_date', r.message.expected_end_date); } }); } }); - Change Colour of a Field
// Client Script | DocType: Sales Invoice | Apply To: Form // Show the Outstanding Amount in red while money is owed, green once paid. frappe.ui.form.on('Sales Invoice', { refresh(frm) { const colour = frm.doc.outstanding_amount > 0 ? 'red' : 'green'; frm.get_field('outstanding_amount').$wrapper .find('.control-value, .control-input input') .css('color', colour); } }); - Show Messages That Change
// Client Script | DocType: Employee | Apply To: Form frappe.ui.form.on('Employee', { refresh(frm) { frm.trigger('employment_type'); }, employment_type(frm) { frm.remove_custom_button(__('Send Contract')); if (frm.doc.employment_type === 'Contract') { frm.add_custom_button(__('Send Contract'), () => { frappe.msgprint(__('Contract will be sent to the employee.')); }); } } }); - Calculate Total Amount
// Client Script | DocType: Opportunity | Apply To: Form // Keep the Opportunity Amount equal to the total of the item rows. frappe.ui.form.on('Opportunity Item', { amount(frm) { set_opportunity_amount(frm); }, items_remove(frm) { set_opportunity_amount(frm); } }); function set_opportunity_amount(frm) { let total = 0; (frm.doc.items || []).forEach(row => total += flt(row.amount)); frm.set_value('opportunity_amount', total); } - Validate Email Format
// Client Script | DocType: Lead | Apply To: Form frappe.ui.form.on('Lead', { validate(frm) { const email_regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (frm.doc.email_id && !email_regex.test(frm.doc.email_id)) { frappe.throw(__('Please enter a valid email address.')); } } }); - Complete Task Button
// Client Script | DocType: Task | Apply To: Form frappe.ui.form.on('Task', { refresh(frm) { if (frm.is_new() || frm.doc.status === 'Completed') return; frm.add_custom_button(__('Complete Task'), () => { frm.set_value('status', 'Completed'); frm.set_value('completed_on', frappe.datetime.get_today()); frm.save(); }); } }); - Enable/Disable Fields
// Client Script | DocType: Sales Order | Apply To: Form // Only Sales Managers can change the additional discount. frappe.ui.form.on('Sales Order', { refresh(frm) { const can_discount = frappe.user.has_role('Sales Manager'); frm.toggle_enable(['additional_discount_percentage', 'discount_amount'], can_discount); } }); - Change Field Properties Dynamically
// Client Script | DocType: Purchase Invoice | Apply To: Form // Make the Set Accepted Warehouse mandatory when Update Stock is ticked. frappe.ui.form.on('Purchase Invoice', { refresh(frm) { frm.trigger('update_stock'); }, update_stock(frm) { frm.set_df_property('set_warehouse', 'reqd', frm.doc.update_stock ? 1 : 0); } });
How Zikpro Helps Clients with ERPNext Server and Client Scripts
- Customised Solutions: Zikpro works closely with clients to learn about their specific business processes and then makes custom server and client scripts that fit those needs.
- Best Practices: The team follows the best practices in scripting that are used in the industry to make sure that the scripts work well and are reliable.
- Training and Support: Zikpro offers training sessions to help clients learn how to keep their scripts up to date and running smoothly, giving them the tools they need to effectively manage their ERPNext environment.
- Troubleshooting and Maintenance: The Zikpro team is always available to help clients fix problems with their scripts, making sure that their operations are not too affected.
- Integration Services: Zikpro helps you connect ERPNext scripts to other systems, which improves functionality and makes operations easier across platforms.
- Regular Updates: Zikpro makes sure that clients’ scripts are always up to date with the newest features and security improvements as ERPNext changes.
In short, Zikpro’s knowledge of ERPNext Server and Client Scripts not only makes the system work better, but it also makes the whole business run more smoothly, which makes it a good partner for companies that use ERPNext.
