NetSuite – Feel the Love!

This month, I’m entering retirement. I’ve been writing software for over 4 decades and since 2017 it’s been almost exclusively spent customizing NetSuite. My time with NetSuite started out somewhat rocky. In fact my very first blog article was a scathing rebuke of the NetSuite platform and support. As I retire, I’d like to bookend with this. It is my pleasure to report good things about NetSuite.

I find NetSuite incredibly extensible. As a developer, you are free to write your own single page apps, meaning you can inject custom pages anywhere in NetSuite. I like writing “modules” comprised of a Suitelet (the page), an HTML file for any statically defined markup, a Restlet (AKA restful API), often times I include a module-specific library, and finally a client script. This offers a clear separation of concerns. You can create your own data tables, custom record types. You can add custom fields to existing records. You can form your own relationships (parent-child) between all types of records. The sky is the limit!

I really like the selection of JavaScript for both server side and client side code. Over the years, the platform has progressed through SuiteScript 1.0, 2.0 and most recently 2.1. SuiteScript 2.1 is JavaScript ES6, which makes it closely compatible with the latest browser supported JavaScript. Many platform-specific methods are available on the server side and client-side. A call to search.lookupFields() works in a Suitelet, Restlet, or Client Script. A JavaScript method like Array.sort() works in server or client code. Unlike other platforms, where a developer might be writing C# on the server side and JavaScript on the client side, NetSuite reduces complexity by standardizing on one language and one set of platform-specific methods.

There are many standard reports, but you also have the option of writing saved searches, which can be printed much like a report. Printed transactions, like Estimates, Sales Orders, Invoices, Pick Tickets, Purchase orders, etc. can be customized in 2 ways. You have the option of using a non-developer basic form. Or you can have someone like me customize an Advanced HTML/PDF Template, which is a combination of HTML and Freemarker templating language.

NetSuite offers developers the option of creating background tasks, like Schedule Scripts. If a Scheduled Script becomes too needy and is monopolizing server-side resources, there’s the option of breaking it up into multiples tasks via a Map/Reduce Script.

A developer can inject custom code into a standard form, like viewing or editing a sales order. This is done through User Event Scripts and custom fields. Its even possible to commandeer a section of a standard form, like editing a sales order and injecting a single page app into a section of that form. Basically the sky’s the limit!

I’ve rambled on enough. In summary: I love working with NetSuite and I love writing JavaScript. And, I wanted to give NetSuite some love now that I’ve worked with it for over 8 years. To NetSuite’s credit, they’ve come a long way in a positive direction regarding both platform and support. If you are not a NetSuite customer, it’s worth a look!

I’ll also end with this shameless plug. I am retiring, but I’d kind of like to keep working with NetSuite. If you need any part time help, or want advice on a project, feel free to contact me. I updated the About page with more info about me. Hit the Contact page or comment on this post if you want to get in touch with me. I’ll still be around.

Cheers,
–Kevin

SuiteScript Example: Saved Search to Excel

I thought I was clever, years ago when I hacked a way to create an Excel worksheet using SuiteScript. I never shared my work in this blog. Fast forward to today. I expected NetSuite would have a method for exporting an Excel worksheet incorporated into native SuiteScript. Unfortunately, this is still not true. But thanks to a very intelligent and energetic NetSuite Support Rep, Ehsan Salimi, I have an example to share.

The code below is written by Ehsan. It is 100% his work, and I take no credit and provide no warranty for it. Feel free to use it as an example, but at your own risk. It comes with no warranty (I have to say that!). After you review his code, I’ll share some thoughts on my version of the same from 3 years ago.

Here is Eshan’s example:

/**
 * @NApiVersion 2.x
 * @NScriptType ScheduledScript
 */
