Introduction
Suitelets are a way to create custom pages in NetSuite. Many different use cases can be solved with Suitelets, including:
- Record Entry when you need more customization then you can accomplish with a NetSuite form
- Custom Reports
- Custom Dashboards
- Utility Pages
- ..and more
Suitelets have a lot of flexibility.
Entry Points
Suitelets just contain one entry point, the onRequest
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define([],
() => {
/**
* Defines the Suitelet script trigger point.
* @param {Object} scriptContext
* @param {ServerRequest} scriptContext.request - Incoming request
* @param {ServerResponse} scriptContext.response - Suitelet response
* @since 2015.2
*/
const onRequest = (scriptContext) => {
}
return {onRequest}
});
The onRequest
function is called when a request is made to the Suitelet. It contains a scriptContext
object that contains the request
and response
objects.
The request
object contains information about the request, including the method
(GET or POST), the parameters
(query string or form data), and the body
(for POST requests). The response
object is used to send a response back to the client.
Many different types of responses can be sent back to the client, including:
- scriptContext.response.writePage() - Renders a form that is created using the
n/ui/serverWidget
module - scriptContext.response.writeFile() - Sends a file to the client
- scriptContext.response.write() - Sends a string to the client that is interpreted as HTML
- scriptContext.response.renderPdf() - Renders a PDF using a template and data
- ..and more
As you can see, SuiteLets don’t have much of a structure. It is up to the developer to decide how to handle the request and what to send back to the client. Let’s look at some common patterns.
Using SuiteLets to create forms
A common pattern is to divide the onRequest
function into two parts, one handling a GET
request and one handling a POST
request. When a user first visits the page, a GET
request is made and the GET
handler is called. When the user submits the form, a POST
request is made and the POST
handler is called.
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
define(['N/ui/serverWidget'],
(serverWidget) => {
/**
* Defines the Suitelet script trigger point.
* @param {Object} scriptContext
* @param {ServerRequest} scriptContext.request - Incoming request
* @param {ServerResponse} scriptContext.response - Suitelet response
* @since 2015.2
*/
const onRequest = (scriptContext) => {
if(scriptContext.request.method === 'GET') {
handleGet(scriptContext);
} else if(scriptContext.request.method === 'POST') {
handlePost(scriptContext);
}
}
const handleGet = (scriptContext) => {
const form = serverWidget.createForm({ title: 'SuiteLet Example' });
form.addField({id: 'custpage_workorder', label: 'Work Order', type: serverWidget.FieldType.SELECT, source: 'workorder' })
form.addSubmitButton({ label: 'Submit' });
scriptContext.response.writePage(form);
}
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
}
return {onRequest}
});
As you can see, the GET
handler creates a form and sends it to the client. The form has a submit button and one field, custpage_workorder
.
This submit button will cause a POST
request to be made to the Suitelet. This POST
request will contain the value of the custpage_workorder
field.
Don’t worry too much about the n/ui/serverWidget
module. That will be covered in a later post.
Example
Let’s build a Suitelet that generates a SuiteQL report based on the value of a select field. We will be running the report detailed in the post SuiteQL Recipe: Work Order Variance.
The handler for the GET
request will look as detailed above.
The handler for the POST
request will look like this:
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
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
const results = getReportResults(workOrderId);
const html = createHTMLTablefromResults(results);
var form = serverWidget.createForm({ title: 'Work Order Variance Results' });
form.addField({
id: 'custpage_html',
type: serverWidget.FieldType.INLINEHTML,
label: 'Results'
}).defaultValue = html;
scriptContext.response.writePage(form);
}
const getReportResults = (workorderid) => {
const theQuery = `` // fill in the query from the post replacing the workorderid with a template string
return query.runSuiteQL({query: theQuery}).asMappedResults();
}
const createHTMLTablefromResults(results){
//to do
}
As we can see we are returning a form that contains an INLINEHTML
field. In the html field we are returning the results of the report as an HTML table.
Here is a simple implementation of the createHTMLTablefromResults
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const createHTMLTablefromResults = (results) => {
let html = `<table>
<tr>
<th>Item</th>
<th>BOM Quantity</th>
<th>Work Order Variance</th>
<th>Assembly Variance</th>
<th>Consumption Unit</th>
<th>Assembly Cost</th>
</tr>`;
results.forEach((result) => {
html += `<tr>
<td>${result.item}</td>
<td>${result.bomquantity}</td>
<td>${result.workordervariance}</td>
<td>${result.assemblyvariance}</td>
<td>${result.consumptionunit}</td>
<td>${result.assemblycost}</td>
</tr>`;
});
html += '</table>';
return html;
}
Click for the full Suitelet
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/query'],
(serverWidget, query) => {
/**
* Defines the Suitelet script trigger point.
* @param {Object} scriptContext
* @param {ServerRequest} scriptContext.request - Incoming request
* @param {ServerResponse} scriptContext.response - Suitelet response
* @since 2015.2
*/
const onRequest = (scriptContext) => {
if(scriptContext.request.method === 'GET') {
handleGet(scriptContext);
} else if(scriptContext.request.method === 'POST') {
handlePost(scriptContext);
}
}
const handleGet = (scriptContext) => {
const form = serverWidget.createForm({ title: 'SuiteLet Example' });
form.addField({id: 'custpage_workorder', label: 'Work Order', type: serverWidget.FieldType.SELECT, source: 'workorder' })
form.addSubmitButton({ label: 'Submit' });
scriptContext.response.writePage(form);
}
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
const results = getReportResults(workOrderId);
const html = createHTMLTablefromResults(results);
var form = serverWidget.createForm({ title: 'Work Order Variance Results' });
form.addField({
id: 'custpage_html',
type: serverWidget.FieldType.INLINEHTML,
label: 'Results'
}).defaultValue = html;
scriptContext.response.writePage(form);
}
const getReportResults = (workorderid) => {
const theQuery = `
WITH allitems AS (
SELECT
WorkorderLines.item AS item
FROM TransactionLine AS WorkorderLines
WHERE WorkorderLines.transaction = ${workorderid}
and WorkorderLines.mainline = 'F'
UNION
SELECT
BomRevisionComponentMember.item AS item,
FROM BomRevisionComponentMember
INNER JOIN Transaction ON Transaction.id = ${workorderid}
AND Transaction.billofmaterialsrevision = BomRevisionComponentMember.bomrevision
), WorkorderLines AS (
SELECT
WorkorderLines.item AS item,
WorkorderLines.quantity * -1 AS quantity
FROM TransactionLine AS WorkorderLines
WHERE WorkorderLines.transaction = ${workorderid}
and WorkorderLines.mainline = 'F'
), AssemblyBuildLines AS (
SELECT
AssemblyBuildLines.item AS item,
AssemblyBuildLines.quantity * -1 AS quantity
FROM TransactionLine AS AssemblyBuildLines
WHERE AssemblyBuildLines.createdFROM = ${workorderid}
and AssemblyBuildLines.mainline = 'F'
), BomRevisionLines AS (
SELECT
BomRevisionComponentMember.item AS item,
BomRevisionComponentMember.quantity * TransactionLine.quantity * UnitsTypeUom.conversionrate AS quantity
FROM BomRevisionComponentMember
INNER JOIN Transaction ON Transaction.id = 1234
AND Transaction.billofmaterialsrevision = BomRevisionComponentMember.bomrevision
INNER JOIN UnitsTypeUom ON UnitsTypeUom.internalid = BomRevisionComponentMember.units
INNER JOIN TransactionLine ON TransactionLine.transaction = transaction.id and TransactionLine.mainline = 'T'
)
SELECT
item.itemid AS itemid,
(COALESCE(BomRevisionLines.quantity, 0))/ConsumptionUom.conversionrate AS bomquantity,
(COALESCE(BomRevisionLines.quantity, 0) - COALESCE(WorkorderLines.quantity, 0))/ConsumptionUom.conversionrate AS workordervariance,
(COALESCE(BomRevisionLines.quantity, 0) - COALESCE(AssemblyBuildLines.quantity, 0))/ConsumptionUom.conversionrate AS assemblyvariance,
ConsumptionUom.pluralAbbreviation AS consumptionunit,
(COALESCE(AssemblyBuildLines.quantity, 0) * item.averagecost)/ConsumptionUom.conversionrate AS assemblycost
FROM allitems
INNER JOIN Item ON Item.id = allitems.item
INNER JOIN UnitsTypeUom AS ConsumptionUom ON ConsumptionUom.internalid = Item.consumptionunit
LEFT JOIN WorkorderLines ON workorderlines.item = allitems.item
LEFT JOIN AssemblyBuildLines ON AssemblyBuildLines.item = allitems.item
LEFT JOIN BomRevisionLines ON bomrevisionlines.item = allitems.item
` // fill in the query from the post replacing the workorderid with a template string
return query.runSuiteQL({query: theQuery}).asMappedResults();
}
const createHTMLTablefromResults = (results) => {
let html = `<table>
<tr>
<th>Item</th>
<th>BOM Quantity</th>
<th>Work Order Variance</th>
<th>Assembly Variance</th>
<th>Consumption Unit</th>
<th>Assembly Cost</th>
</tr>`;
results.forEach((result) => {
html += `<tr>
<td>${result.item}</td>
<td>${result.bomquantity}</td>
<td>${result.workordervariance}</td>
<td>${result.assemblyvariance}</td>
<td>${result.consumptionunit}</td>
<td>${result.assemblycost}</td>
</tr>`;
});
html += '</table>';
return html;
}
return {onRequest}
});
Writing raw HTML
We can also use the response.write
method to write raw HTML to the page. This can be useful if you want to load external scripts or stylesheets.
Let’s imagine we want to render the previous table as a bootstrap table. While we are able to add scripts and styles an inline HTML field, it isn’t strictly valid HTML as stylesheets are meant to be loaded in the head of the document.
Let’s add a new function createHTMLpage
that will take the html table as a parameter and return a valid html document which we can then write to the page.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
createHTMLpage = (htmlTable) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Document</title>
</head>
<body>
${htmlTable}
</body>
</html>
`
}
Now let’s update our handlePost
function to use this new function.
1
2
3
4
5
6
7
8
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
const results = getReportResults(workOrderId);
const html = createHTMLTablefromResults(results);
const htmlPage = createHTMLpage(html);
scriptContext.response.write(htmlPage);
}
As we can see, we don’t create a form anymore, we just write the html page to the response. We don’t have to load the serverWidget
module anymore either.
Remember to add the bootstrap classes to the table in the createHTMLTablefromResults
function.
Click here to see the full code
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/query'],
(serverWidget) => {
/**
* Defines the Suitelet script trigger point.
* @param {Object} scriptContext
* @param {ServerRequest} scriptContext.request - Incoming request
* @param {ServerResponse} scriptContext.response - Suitelet response
* @since 2015.2
*/
const onRequest = (scriptContext) => {
if(scriptContext.request.method === 'GET') {
handleGet(scriptContext);
} else if(scriptContext.request.method === 'POST') {
handlePost(scriptContext);
}
}
const handleGet = (scriptContext) => {
const form = serverWidget.createForm({ title: 'Enter Work Order' });
form.addField({id: 'custpage_workorder', label: 'Work Order', type: serverWidget.FieldType.SELECT, source: 'workorder' })
form.addSubmitButton({ label: 'Submit' });
scriptContext.response.writePage(form);
}
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
const results = getReportResults(workOrderId);
const html = createHTMLTablefromResults(results);
const htmlPage = createHTMLpage(html);
scriptContext.response.write(htmlPage);
}
const getReportResults = (workorderid) => {
const theQuery = `
WITH allitems AS (
SELECT
WorkorderLines.item AS item
FROM TransactionLine AS WorkorderLines
WHERE WorkorderLines.transaction = ${workorderid}
and WorkorderLines.mainline = 'F'
UNION
SELECT
BomRevisionComponentMember.item AS item,
FROM BomRevisionComponentMember
INNER JOIN Transaction ON Transaction.id = ${workorderid}
AND Transaction.billofmaterialsrevision = BomRevisionComponentMember.bomrevision
), WorkorderLines AS (
SELECT
WorkorderLines.item AS item,
WorkorderLines.quantity * -1 AS quantity
FROM TransactionLine AS WorkorderLines
WHERE WorkorderLines.transaction = ${workorderid}
and WorkorderLines.mainline = 'F'
), AssemblyBuildLines AS (
SELECT
AssemblyBuildLines.item AS item,
AssemblyBuildLines.quantity * -1 AS quantity
FROM TransactionLine AS AssemblyBuildLines
WHERE AssemblyBuildLines.createdFROM = ${workorderid}
and AssemblyBuildLines.mainline = 'F'
), BomRevisionLines AS (
SELECT
BomRevisionComponentMember.item AS item,
BomRevisionComponentMember.quantity * TransactionLine.quantity * UnitsTypeUom.conversionrate AS quantity
FROM BomRevisionComponentMember
INNER JOIN Transaction ON Transaction.id = 1234
AND Transaction.billofmaterialsrevision = BomRevisionComponentMember.bomrevision
INNER JOIN UnitsTypeUom ON UnitsTypeUom.internalid = BomRevisionComponentMember.units
INNER JOIN TransactionLine ON TransactionLine.transaction = transaction.id and TransactionLine.mainline = 'T'
)
SELECT
item.itemid AS itemid,
(COALESCE(BomRevisionLines.quantity, 0))/ConsumptionUom.conversionrate AS bomquantity,
(COALESCE(BomRevisionLines.quantity, 0) - COALESCE(WorkorderLines.quantity, 0))/ConsumptionUom.conversionrate AS workordervariance,
(COALESCE(BomRevisionLines.quantity, 0) - COALESCE(AssemblyBuildLines.quantity, 0))/ConsumptionUom.conversionrate AS assemblyvariance,
ConsumptionUom.pluralAbbreviation AS consumptionunit,
(COALESCE(AssemblyBuildLines.quantity, 0) * item.averagecost)/ConsumptionUom.conversionrate AS assemblycost
FROM allitems
INNER JOIN Item ON Item.id = allitems.item
INNER JOIN UnitsTypeUom AS ConsumptionUom ON ConsumptionUom.internalid = Item.consumptionunit
LEFT JOIN WorkorderLines ON workorderlines.item = allitems.item
LEFT JOIN AssemblyBuildLines ON AssemblyBuildLines.item = allitems.item
LEFT JOIN BomRevisionLines ON bomrevisionlines.item = allitems.item
` // fill in the query from the post replacing the workorderid with a template string
return query.runSuiteQL({query: theQuery}).asMappedResults();
}
const createHTMLTablefromResults = (results) => {
let html = `<table class="table table-striped">
<tr>
<th>Item</th>
<th>BOM Quantity</th>
<th>Work Order Variance</th>
<th>Assembly Variance</th>
<th>Consumption Unit</th>
<th>Assembly Cost</th>
</tr>`;
results.forEach((result) => {
html += `<tr>
<td>${result.item}</td>
<td>${result.bomquantity}</td>
<td>${result.workordervariance}</td>
<td>${result.assemblyvariance}</td>
<td>${result.consumptionunit}</td>
<td>${result.assemblycost}</td>
</tr>`;
});
html += '</table>';
return html;
}
createHTMLpage = (htmlTable) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Document</title>
</head>
<body>
${htmlTable}
</body>
</html>
`
}
return {onRequest}
});
Using a SuiteLet to trigger a Map/Reduce script
This is a simple example of how to trigger a Map/Reduce script from a SuiteLet. The Suitelet form will again have one field for the Work Order ID. The POST request will trigger the Map/Reduce script and then the SuiteLet will simply write a message to the page to let the user know that the script has been triggered.
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
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/task'],(task) => {
const onRequest = (scriptContext) => {
if(scriptContext.request.method === 'GET') {
handleGet(scriptContext);
} else if(scriptContext.request.method === 'POST') {
handlePost(scriptContext);
}
}
const handleGet = (scriptContext) => {
const form = serverWidget.createForm({ title: 'SuiteLet Example' });
form.addField({id: 'custpage_workorder', label: 'Work Order', type: serverWidget.FieldType.SELECT, source: 'workorder' })
form.addSubmitButton({ label: 'Submit' });
scriptContext.response.writePage(form);
}
const handlePost = (scriptContext) => {
const workOrderId = scriptContext.request.parameters.custpage_workorder;
const mrTask = task.create({
taskType: task.TaskType.MAP_REDUCE,
scriptId: 'customscript_mr_script',
deploymentId: 'customdeploy_mr_script',
params: {custscript_mr_workorder: workOrderId}
});
mrTask.submit();
scriptContext.response.write('Map/Reduce script has been triggered');
}
})
Replace the scriptId and deploymentId with the script and deployment you want to trigger. Make sure a parameter exists in the Map/Reduce script that matches the name of the parameter you are passing in the params object.
Conclusion
I hope this has been helpful and given you some ideas on how to use SuiteLets in your NetSuite projects. Suitelets remain one of the most ubiquitous tools in the NetSuite developer’s toolbox so it’s important to understand how they work and how to use them effectively.
More examples of Suitelets will be coming soon so stay tuned!