SuiteTalk 2021.2 Deprecating HMAC-SHA1

I recently got this in an email from NetSuite: “You are receiving this notification because you use the Token-based Authentication (TBA) feature in your account for integrations that use HMAC-SHA1 as a signature method. As of NetSuite 2021.2, the use of HMAC-SHA1 will be deprecated. Before your account is upgraded to 2021.2, you must change your integrations to use HMAC-SHA256 as the signature method.

It sounded ominous, but turned out not to be a huge effort. If you’re like me, this TBA stuff captured my attention just long enough to get it working. So getting back into it took a little refamiliarizing. Hopefully this will save you some time.

In my post SuiteTalk TBA Example in C#, you’ll find the following code snippet. Here are the changes required to move from HMAC-SHA1 to HMAC-SHA256. I commented out the old block to highlight what changed.

Problem: Leading Zeros in Server-Side JavaScript

I needed a method that would validate dates in multiple formats. Here are examples of dates that needed to pass validation: ‘6/9/2021’, ‘6/10/2021’, ‘9-Jun-2021′, ’10-Jun-2021′, ’06/09/2021′, ’06/10/2021’. This problem came to me on June 9th, 2021. So ’06/09/2021′ failed, but ’06/10/2021′ passed. Hmm… If you guessed it was not allowing backdates, you’d be wrong.

In my testing I learned that typeof 09 == ‘number’, but parseInt(09) = NaN. How can that be?

The NetSuite (server-side) JavaScript engine treats numbers with leading zeros as base-8 (octal). So be sure to strip leading zeros, even on integer numbers of type ‘number’. Since base-8 only included digits 0 through 7, parseInt(09) is not a number!

Replace NetSuite’s Native Rich Text Editor with TinyMCE

NetSuite’s native rich text editor has some problems. It often accepts or even creates HTML that NetSuite won’t print. Here’s how to replace it with TinyMCE, a rich text editor that works much better.

Start here, Marty Zigman’s wonderful article entitled, Replace NetSuiteā€™s Rich Text Editor with a Custom HTML Editor.

Now that you know all the steps, here’s my method. It’s much shorter and has fewer moving parts, but it breaks a few NetSuite rules regarding not editing the DOM.

It all begins with understanding what make’s NetSuite’s out-of-the-box rich text control work. Here is what the control looks like. In this case, I’m editing a custom fields called custbody_note_internal.

And here is the code that represents this control.

The rubber meets the road here:

<input id="custbody_note_internal" name="custbody_note_internal" type="hidden">

This is the control that is read back by the server to update the field in the underlying record. I found that if I swapped this type=”hidden” for a textarea control, the server accepted it just the same! And once you make the swap to a textarea, you can easily turn that textarea into a TinyMCE control.

Per Marty’s article above, you need to include the TinyMCE javascript library. He does this via client-side JavaScript and also adds code to wait for it to load. In my case (and I’m not showing this here), I load the reference along with other JavaScript libraries on the server-side. I download the link as part of the page. I’m still referencing the CDN delivered library. Because I’m delivering the link in the page and not dynamically via client-side JavaScript, I don’t need to wait for the library to load, which saves a step for me.

Next, anyplace client-side where I can run an after-load script tucked away inside a $(document).read() function, I place this code.

Let’s walk through this.

Line 1367: I’m getting a reference to multiple custom fields that all require a rich text edit swap to TinyMCE.
Line 1370: I’m getting the name and ID of the “Rubber meets the road hidden input field.”
Line 1371: I’m getting the initial value of the rich text editor I’m replacing.
Line 1372: I’m replacing the span tag on line 18 of the HTML with a textarea that has the name and ID of my custom field. It also has a class of “tinymce-richtext”.
Line 1379: I’m initializing TinyMCE on all my swapped textareas. I use the class to initialize them all at once.
Line 1384: Here is magic! This is an onchange event fired by TinyMCE. It copies the contents of the TinyMCE editor into the textarea that I created in line1372.

What’s amazing about this is that TinyMCE will convert pasted images into base-64 encoded html tags. These “Image” tags cut and paste just like regular text. so you can paste snippets of screens or any other image from your clipboard into the TinyMCE editor and not only will it save with the underlying record, it prints too!

This solution breaks NetSuite’s rule of not editing the DOM. However, it saves a number of steps outlined in Marty’s article. It comes with no guarantees. Use it at your own risk. And… be sure to test it with each new release of NetSuite. OK… That’s my final disclaimer. Good luck!

SuiteScript – Find Sublist Line by Value

This one eluded me. I needed to update Item Pricing records under a customer. So how to avoid iterating through every item in the sublist in order to find the line to update? Here’s the secret!

  • Load the customer using isDynamic = false (Standard Mode)
  • Lookup the line number of the line you need update using record.findSublistLineWithValue()
  • Then simply update the line just like you always do using the line number (zero based offset).