define(['N/search', 'N/file'], function (search, file) {

    /**
     * Base64 encode / decode
     * http://www.webtoolkit.info/
     */
    var Base64 = {
        // private property
        _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

        // public method for encoding
        encode: function (input) {
            var output = "";
            var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
            var i = 0;

            input = Base64._utf8_encode(input);

            while (i < input.length) {
                chr1 = input.charCodeAt(i++);
                chr2 = input.charCodeAt(i++);
                chr3 = input.charCodeAt(i++);

                enc1 = chr1 >> 2;
                enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
                enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
                enc4 = chr3 & 63;

                if (isNaN(chr2)) {
                    enc3 = enc4 = 64;
                } else if (isNaN(chr3)) {
                    enc4 = 64;
                }

                output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
            }

            return output;
        },

        // public method for decoding
        decode: function (input) {
            var output = "";
            var chr1, chr2, chr3;
            var enc1, enc2, enc3, enc4;
            var i = 0;

            input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

            while (i < input.length) {
                enc1 = this._keyStr.indexOf(input.charAt(i++));
                enc2 = this._keyStr.indexOf(input.charAt(i++));
                enc3 = this._keyStr.indexOf(input.charAt(i++));
                enc4 = this._keyStr.indexOf(input.charAt(i++));

                chr1 = (enc1 << 2) | (enc2 >> 4);
                chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
                chr3 = ((enc3 & 3) << 6) | enc4;

                output = output + String.fromCharCode(chr1);

                if (enc3 != 64) {
                    output = output + String.fromCharCode(chr2);
                }
                if (enc4 != 64) {
                    output = output + String.fromCharCode(chr3);
                }
            }

            output = Base64._utf8_decode(output);
            return output;
        },

        // private method for UTF-8 encoding
        _utf8_encode: function (string) {
            string = string.replace(/\r\n/g, "\n");
            var utftext = "";

            for (var n = 0; n < string.length; n++) {
                var c = string.charCodeAt(n);

                if (c < 128) {
                    utftext += String.fromCharCode(c);
                } else if ((c > 127) && (c < 2048)) {
                    utftext += String.fromCharCode((c >> 6) | 192);
                    utftext += String.fromCharCode((c & 63) | 128);
                } else {
                    utftext += String.fromCharCode((c >> 12) | 224);
                    utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                    utftext += String.fromCharCode((c & 63) | 128);
                }
            }

            return utftext;
        },

        // private method for UTF-8 decoding
        _utf8_decode: function (utftext) {
            var string = "";
            var i = 0;
            var c = c1 = c2 = 0;

            while (i < utftext.length) {
                c = utftext.charCodeAt(i);

                if (c < 128) {
                    string += String.fromCharCode(c);
                    i++;
                } else if ((c > 191) && (c < 224)) {
                    c2 = utftext.charCodeAt(i + 1);
                    string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                    i += 2;
                } else {
                    c2 = utftext.charCodeAt(i + 1);
                    c3 = utftext.charCodeAt(i + 2);
                    string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                    i += 3;
                }
            }

            return string;
        }
    };

    /**
     * Definition of the Scheduled script trigger point.
     *
     * @param {Object} scriptContext
     * @param {string} scriptContext.type - The context in which the script is executed. It is one of the values from the scriptContext.InvocationType enum.
     * @Since 2015.2
     */
    function execute(scriptContext) {
        var mySearch = search.load({
            id: 'customsearch1783'
        });

        var mySearchResultSet = mySearch.run();
        log.debug('Results...', mySearchResultSet);



        var xmlString = ''; 
        xmlString =
            '<?xml version="1.0" encoding="UTF-8"?>\n' +
            '<?mso-application progid="Excel.Sheet"?>\n' +
            '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"\n' +
            ' xmlns:o="urn:schemas-microsoft-com:office:office"\n' +
            ' xmlns:x="urn:schemas-microsoft-com:office:excel"\n' +
            ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"\n' +
            ' xmlns:html="http://www.w3.org/TR/REC-html40">\n' +
            ' <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">\n' +
            '  <Author>SuiteScript</Author>\n' +
            '  <Created>' + new Date().toISOString() + '</Created>\n' +
            '  <Version>16.00</Version>\n' +
            ' </DocumentProperties>\n' +
            ' <Styles>\n' +
            '  <Style ss:ID="Default" ss:Name="Normal">\n' +
            '   <Alignment ss:Vertical="Bottom"/>\n' +
            '   <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>\n' +
            '  </Style>\n' +
            ' </Styles>\n' +
            ' <Worksheet ss:Name="Sheet1">\n' +
            '  <Table>\n' +
            '   <Row>\n' +
            '    <Cell><Data ss:Type="String">Internal id</Data></Cell>\n' +
            '    <Cell><Data ss:Type="String">Name</Data></Cell>\n' +
            '   </Row>\n';
            
            mySearchResultSet.each(function (resultObject) {
              var internalid = resultObject.getValue({
                  name: 'internalid'
              });
              log.debug('internal id', internalid);
              var name = resultObject.getText({
                  name: 'entity'
              });
              log.debug('name', name);

              xmlString += '   <Row>\n' +
              '    <Cell><Data ss:Type="String">' + internalid + '</Data></Cell>\n' +
              '    <Cell><Data ss:Type="String">' + name + '</Data></Cell>\n' +
              '   </Row>\n';

              return true;
            });

          
            xmlString += '  </Table>\n' +
            ' </Worksheet>\n' +
            '</Workbook>';

        // Encode XML string to Base64
        var base64String = Base64.encode(xmlString);

        var date = new Date();

        var fileObj = file.create({
            name: 'Saved Search Result - ' + date.toLocaleDateString() + '.xls',
            fileType: file.Type.EXCEL,
            contents: base64String,
            description: 'This is an XML-based Excel file.',
            folder: 877
        });

        var fileId = fileObj.save();
        log.debug('File ID...', fileId);
    }

    return {
        execute: execute
    };
});

In my version, I used the ‘N/xml’ and ‘N/encode’ modules to replace some of the custom code supplied in Ehsan’s version.

Here, I’m showing that we used the same technique for starting a worksheet. We both copied the header from an existing Excel worksheet. You see it above in his variable named xmlString. In my case, I also copied the row with column headers, so my code only replaced data rows. I did that to also capture the formatting of the column header rows.

In my version, all XML elements were created and escaped using standard SuiteScript modules.

In my version, the file encoding is also done using standard SuiteScript.

I hope this saved you doing the research. As of April 2025, NetSuite does not offer a SuiteScript module that encodes Excel files natively. And if you still need this functionality, here is some help getting started.