Home Anatomy of SuiteScript: User Event
Post
Cancel

Anatomy of SuiteScript: User Event

Introduction

User event scripts are scripts that run on the server side when a record is loaded or edited. Because it runs on the server side, it can not modify the UI of the record after it has been loaded. It can only modify the form as it is being loaded with the beforeLoad entry point.

Entry Points

User event scripts have three entry points: beforeLoad, beforeSubmit, and afterSubmit.

beforeLoad

This script is ran before the record is loaded either in the UI or even when calling record.load in another SuiteScript.

Let’s look at the JSdoc for the beforeLoad entry point:

1
2
3
4
5
6
7
8
9
        /**
         * Defines the function definition that is executed before record is loaded.
         * @param {Object} scriptContext
         * @param {Record} scriptContext.newRecord - New record
         * @param {string} scriptContext.type - Trigger type; use values from the context.UserEventType enum
         * @param {Form} scriptContext.form - Current form
         * @param {ServletRequest} scriptContext.request - HTTP request information sent from the browser for a client action only.
         * @since 2015.2
         */

scriptContext.newRecord is the record that is being loaded. Note that you can not update this record, it is only for reading.

scriptContext.type is the type of event that triggered the script. Some common types are create, edit, view, and copy.

So if we want to run some code only when creating a record we can write:

1
2
3
4
5
        const beforeLoad = (scriptContext) => {
            if (scriptContext.type === "create") {
                // do something
            }
        }

There is an enum in the scriptContext object that contains all the possible values. It is good practice to use the enum instead of hard coding the string.

1
2
3
4
5
        const beforeLoad = (scriptContext) => {
            if (scriptContext.type === scriptContext.UserEventType.CREATE) {
                // do something
            }
        }

We also have access to the scriptContext.form object.

This object is a representation of the form that is being loaded. We can use this object to modify the form as it is being loaded.

For example, we can hide a field on the form:

1
2
3
4
5
        const beforeLoad = (scriptContext) => {
              scriptContext.form
              .getField({id: "custrecord_my_field"})
              .updateDisplayType({displayType: "hidden"});
        }

beforeSubmit

This script is ran before the record is submitted. As soon as the user presses the save button, the script runs. If the script raises an error, the record is not saved.

Let’s look at the JSdoc tag for this record:

1
2
3
4
5
6
7
8
        /**
         * Defines the function definition that is executed before record is submitted.
         * @param {Object} scriptContext
         * @param {Record} scriptContext.newRecord - New record
         * @param {Record} scriptContext.oldRecord - Old record
         * @param {string} scriptContext.type - Trigger type; use values from the context.UserEventType enum
         * @since 2015.2
         */

The scriptContext.type is the same as the beforeLoad entry point but there are more possible values.

User event types like approve, cancel, xedit aren’t ever loaded in a conventional sense and therefore don’t trigger the beforeLoad entry point.

the edit event is triggered when the user clicks the edit button on the record. The xedit event is triggered when inline editing a list.

We have two record objects: scriptContext.newRecord and scriptContext.oldRecord. scriptContext.newRecord is the record that is being submitted with all the changes. scriptContext.oldRecord is the record as it was loaded before the changes.

We can use these two objects to compare the changes and do some logic based on the changes.

For example if we want to make sure that the Document Number - internally named tranid - is not changed after the record is submitted we can write:

1
2
3
4
5
6
7
        const beforeSubmit = (scriptContext) => {
            if (scriptContext.type === scriptContext.UserEventType.EDIT) {
                if (scriptContext.newRecord.getValue({fieldId: "tranid"}) !== scriptContext.oldRecord.getValue({fieldId: "tranid"})) {
                    throw "Document Number can not be changed after the record is submitted"; // in production I create a proper error with 'N/error'
                }
            }
        }

afterSubmit

This entry point is ran after the record is submitted. If the script raises an error, unlike the BeforeSubmit entry point, the record is saved. This is the cause of much confusion among developers.

Let’s look at the JSDoc for this entry point:

1
2
3
4
5
6
7
8
        /**
         * Defines the function definition that is executed after record is submitted.
         * @param {Object} scriptContext
         * @param {Record} scriptContext.newRecord - New record
         * @param {Record} scriptContext.oldRecord - Old record
         * @param {string} scriptContext.type - Trigger type; use values from the context.UserEventType enum
         * @since 2015.2
         */

As you can see, the scriptContext object is the same as the beforeSubmit entry point.

