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:

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:

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

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:

  1. 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 + "-.#####"
  2. 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.")
  3. 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
  4. 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
  5. 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}",
    )
  6. 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,
            })
  7. 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}")
  8. 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)
  9. 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()
  10. 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:

  1. 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');
      }
    });
  2. 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]));
      }
    });
  3. 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);
          }
        });
      }
    });
  4. 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);
      }
    });
  5. 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.'));
          });
        }
      }
    });
  6. 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);
    }
  7. 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.'));
        }
      }
    });
  8. 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();
        });
      }
    });
  9. 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);
      }
    });
  10. 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

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.

ERPNext Partner
WhatsApp Support ×

Hello 👋

How can we help you?

Chat on WhatsApp