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.