We can use this entry point to do some logic after the record is submitted. A common use case is creating a another record based on the submitted record.

beforeSubmit vs afterSubmit

beforeSubmit and afterSubmit are very similar.

Because beforeSubmit will prevent the record from being saved in case of an error, it is a good place to do some validation. Another place to do validation is in the client side script in the saveRecord entry point. The saveRecord entry point doesn’t have access to the oldRecord object so it is not possible to do validation based on the old record. Because of the design of the beforeSubmit entry point, the user will lose all the changes they made if there is an error.

afterSubmit is a good place to do some logic after the record is saved. If any error is raised before the record is saved, NetSuite won’t trigger the afterSubmit entry point.

Example script

In this example script we will be using the afterSubmit entry point to check inventory for the lines on a work order. If the inventory is missing, we will send an email to the user listing the item that don’t have enough inventory and what inventory they do have.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
@NApiVersion 2.1
@NScriptType UserEventScript
*/
define(['N/record', 'N/query', 'N/email', 'N/runtime'], (record, query, email, runtime) => {

   const afterSubmit = (scriptContext) => {
    const rec = scriptContext.newRecord
    const lines = rec.getLineCount({sublistId: "item"})

    for(let i = 0; i < lines; i++) {
        const item = rec.getSublistValue({sublistId: "item", fieldId: "item", line: i})
        const location = rec.getSublistValue({sublistId: "item", fieldId: "location", line: i})
        const quantity = rec.getSublistValue({sublistId: "item", fieldId: "quantity", line: i})

        const itemdisplay = rec.getSublistText({sublistId: "item", fieldId: "item", line: i})
        const locationdisplay = rec.getSublistText({sublistId: "item", fieldId: "location", line: i})

        const inventory = getInventory(item, location)
        const quantityatlocation = inventory.filter((inv) => inv.locationid === location).reduce((acc, inv) => acc + inv.quantityonhand, 0)

        if(quantityatlocation < quantity) {
          sendEmail(itemdisplay, locationdisplay, quantity, quantityatlocation, inventory)
        }
    }



   }

   const getInventory = (item, location) => {
       const theQuery = `
       SELECT
       quantityonhand,
        BUILTIN.DF(location) AS location
        BUILTIN.DF(binnumber) AS bin
        BUILTIN.DF(inventorynumber) AS lot
        location AS locationid,
        from inventorybalance
       WHERE item = ${item}
       `
       return query.runSuiteQL({query: theQuery}).asMappedResults()
   }


   const sendEmail = (tranid, item, location, quantityrequired, quantityonhand, inventory) => {
        const useremail = runtime.getCurrentUser().email

        const body = `
        <p>There is not enough inventory for ${item} at ${location}</p>
        <p>Quantity required: ${quantityrequired}</p>
        <p>Quantity on hand: ${quantityonhand}</p>
        <p>Inventory details:</p>
        <table>
        <tr>
            <th>Location</th>
            <th>Bin</th>
            <th>Lot</th>
            <th>Quantity</th>
        </tr>
        ${inventory.map((inv) => `
        <tr>
            <td>${inv.location}</td>
            <td>${inv.bin}</td>
            <td>${inv.lot}</td>
            <td>${inv.quantityonhand}</td>
        </tr>
        `).join("")}
        </table>
        `

        email.send({
          author: 1,
          recipients: useremail,
          subject: "Not enough inventory for " + tranid + " at " + location,
          body: body
        })

   }


   return {
        afterSubmit
   }

})

In this example we are iterating over the lines on the work order and checking the inventory for each line. If the inventory is missing, we are sending an email to the user with the details of the missing inventory. A separate line will be sent for each item that is missing inventory. In a real world scenario, you would probably want to send a single email with all the missing inventory.

The getInventory function is using the query module to run a SuiteQL query to get the inventory for the item at the location. The sendEmail function is using the email module to send an email to the user with the details of the missing inventory. I used the author id 1 in the email.send function. Usually I would create a user in the system that is used for sending emails from scripts.

Conclusion

User event scripts are a great way to add some logic to your NetSuite system.

In this post we looked at the different entry points for user event scripts. We also looked at the beforeSubmit and afterSubmit entry points and how they are different. We looked at an example script that uses the afterSubmit entry point to perform some checks after a record is submitted.

In the next post we will look at client scripts and how they can be used to perform certain actions on a page.

This post is licensed under CC BY 4.0 by the author.

Anatomy of SuiteScript: Script Deconstruction

Anatomy of SuiteScript: Client Script