Here is a code example.

Here is the code in a format that you can cut and paste.

var customer = record.load({
    type: record.Type.CUSTOMER,
    id: kvp.customer_id,
    isDynamic: false
})

updated_kvp.pricing_items.forEach(
    function (pricing_item) {
        if (pricing_item.hasOwnProperty('unit_price')) {

            var lineNumber = customer.findSublistLineWithValue({
                sublistId: 'itempricing',
                fieldId: 'item',
                value: pricing_item.id
            });

            if (lineNumber >= 0) {
                customer.setSublistValue({
                    sublistId: 'itempricing',
                    fieldId: 'price',
                    line: lineNumber,
                    value: pricing_item.unit_price
                });
            }
        }
    }
)

customer.save();

SuiteScript – Accessing a Custom Record Type Sublist

This is documented in several NetSuite and non-NetSuite articles. I read them all and still found it almost impossible to decipher how to programmatically read a custom record type sublist.

Feel free to start with SuiteAnswers article 65795, or Google “Sublist recmach”, or whatever else you can think of… Or just read this! In my example I’m using two custom record types. The first is the parent of the second. The second is a sublist of the first.

Here is the parent custom record type. Nothing is important here except the name: customrecord_logic_software_agreements

Here is the child custom record type where it is very important to check the “Allow Child Record Editing.” Otherwise, nothing works! It is also vitally important to include a reference to the parent table. Here you see my reference to “Software Agreement.” Notice it is of type “List/Record” and the reference is to the custom record type “Software Agreements.”

Next, and also essential, is the definition of the foreign-key relationship between child and parent. In my case, custrecord_logic_sft_line_agreement. It is important to make sure to note that this field is the link to the parent record. You do that by checking “Record is Parent.”

Now, here is where the magic begins. The sublistId is the scriptId of the foreign-key field prefixed with “recmach”. Crazy, I know! But… It works!

I hope this saves you a lot of time. And more importantly, I hope this saves ME a lot lot of time the next time I want to programmatically reference a non-standard record type as a sublist.

Happy coding!

SuiteScript’s ‘N/Search’ filterExpression – Don’t forget it!

This came up yesterday. Here is my note regarding how it works.

If you create a search in SuiteScript 2.0, you can set a filter expression via the filters property.

