SuiteScript 2.0 – Queuing Background Tasks

I have tasks that use too many resources to run in the foreground. So I had previously pushed them to the background and expected them to queue. However, they didn’t. If a map/reduce script was already running and another was submitted, the second instance simply failed.

Here’s where I went wrong. When kicking off my background map/reduce script, I got too specific. If you submit your background job like this, you can force the job/task to queue. If you add the optional deploymentId you cannot.

var myTask = task.create({
     taskType: task.TaskType.MAP_REDUCE,
     scriptId: 'customscript_map_recalc',
     params: { custscript_customer: customer_id }
 });
 var task_id = myTask.submit();

To force a background job to queue, you need to create multiple deployments for the same script. Then when you submit the job, leave off the deploymentId and the system will select a deployment ID for you.

Of course you have the option of purchasing additional NetSuite processors, which will allow you to move beyond the 2 you get by default. But most of us are limited to 2. And jobs can be configured to run on one or multiple processors. In my case, I need operations to occur in a specific order, so I limit my jobs to one.

In either case, if you don’t setup your jobs to queue and one is already running, the next one fails.

The easiest way I found to create multiple deployments is to “Save As” your initial deployment you create when you save your script for the first time.

Skip Freemarker by Delivering Saved Search Results in a Suitelet

It took me a number of years working as a NetSuite developer to stumble onto this. It’s worth a look.

As a developer, you have the option of writing a saved search and creating your own custom advanced PDF/HTML template to print it. It’s a fantastic option. However, it also requires knowledge of the Freemarker templating language.

So here’s my alternative…

First, the saved search. Code it any way you like. Supply custom titles or not. There are no restrictions.

And here is the end-in-mind, a formatted table included in a Suitelet. Obviously, I’ve burred the data. Can’t share that. Sorry!

Here is now I read the results from the Saved Search.

And here is how I format the results. You’ll need an INLINE_HTML field on your form to hold the resulting markup. I’m counting on you knowing how to do that.

Here is the code in form you can cut and paste.

        function getIssues() {
            var issues = new Array();

            var mySearch = search.load({
                id: 'customsearch_sales_order_issues_2'
            });

            var myPages = mySearch.runPaged({ pageSize: 1000 });

            for (var i = 0; i < myPages.pageRanges.length; i++) {
                var myPage = myPages.fetch({ index: i });
                myPage.data.forEach(
                    function (result) {
                        var issue = {};
                        mySearch.columns.forEach(
                            function (col, index) {
                                issue['column_' + index] = { label: col.label, text: result.getText(col), value: result.getValue(col) }
                            }
                        )
                        issues.push(issue);
                    }
                );
            }
            return issues;
        }
        function formatIssues(issues) {
            var html = new Array();
            html.push('<table class="RPT">');
            html.push('<thead>');

            if (issues.length > 0) {
                var issue = issues[0];

                html.push('<tr>');
                for (var i = 0; i < 20; i++) {
                    if (issue.hasOwnProperty('column_' + i)) {
                        var sortType = isNaN(issue['column_' +i].text || issue['column_' +i].value) ? 'string' : 'float';
                        html.push('<th data-sort="'+sortType+'">' + issue['column_' + i].label + '</th>');
                    }
                }
                html.push('</tr>');
            }

            html.push('</thead>');
            html.push('<tbody>');

            issues.forEach(
                function (issue) {
                    html.push('<tr>');
                    for (var i = 0; i < 20; i++) {
                        if (issue.hasOwnProperty('column_' + i)) {
                            var vAlign = isNaN(issue['column_' +i].text || issue['column_' +i].value) ? 'left' : 'right';
                            html.push('<td align="' + vAlign + '">' + (issue['column_' + i].text || issue['column_' + i].value) + '</td>');
                            break;
                        }
                    }
                    html.push('</tr>');
                }
            )


            html.push('</tbody>');
            html.push('</table>');

            return html.join("\n");
        }

Whoops! Just noticed this… Here is a fix.

NetSuite Upgrade Breaks NetSuite-Sync

I’ve written about this before. However, if you follow me, here is one more reminder.

This morning, after our 2020.2 upgrade overnight, I began writing JavaScript code and hit my normal keystrokes [ctl] + [alt] + u to upload my JS file to the server. To my surprise, I found Netsuite-Sync broken. Since I’ve blogged about this in the past, the answer was right here in my own blog.

Here’s the fix: If you have a Release Preview account, when you are prompted to select a role, do NOT pick your production Netsuite Administrator role. Select another role with appropriate permissions to your file cabinet. That’s it. Simple as that!

https://followingnetsuite.com/2019/09/05/vscode-netsuite-sync-breaks-with-release-preview-2019-2/

SuiteScript – Mainline filter not working

It’s becoming a common theme for me to write about little frustrations. Hopefully this little frustration will save us both time, as I’m sure I’ll hit this again myself.

According the NetSuite’s Record’s Browser, this should work. And in fact it does… but only if you know ask nicely!

