Home Anatomy of SuiteScript: Suitelet
Post
Cancel

Anatomy of SuiteScript: Suitelet

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!

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

Anatomy of SuiteScript: Restlet

SuiteQL Recipe: Bills With Just Credits