var mySearch.create({
   type: [whatever type you like],
   columns: [ search.createColumn({ name: 'internalid'... }),
   filters: [
      ['internalid', 'anyof', [more filter stuff here]]
 }

This works great. It accepts the advanced filter, no problem! However, when I load an existing saved search, I cannot submit a filter expression in the same way. Here’s how that works.

var mySearch.load({
   id: [an id of an existing saved search]
};
mySearch.filterExpression = [
   ['internalid', 'anyof' [more filter stuff here]]
];

When you load an existing search and want an advanced filter expression, you must use the filterExpression property!

Transactions may not reflect item setting changes

Here’s a gotcha! Changing a setting on a group item after you’ve added it to a transaction may not get reflected in your transaction going forward.

A perfect example of this is the setting “Display components on Transactions” checkbox in “item Groups”. If you print your transactions using a standard print template, you’ll probably want this checkbox unchecked. This prints just the group item, no subitems.

If you print your transactions using an Advanced PDF/HTML template, you will probably want this checked, so you can add up the total of all subitems and print it as the total for the group.

This become problematic when you switch from a standard print template to an advanced template. This is huge, so don’t miss it!!!!!

Any transaction that had that a group item added to it prior to changing that checkbox, remembers the previous setting. So copying an old transaction where the checkbox was checked yields the same setting on the line item in the copy. HUGE!!!

I’ve wasted way too much time tracking down group items that don’t print correctly after swapping from a basic print template to an advance print template. Hopefully this saves you and me both wasted time tracking this down in the future.

Custom Fields – Propagating Default Values

NetSuite’s ability to link custom fields and propagate default values is amazing. So much so, I wanted to make sure everyone following me knows how it works.

My task was to create a custom field, a dropdown at the customer level. Then add the same field to estimates and sales orders. When new estimates or sales orders are created, set the dropdown to the value of the customer assigned to the new transaction. However, allow the field to be modified after the fact. Then, when an estimate is copied to a sales order, let the modified selection follow to the new transaction. And, if the customer is changed, revisit the selection on same transaction and set it to the default value of the new customer. Crazy! I know.

Instead of writing complicated event scripts, this is easily accomplished when defining the custom field. Simple as that. Here’s the trick.

I added a custom dropdown at the customer level and populated from a list. Next, I created another custom field at the transaction level and assigned it to estimates and sales orders. It also populates from the same list. Here is how I sourced the custom field at the transaction level. It looks back to the customer to get its default value. Then it can be modified at any point thereafter. The most important thing to understand is that checking the “Store Value” checkbox, makes this behave as I’d described above.

Unchecking that checkbox will make this a “Calculated field.” In that case, it will always display whatever value is currently set in the dropdown at the customer level. This is a very important distinction!

Saved Search which includes child customer’s transactions

One of the fantastic things NetSuite supports is parent/child relationships among customers. The problem is figuring out how to write a saved search that leverages that.

I’ve been missing until now a relationship on customer records called “Top Level Parent Fields…” The second relationship is is “Transaction Fields…” Combine those two and you get the missing link (pun intended) which allows you to show all transactions for both the parent company and all its kids.

This is the criteria you’d use to pull one parent-company and all child-company transactions into one result set. If you wanted to reference this from SuiteScript, say inside a Suitelet, you’d remove the “Top Level Parent: Company Name” and add a filter on “Top Level Parent: Internal ID” or {toplevelparent.internalid}. You could also move this filter from Criteria in the Available Filters tab .

Here, my result set includes a list of parent customers followed by the child customer that actually had the transaction. All fields on the transaction record are available. I’ve just pulled a few as an example.

As you continue to create transactions, you will probably want to filter on “Transation Fields: Date” or {transaction.date}, either in the criteria saved with your search, or in the code that references this search.

Here is a simple example of how to filter directly in the saved search.

Suitelet – Custom Autocomplete Field

Leveraging jQuery UI autocomplete turned out to be amazingly easy and added functionality that is not present in the standard NetSuite autocomplete control.

Here are the steps I followed along with some sample code.

  1. Add the jQuery UI library to your Suitelet
  2. Create a Restlet specific to the field you wish to have autocomplete on
  3. Add the autocomplete feature to an <input type=”text”> control in your form

I’m not going to cover each of these steps in details. I’ll say that adding the jQuery UI library is explained in SuiteAnswers. I did it by uploading the jQuery UI minimized library (a *.js file) to the NetSuite file cabinet and then adding it as a reference in an INLINEHTML control in my form.

When you build your Suitelet, you’ll do something like this…

            var suiteletControl =
                form.addField({
                    id: 'custpage_newcontrol',
                    type: serverWidget.FieldType.INLINEHTML,
                    label: 'Dynamic HTML'
                });

            suiteletControl.defaultValue = '[Your HTML goes here]';

Be sure to put your <input type=”text” id=”whatever” /> control here. Then add some JavaScript that looks like this…

                var whateverElement = $('#whatever');
                whateverElement.autocomplete({
                    source: '/app/site/hosting/restlet.nl?script=[restlet internalid]&deploy=1',
                    minLength: 3
                });

This JavaScript can live directly in a <script> tag in your HTML above. Be sure to run it after the full form has loaded. If you don’t know how to do that, you shouldn’t be attempting any of this. Sorry, I’m really not a jerk!

Finally, you’ll need a restlet. My restlet is looking up vendors that match the text in the “whatever” textbox. The minLength property of the autocomplete object shown above tells jQuery AutoComplete to omit calling the Restlet until there is at least 3 characters in the textbox.

/**
 * @NApiVersion 2.0
 * @NscriptType restlet
 */
define(['N/search', 'N/log'],
    function (search, log) {

        function GET(context) {
            var term = context.term;

            var mySearch = search.create({
                type: search.Type.VENDOR,
                columns: [
                    search.createColumn({ name: 'entityid', sort: 'ASC'}),
                    search.createColumn({ name: 'isinactive'})
                ],
                filters: [
                    ['entityid', 'startswith', term ]
                ]
            })

            var results = mySearch.run().getRange(0,25);
            var suggestions = new Array();
            results.forEach(
                function(result) {
                    var entityid = result.getValue('entityid');
                    var inactive = result.getValue('isinactive') ? ' (inactive)' : '';
                    var label = entityid + inactive;
                    suggestions.push({
                        label: label,
                        value: entityid
                    })
                }
            )

            return JSON.stringify(suggestions);
            
        }

        function POST(context) {

        }

        function PUT(context) {

        }

        function DELETE(context) {

        }

        return {
            get: GET,
            post: POST,
            put: PUT,
            delete: DELETE
        }
    }
)

And finally, in my example, I’m showing how to return a list of vendors, which includes inactive vendors. However, if they are inactive, I’ll append the string ‘ (inactive)’ to the vendor’s company name in the dropdown list. However, the value (as opposed to the label) property in the object returned from the Restlet includes just the vendor’s entityid. That is what gets placed in the textbox when the user selects a row in the autocomplete dropdown list. Super simple, once you get this simple example working in your copy of NetSuite. You can build on this to do things like showing items that are in-stock, etc.