4 Working with Sublists in SuiteScript 2.0
Created: May 22, 2017
Earlier in the series, we looked at how the N/record module allows us to work with records and body fields. That same module allows us to work with sublists on those records. We'll be demonstrating how the 1.0 sublist APIs have been converted in to 2.0 as well as some significant changes to the behaviour of sublists in 2.0.
Before we can examine the sublist APIs, we have to understand a very important SuiteScript concept: the Record Mode.
Record Modes: Standard and Dynamic
There are two different modes which you can use for working with Records in SuiteScript: Standard Mode and Dynamic Mode.
Standard Mode is the default mode. In this mode, the record's fields are not sourced, calculated, or validated until you save the record. This means that when working in Standard Mode, you will need to create the record, save it, then re-load it from the database in order to retrieve any formula or sourced field values. Because nothing is sourced or calculated until the record is saved, the order in which you set the fields on your record does not matter in Standard Mode; NetSuite will automatically set everything in the correct order when the record is submitted.
Dynamic Mode, however, is the real-time mode. Fields are calculated, sourced, and validated immediately as they are set. This mode largely mimics the behaviour you would see in the UI. You will get errors for invalid values right away, and calculated or sourced fields will be updated right away. Because of this real-time action, the order in which you set fields does matter in Dynamic Mode, so you are responsible for making sure your script sets fields in the same order that you would in the UI.
Now that we know what the Record Modes are, how does it actually look in development? We will go through several examples very shortly, but first we need to know that there are actually two separate APIs for working with sublists based on the Record Mode. These separate APIs also existed in SuiteScript 1.0, so let's look at how they have been translated over to 2.0.
API Equivalencies
Just like there were with record and body field APIs, there is essentially a 1:1 relationship between the 1.0 and 2.0 APIs for working with sublists.
There are some cosmetic changes to note are in the function names, where many of the 2.0 names have dropped the word "Item", and they have replaced "LineItem" with "Sublist". Other than that, the functionality and the parameters you can provide to each function are largely unchanged.
Yet again I have bombarded you with the theory; now it's time to get into the code.
Example: Add a Line
// 1.0
function onRequest(params) {
var order = nlapiCreateRecord("salesorder", {recordmode: "standard", entity: "75"});
addLine(order);
var orderId = nlapiSubmitRecord(order, true);
order = nlapiLoadRecord("salesorder", orderId, {recordmode: "dynamic"});
updateLine(order);
insertLine(order);
removeLine(order);
}
function addLine(rec) {
// TODO
}
function updateLine(rec) {
// TODO
}
function insertLine(rec) {
// TODO
}
function removeLine(rec) {
// TODO
}
// 2.0
/**
* @NApiVersion 2.0
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(["N/record"], function (r) {
function onRequest() {
var order = r.create({
type: r.Type.SALES_ORDER,
isDynamic: false,
defaultValues: {entity: 75}
});
addLine(order);
var orderId = order.save({enableSourcing: true});
order = r.load({
type: r.Type.SALES_ORDER,
id: orderId,
isDynamic: true
});
updateLine(order);
insertLine(order);
removeLine(order);
}
function addLine(rec) {
// TODO
}
function updateLine(rec) {
// TODO
}
function insertLine(rec) {
// TODO
}
function removeLine(rec) {
// TODO
}
return {onRequest: onRequest};
});
Here I've created an example Suitelet in both 1.0 and 2.0. I've set up the boilerplate of each script, and I've added the code to initialize a new Sales Order in each. I've created separate functions for the various line operations that we'll be performing; that way, we can focus just on the code for each operation, one at a time.
Again I won't be going through and testing this script in the article as this is just to show the API transition more than it is to show the outcomes of the code; those have not changed between the two versions.
The first thing we'll look at is how to add a new line item to our Sales Order. In both cases you can see I've instantiated the Sales Order in Standard mode, but for adding a new line, the API will actually be the same.
First let's add a line with Item 123 and a quantity of 5:
// 1.0
function addLine(rec) {
rec.selectNewLineItem("item");
rec.setCurrentLineItemValue("item", "item", "123");
rec.setCurrentLineItemValue("item", "quantity", 5);
rec.commitLineItem("item");
}
// 2.0
function addLine(rec) {
rec.selectNewLine({sublistId: "item"});
rec.setCurrentSublistValue({sublistId: "item", fieldId: "item", value: "123"});
rec.setCurrentSublistValue({sublistId: "item", fieldId: "quantity", value: 5});
rec.commitLine({sublistId: "item"});
}
This should look familiar to most, but we'll step through it anyway.
When we're adding a new line, we use the API that basically mimics what a user would do in the UI. We have to select the line we want to work with first. This selects the next available line in the sublist (i.e. the empty line at the end of the list).
Next we set the appropriate columns on the sublist - in this case, the Item and the Quantity.
Lastly, we commit our changes to the line. This mimics the user clicking "Add" on the line after making all of their changes.
As you can see, the 2.0 API is essentially a direct port of the 1.0 API; it is a very straight-forward transition.
Example: Update an Existing Line
Now that we've added a line to our Sales Order, we can save it and then re-load it to make sure we have the latest data. This time, we're loading the record in Dynamic mode.
After we've loaded the record, we want to update the line we just added; perhaps we want to increase the quantity by 2. This will require us to read the quantity value from the line, add 2 to the value, and then set it again on the same line.
// 1.0
function updateLine(rec) {
rec.selectLineItem("item", 1);
var currentQuantity = parseFloat(rec.getCurrentLineItemValue("item", "quantity"));
var newQuantity = currentQuantity + 2;
rec.setCurrentLineItemValue("item", "quantity", newQuantity);
rec.commitLineItem("item");
}
// 2.0
function updateLine(rec) {
rec.selectLine({sublistId: "item", line: 0});
var currentQuantity = parseFloat(rec.getCurrentSublistValue({
sublistId: "item",
fieldId: "quantity"
}));
var newQuantity = currentQuantity + 2;
rec.setCurrentSublistValue({
sublistId: "item",
fieldId: "quantity",
value: newQuantity
});
rec.commitLineItem({sublistId: "item"});
}
Note that we use parseFloat
when we read the value from the sublist. NetSuite
is notoriously inconsistent at returning typed values (or maybe it's very
consistent at only returning Strings, I'm honestly not sure which it is). This
has not changed in 2.0, so we want to maintain this habit of ensuring we're
receiving the correctly typed value.
Here you should notice a very important distinction between 1.0 and 2.0. In 1.0, the first line of a sublist had an index of 1. In 2.0, the first line of a sublist now has index 0, just like a typical JavaScript Array does. I feel like this is a very welcome correction to a baffling aspect of SuiteScript 1.0.
Other than that, as you can see the flow is still the same between 1.0 and 2.0; we select a specific line, modify it, then commit it. Since our record is in Dynamic mode, our changes to the field and the record are sourced, calculated, and validated immediately.
Example: Insert a Line
Just like the user can insert a line at any point in the sublist by clicking the Insert button, we can also insert lines using SuiteScript. The API is nearly identical to that of adding a new line.
// 1.0
function insertLine(rec) {
rec.insertLineItem("item", 1);
rec.setCurrentLineItemValue("item", "item", "77");
rec.setCurrentLineItemValue("item", "quantity", 1);
rec.setCurrentLineItemValue("item", "rate", 20);
rec.commitLineItem("item");
}
// 2.0
function insertLine(rec) {
rec.insertLine({sublistId: "item", line: 0});
rec.setCurrentSublistValue({sublistId: "item", fieldId: "item", value: "77"});
rec.setCurrentSublistValue({sublistId: "item", fieldId: "quantity", value: 1});
rec.setCurrentSublistValue({sublistId: "item", fieldId: "rate", value: 20});
rec.commitLine({sublistId: "item"});
}
We use the insert
function and specify the index where we want to insert the
line. In 1.0, by specifying an index of 1, we're saying that we want to insert
this new line at the beginning of the sublist. In order to insert the new line
at the beginning of the sublist in 2.0, we specify index 0 instead of index 1 as
we did in 1.0.
Other than that, the calls we make are identical to adding a line.
Example: Finding and Removing a Line
In our last example today, we're going to remove a line, but we're going to do so by using a function that I don't see many SuiteScript developers leveraging: the function to find a sublist line by a specific value. The ability to find a line based on a value exists in both 1.0 and 2.0.
Previously in our script, we inserted Item 77 into our sublist. Now we want to remove that item from the list, but pretend we don't actually know which index it's on.
Before we can remove a line, we need to know its index. We can use the sublist's find functionality to locate the correct index.
// 1.0
function removeLine(rec) {
var index = rec.findLineItemValue("item", "item", 77);
if (index === -1) {
// Not found
return;
}
rec.removeLineItem("item", index);
}
// 2.0
function removeLine(rec) {
var index = rec.findSublistLineWithValue({
sublistId: "item",
fieldId: "item",
value: 77
});
if (index === -1) {
// Not found
return;
}
rec.removeLine({sublistId: "item", line: index});
}
Here we use findLineItemValue
to determine the index of the line on the item
sublist that contains a value of 77
in its item column. If there are multiple
lines that match your criteria, findLineItemValue
returns the index of the
first occurrence it encounters. If no lines match your criteria, then it will
return -1
.
The find
function's name has changed slightly in 2.0, but other than that it
behaves exactly the same.
recalc Replaced by sublistChanged
Before we wrap up, there is one important change to SuiteScript 2.0 Client Scripts that impacts how we work with sublists.
In 1.0 Client Scripts, there is an event called recalc
. This event fires
whenever a change has been made to a sublist that impacts the total of a
transaction.
Let's say you are editing a Sales Order in the UI, and you adjust the Quantity
of a line. That change will obviously change the total of the order, so NetSuite
will fire the recalc
event to any Client Scripts. However, if instead of
changing the quantity, you changed something that did not impact the total, like
perhaps the Location or the Item Description, then NetSuite would not
fire recalc
. This made it problematic to detect and handle changes to sublists
that did not impact the total of a transaction.
In 2.0, there is no longer a recalc
event on Client Scripts. It has been
replaced by an event named sublistChanged
. Unlike recalc
, this event gets
fired whenever any line change has been committed, regardless of what that
change was. Whether you are inserting, removing, adding, or modifying a line,
and no matter which columns you modify, this new event will get fired as soon as
you commit that line change. This is much less confusing than recalc
, and it
allows us to build far more robust sublist interactions in our Client Scripts.