When you filter on “Mainline”, you must do it in a filter expression. A standard search.createFilter() blows up.

Every filter in the filter expression I show above works using search.createFilter(), except “Mainline.”

SuiteScript – Referencing Columns in a Saved Search

This IS documented in SuiteAnswers. However, it took more than a minute to find. So here is my simplified version of “How to load and run a saved search referencing multiple formula columns from SuiteScript 2.0 code.”

Here is my saved search. You’ll notice it has multiple formula columns.

The problem is this. What are my column names. It’s possible to execute result.getValue(‘formulacurrency’). However, it is not possible to excute result.getValue(‘formulacurrency1’) or result.getValue(‘formulacurrency_1’). Neither work.

If you press into debugger to see the column names, you run out of luck.

Moving back one level in the call-stack and looking at mySearch, you see that all columns have been defined , some without a distinct name, all include the distinct formula.

So the solution to how to reference all formula columns is this.

result.getValue(mySearch.columns[4])

I hope this gets you across the finish line. Happy coding!

Reporting Invoices by Vendor

How hard could getting a report of sales by customer and vendor be in NetSuite? Ummm… Harder than you think!

You might look throught the list of standard reports and find this one.

… and you’ll find it a dead end!

Here’s how we solved this.

  1. First Create a custom column in invoice line items to hold the vendor.
  2. Then, you’ll need a “Before Save” event script on Invoices.
    • Look through your line items and find those with no vendor
    • Look up the preferred vendor for those items.
    • Set the custom vendor column to the item’s preferred vendor.

Now you are ready to report.

Here is the secret sauce. And note, if you try to use a “Calculated column”, one where the “Save Value” checkbox is unchecked and the field is sourced from the item’s preferred vendor… well… it won’t work. It simply isn’t an option to select in your report. So there is no getting around the custom field and event script solution at this time.

SuiteScript – Filter Expression – Between Dates

This simple snippet of code gave me fits. Here is a working example of filtering by date range in a SuiteScript 2.0 filter expression.

The first thing you need to know is, don’t try to use the search operator “between” to filter a date range. It doesn’t work. Or I should say, it appears to work, but in reality, it ignores the filter altogether. Instead do this.

SuiteScript N/Search – Grouping Numeric Formula Columns

I wanted a Suitelet that showed rolled up totals based on the expected close date on quotes, by month. There were 2 gotchas in this process. First, how to pull out the month in a search column, and secondly, how to roll that up. Here’s how I did it.

To pull the month out of a date field in a search column, use a numeric formula shown below. Names of numeric formulas must begin with ‘formulanumeric’. This same example will work if you use ‘formulatext’, but won’t roll up.

search.createColumn({
    name: ‘formulanumeric1′,
formula: ‘EXTRACT(MONTH FROM {expectedclosedate})
})

Here is an example of how I used this. I’m pushing my results into an INLINEHTML field.

Grouping Formulas

SuiteScript – Reading Cookies Server-Side

Perhaps I’m missing something. However, I looked and could not find any support for reading cookies in a server-side script in SuiteScript. You can read request headers, but nothing on parsing cookies. Bummer!

My task at hand was saving state of check boxes in a Suitelet. Easy enough? Right? When you serve the page (server side), remember the settings in a cookie. Next GET request, set the check boxes using those cookies. But wait, NetSuite doesn’t appear to support cookies on the server side. In fact, even if you roll your own solution, you still can’t write to a cookie from server-side script.

Note: I’m a maintenance programmer on this example. My company hired a consultant to write this originally. So please let’s just stick to concepts and don’t throw any stones. And… It’s old enough, it’s SuiteScript 1.0. 

So the only apparent way to save checkbox state is to commandeer the submit event (client side) and write your cookies there. Here is a snippet of client script.

Client Side Snippet

When the page reaches the server, the cookies have been set and can be read like this.

Server Side Snippet

And finally, here is where I’m reading my checkbox values from the array of cookies created above. That array has a bunch of other stuff in there too (see all the blurred stuff in the request header shown below), so I need find my specific value from that list.

getCookie

Another quirk of server-side script is that many of the array functions you’re used to on the client side are not available on the server side. Yet another bummer!

Here is the request header passed from client to server. My cookies live at the bottom.

Request Header

SuiteScript – Disable buttons until the page loads?

Here is a very complex problem with a very simple solution. I wasted way too much time trying to understand how to protect my users from clicking a <button> tag in my inline HTML before the NetSuite page had fully loaded. Disabling the button and then enabling it after the page loads? That didn’t work. Managing the event queue? Uh… No, that’s a dead end.

Here is the super-secret handshake! Don’t use <button> tags. Use what NetSuite uses, <input type=”button”> instead. No other code is required.

Using the latter, your buttons are automatically disabled until the page fully loads. In my case, using <button> tags left the tags clickable, and when clicked, it redirected my page to a brand new transaction, instead of editing an existing transaction. Very (very) frustrating!