Populate Item Substitution
This use case shows you how to provide a user the option of selecting pre-defined substitute items for a sales order when there is insufficient quantity of a selected item.
The use case adds a custom button to the sales order that the user can click when a substitute item has been physically picked so that they can enter information, such as quantity and serial number of picked items, so that inventory records are updated correctly.
Customization Details
The customization for this use case includes:
-
A custom field (Primary Substitute) to store the preferred substitute item if the original item is unavailable
-
A custom field (Substitute Item ID) to store the ID of the primary substitute item
-
A custom field (Substitute Available Qty) to store the available quantity for the primary substitute item
-
A custom field (Substitute Processed) that provides an indication of when the substitute item processing is complete
-
A script parameter (Client Script Path) to store the path to the script that executes a Suitelet
-
A script parameter (Helper Client Script Path) to store the path to the helper script that populates fields on the Suitelet
-
A script parameter (Max Lines) to store the maximum number of lines on the Suitelet
-
A script parameter (Inventory Numbers Search) to store the ID of the Inventory Numbers saved search
-
A script parameter (Inventory Adjustment Account) to store the ID of an Inventory Adjustment account
-
A script parameter (Item ID from Name Saved Search) to store the ID of the From Name saved search
-
A script parameter (Substitute Available Qty Empty) to store an alert message that is displayed when a substitute item has 0 quantity
-
A saved search (Inventory Numbers) that returns serial numbers for inventory items
-
A saved search (Item ID from Name) that returns item identifiers associated with items when a name is specified
-
A user event script triggered on the beforeLoad entry point
-
A client script triggered on the validateLine entry point
-
A helper client script triggered on the executeSuitelet and saveRecord entry points
-
A Suitelet triggered on the OnRequest entry point when the user clicks the custom button
Steps in this tutorial to complete this customization:
Before You Begin
The following table lists features, permissions, and other requirements necessary for performing this tutorial and implementing the solution:
Required Features |
The following features must be enabled in your account:
For more information, see Enabling Features |
Required Permissions |
You will need a role with access to the following:
|
Other Requirements |
You must already have pre-defined substitute items. This tutorial does not cover assigning substitute items. |
Step 1: Create the Custom Fields and Saved Searches
This customization uses four custom fields. The Primary Substitute field stores the preferred substitute item if the original item is unavailable. The Substitute Item ID field stores the ID of the primary substitute item. The Substitute Available Qty field stores the available quantity for the primary substitute item. And the Substitute Processed field indicates whether the processing for the substitute item is complete. Each custom field is a custom transaction line field added to the sales order record.
This customization also uses two saved searches. The Inventory Numbers saved search returns serial numbers for inventory items, sorted numerically. The Item ID from Name saved search returns item identifiers associated with items when an item name is specified, sorted by name.
To create the Primary Substitute field:
-
Go to Customization > Lists, Records, Fields > Transaction Line Fields > New and enter the following values:
Field
Value
Label
Primary Substitute Item
ID
_item_substitute
Type
List/Record
List/Record
Item
Store Value
Checked
Applies To
Sale Item
-
Click Save.
To create the Substitute Item ID field:
-
Go to Customization > Lists, Records, Fields > Transaction Line Fields > New and enter the following values:
Field
Value
Label
Substitute Item ID
ID
_sub_item_id
Type
Integer Number
Store Value
Checked
Applies To
Sale Item
-
Click Save.
To create the Substitute Available Qty field:
-
Go to Customization > Lists, Records, Fields > Transaction Line Fields > New and enter the following values:
Field
Value
Label
Substitute Available Qty
ID
_sub_avail_qty
Type
Integer Number
Store Value
Checked
Applies To
Sale Item
-
Click Save.
To create the Substitute Processed field:
-
Go to Customization > Lists, Records, Fields > Transaction Line Fields > New and enter the following values:
Field
Value
Label
Substitute Processed
ID
_line_processed_substitute
Type
Check Box
Store Value
Checked
Applies To
Sale Item
-
Click Save.
To create the Inventory Numbers search:
-
Go to Reports > Saved Searches > All Saved Searches > New.
-
Click Inventory Numbers.
-
On the Inventory Numbers page, enter or select the following values:
Field
Value
Search Title
Inventory Numbers
ID
_inventory_numbers
Public
Checked
Results > Sort By
Number
Results > Output Type
Normal
Columns > Fields
Number
Remove all other fields that may have been preselected.
-
Click Save & Run to confirm that your search will run and return the proper results.
To create the Item ID from Name search:
-
Go to Reports > Saved Searches > All Saved Searches > New.
-
Click Item.
-
On the Saved Item Search page, enter or select the following values:
Field
Value
Search Title
Item ID from Name
ID
_itemid_from_name
Public
Checked
Criteria > Filter
Type
Saved Item Search Pop-up Window > Type
none of
Description, Discount, Markup, Non-Inventory Item, Other Charge, Payment, Service, Subtotal
Results > Sort By
Name
Results > Output Type
Normal
Columns > Fields
Name and Internal ID
Remove all other fields that may have been preselected.
-
Click Save & Run to confirm that your search will run and return the proper results.
For more information about creating custom fields and searches, see the following help topics:
Step 2: Write the Add Fulfill with Substitutes Button Script
This script adds the Fulfill With Substitutes button on the sales order record.
Script Summary
The following table summarizes this script:
Script: Add Fulfill with Substitutes Button |
|
---|---|
Script Type |
|
Modules Used |
|
Entry Points |
For more information about script types and entry points, see SuiteScript 2.x Script Types.
The Complete Script
This tutorial includes the complete script along with individual steps you can follow to build the script in logical sections. The complete script is provided below so that you can copy and paste it into your text editor and save the script file as a .js file (for example, ue_ItemSubstitution.js).
If you would rather create this script by adding code in logical sections, follow the steps in Build the Script.
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger
This sample uses SuiteScript 2.1. For more information, see SuiteScript 2.1.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*/
define(['N/runtime', 'N/log'], (runtime, log) => {
function beforeLoad(scriptContext) {
const stLogTitle = 'beforeLoad_addButton';
try {
if (scriptContext.type === scriptContext.UserEventType.EDIT) {
const clientScriptPath = runtime.getCurrentScript().getParameter({
name: 'custscript_client_script_path'
});
if (isEmpty(clientScriptPath)) {
log.error({
title: stLogTitle,
details: 'You must specify the location of the client script path in a script parameter (custscript_client_script_path).'
});
return;
}
const currentRecord = scriptContext.newRecord;
const form = scriptContext.form;
form.clientScriptModulePath = clientScriptPath;
form.addButton({
id: 'custpage_execute_suitelet_button',
label: 'Fulfill with Substitutes',
functionName: 'executeSuitelet'
});
}
return true;
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
return {
beforeLoad: beforeLoad
};
});
Build the Script
You can write the script using a step-by-step approach that includes the following:
The code snippets included below do not account for indentation. Refer to The Complete Script for suggested indentation.
Start with required opening lines
JSDoc comments and a define
function are required at the top of the script file. The JSDoc comments in this script indicate that it is a SuiteScript 2.1 user event script. The script uses two SuiteScript modules specified in the define
statement:
-
N/runtime
-provides access to runtime settings for company, script, sessions, system, user, and version -
N/log
– allows you to log execution details
Start a new script file using any text editor and place the following JSDoc comments and define
function at the top of the file:
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*/
define(['N/runtime', 'N/log'], (runtime, log) => {
});
Create the entry point function
This script is triggered on the beforeLoad
entry point when the user creates a new sales order. A try-catch block is used to log any errors that might occur during script execution. Most of the script code will be placed in the try block.
Add the following function definition and initial try-catch block statements at the top of the define
function:
function beforeLoad(scriptContext) {
const stLogTitle = 'beforeLoad_addButton';
try {
return true;
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
Check the scriptContext
This function will only add the custom button when the user either edits an existing sales order.
Add the following code as the first statement in the try block above the return true;
statement:
if (scriptContext.type === scriptContext.UserEventType.EDIT) {
}
Check the script parameter
This function relies on a script parameter to specify the location of the client script that executes the Suitelet.
Add the following code within the if
statement that checks the scriptContext
:
const clientScriptPath = runtime.getCurrentScript().getParameter({
name: 'custscript_client_script_path'
});
if (isEmpty(clientScriptPath)) {
log.error({
title: stLogTitle,
details: 'You must specify the location of the client script path in a script parameter (custscript_client_script_path).'
});
return;
}
Get the current sales order record and form
This function needs access to the sales order record and form.
Add the following code within the if
statement that checks the script context:
const currentRecord = scriptContext.newRecord;
const form = scriptContext.form;
form.clientScriptModulePath = clientScriptPath;
Add a button to the sales order form
This function adds a button to the sales order form that will execute the Suitelet when clicked.
Add the following code within the if
statement that checks the script context:
form.addButton({
id: 'custpage_execute_suitelet_button',
label: 'Fulfill with Substitutes',
functionName: 'executeSuitelet'
});
Create the isEmpty function
This user event script uses a support function to determine if the script parameter is empty/null/undefined.
Add the following code after the end of the beforeLoad
function:
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
Create the return statement
This script associates the beforeLoad
function with the beforeLoad
user event entry point.
Add the following code immediately above the closing });
in your script:
return {
beforeLoad: beforeLoad
};
Save your script file
You need to save your script file so you can load it to the NetSuite File Cabinet. Before you save your script file, you may want to adjust the indentation so that the script is readable. Refer to The Complete Script for suggested indentation.
When you are happy with how your script file reads, save it as a .js file (for example, ue_ItemSubstitution.js).
Step 3: Write the Item Substitution Button Helper Script
This script executes the Suitelet when the Fulfill With Substitutes button is clicked.
Script Summary
The following table summarizes this script:
Script: Item Substitution Button Helper |
|
---|---|
Script Type |
|
Modules Used |
|
Entry Points |
|
For more information about script types and entry points, see SuiteScript 2.x Script Types.
The Complete Script
This tutorial includes the complete script along with individual steps you can follow to build the script in logical sections. The complete script is provided below so that you can copy and paste it into your text editor and save the script file as a .js file (for example, cs_itemSubstitution_helper.js).
If you would rather create this script by adding code in logical sections, follow the steps in Build the Script.
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger
This sample uses SuiteScript 2.1. For more information, see SuiteScript 2.1.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*/
define(['N/url', 'N/currentRecord', 'N/ui/dialog', 'N/log'], (url, currentRecord, dialog, log) => {
function executeSuitelet() {
try {
const record = currentRecord.get();
const recordID = record.id;
console.log('record ID in executeSuitelet', recordID);
const suiteUrl = url.resolveScript({
scriptId: 'customscript_sl_item_substitution',
deploymentId: 'customdeploy_sl_item_substitution',
params: {
record_id: recordID
},
returnExternalUrl: false
});
window.open(suiteUrl, 'Item Substitution',"width=1300,height=700");
} catch(error) {
alert(error.toString());
}
return true;
}
function saveRecord(scriptContext) {
try {
const currentRecord = scriptContext.currentRecord;
const countLines = currentRecord.getLineCount({
sublistId:'so_lines_sublist'
});
for (let i = 0; i < countLines; i++) {
let substQty = currentRecord.getSublistValue({
sublistId: 'so_lines_sublist',
fieldId: 'item_substitute_qty',
line: i
});
let serialNumbers = currentRecord.getSublistValue({
sublistId: 'so_lines_sublist',
fieldId: 'item_substitute_serial_nums',
line: i
});
if (isEmpty(substQty) || isEmpty(serialNumbers)) {
dialog.alert({
title: 'Alert',
message: 'Substitution Quantity and Serial Numbers can not be empty'
});
return false;
} else {
let serialNumbersSplit = serialNumbers.split(',');
let serialNumbersQty = serialNumbersSplit.length;
if (serialNumbersQty !== substQty) {
dialog.alert({
title: 'Alert',
message: 'Substitution Quantity and Serial Numbers Quantity needs to match.'
});
return false;
}
}
}
const salesOrderURL = url.resolveRecord({
recordType: 'salesorder',
recordId: currentRecord.getValue({
fieldId: 'sales_order_id'
}),
isEditMode: false
});
window.ischanged = false;
window.open(salesOrderURL);
window.close();
} catch(error) {
alert(error.toString());
}
}
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
return {
executeSuitelet: executeSuitelet,
saveRecord: saveRecord
};
});
Build the Script
You can write the script using a step-by-step approach that includes the following:
The code snippets included below do not account for indentation. Refer to The Complete Script for suggested indentation.
Start with required opening lines
JSDoc comments and a define
function are required at the top of the script file. The JSDoc comments in this script indicate that it is a SuiteScript 2.1 client script. The script uses four SuiteScript modules specified in the define
statement:
-
N/url
-allows you to determine URL navigation paths within NetSuite or to format URL strings -
N/currentRecord
– provides access to the record instance that you are currently working on. -
N/ui/dialog
– allows you to create a modal dialog that is displayed until a button on the dialog is pressed -
N/log
– allows you to log execution details
Start a new script file using any text editor and place the following JSDoc comments and define
function at the top of the file:
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*/
define(['N/url', 'N/currentRecord', 'N/ui/dialog', 'N/log'], (url, currentRecord, dialog, log) => {
});
Create the entry point functions
This script is triggered on the saveRecord
entry point when the user saves a new sales order that uses the substitute item information about the Suitelet form and on the executeSuitelet
entry point when the user clicks the Open Suitelet button. A try-catch block is used to log any errors that might occur during script execution. Most of the script code will be placed in the try block.
Add the following function definitions and initial try-catch block statements at the top of the define
function:
function executeSuitelet() {
try {
} catch(error) {
alert(error.toString());
}
return true;
}
function saveRecord(scriptContext) {
try {
} catch(error) {
alert(error.toString());
}
}
Complete the executeSuitelet function
This helper client script defines an executeSuitelet
function. Within this function, you will add code for the following:
Get the current record and ID
This function uses the current sales order record ID.
Add the following code at the top of the try block of the executeSuitelet function:
const record = currentRecord.get();
const recordID = record.id;
console.log('record ID in executeSuitelet', recordID);
Resolve the script URL
This function needs to know the location of the Suitelet.
Add the following code within the try block of the executeSuitelet function:
const suiteUrl = url.resolveScript({
scriptId: 'customscript_sl_item_substitution',
deploymentId: 'customdeploy_sl_item_substitution',
params: {
record_id: recordID
},
returnExternalUrl: false
});
Display the Suitelet
This function displays the Suitelet in its own window, with a specific size and title.
Add the following code within the try block of the executeSuitelet function:
window.open(suiteUrl, 'Item Substitution',"width=1300,height=700");
Complete the saveRecord function
This helper client script defines a saveRecord
function to validate values entered by the user on the Suitelet. This function is called when the user clicks the Save button on the Suitelet. Within this function, you will add code for the following:
Get the sales order and number of sublist lines
This function processes sublist lines on the sales order.
Add the following code at the top of the try block of the saveRecord function:
const currentRecord = scriptContext.currentRecord;
const countLines = currentRecord.getLineCount({
sublistId:'so_lines_sublist'
});
Verify the sublist items
This function iterates through all sublist items to verify the item quantity and item serial numbers, using a for
loop.
Add the following code within the try block of the saveRecord function:
for (let i = 0; i < countLines; i++) {
}
Get the quantity of the substitute item and serial numbers
This function gets the quantity of the substitute item ordered and the serial numbers for each item.
Add the following code within the for
loop:
let substQty = currentRecord.getSublistValue({
sublistId: 'so_lines_sublist',
fieldId: 'item_substitute_qty',
line: i
});
let serialNumbers = currentRecord.getSublistValue({
sublistId: 'so_lines_sublist',
fieldId: 'item_substitute_serial_nums',
line: i
});
Verify the quantity and serial numbers
This function requires the Substitute Item Quantity and Serial Number fields. It also requires that there is a serial number for each item.
Add the following code within the for
loop:
if (isEmpty(substQty) || isEmpty(serialNumbers)) {
dialog.alert({
title: 'Alert',
message: 'Substitution Quantity and Serial Numbers can not be empty'
});
return false;
} else {
let serialNumbersSplit = serialNumbers.split(',');
let serialNumbersQty = serialNumbersSplit.length;
if (serialNumbersQty !== substQty) {
dialog.alert({
title: 'Alert',
message: 'Substitution Quantity and Serial Numbers Quantity needs to match.'
});
return false;
}
}
Get the URL to the sales order
This function will return to the sales order when the user saves the Suitelet and all values are verified. To do this, the URL of the sales order is required.
Add the following code after the end of the for
loop. Ensure that youi add this code within the try block of the saveRecord function.
const salesOrderURL = url.resolveRecord({
recordType: 'salesorder',
recordId: currentRecord.getValue({
fieldId: 'sales_order_id'
}),
isEditMode: false
});
Return to the sales order and close the Suitelet
This function will display the sales order when the Suitelet is closed, if values entered in the Suitelet are valid.
Add the following code within the try block of the saveRecord function:
window.ischanged = false;
window.open(salesOrderURL);
window.close();
Create the isEmpty function
This helper client script uses a support function to determine if a value is empty/null/undefined.
Add the following code after the end of the saveRecord
function:
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
Create the return statement
This script associates the validateLine
function with the validateLine
client script entry point.
Add the following code immediately above the closing });
in your script:
return {
executeSuitelet: executeSuitelet,
saveRecord: saveRecord
};
Save your script file
You need to save your script file so you can load it to the NetSuite File Cabinet. Before you save your script file, you may want to adjust the indentation so that the script is readable. Refer to The Complete Script for suggested indentation.
When you are happy with how your script file reads, save it as a .js file (for example, cs_itemSubstitution_helper.js).
Step 4: Write the Item Substitution Script
This script reads the names, quantities, and primary substitutes from the items sublists and creates a Substitute Items sublist. The sales order line items are only populated in the Substitute Items sublist if the ordered quantity is greater than the available quantity of the original item. Two custom columns (Substitution Quantity and Serial Numbers) are also added to the sales order. When the order is saved, the Substitution Quantity and Serial Numbers values are used to create an inventory adjustment record, which removes the ordered quantity from the substituted item and adds the same quantity to the original line item. The sales order line items’ inventory detail subrecords are updated with the entered serial numbers.
Error handling is provided to handle missing serial numbers for a serialized line item.
Script Summary
The following table summarizes this script:
Script: Item Substitution Suitelet |
|
---|---|
Script Type |
|
Modules Used |
|
Entry Points |
For more information about script types and entry points, see SuiteScript 2.x Script Types.
The Complete Script
This tutorial includes the complete script along with individual steps you can follow to build the script in logical sections. The complete script is provided below so that you can copy and paste it into your text editor and save the script file as a .js file ( (for example, sl_itemSubstitution.js).
If you would rather create this script by adding code in logical sections, follow the steps in Build the Script.
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger
This sample uses SuiteScript 2.1. For more information, see SuiteScript 2.1.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(['N/ui/serverWidget', 'N/runtime', 'N/record', 'N/search', 'N/http', 'N/log'], (widget, runtime, record, search, http, log) => {
function onRequest(scriptContext) {
const stLogTitle = 'onRequest';
try {
if (scriptContext.request.method === http.Method.GET) {
const maxLinesParam = runtime.getCurrentScript().getParameter({
name: 'custscript_max_lines'
});
const recordID = scriptContext.request.parameters.record_id;
let itemSubForm = widget.createForm({
title: 'Item Substitution'
});
const headerTab = itemSubForm.addTab({
id: 'headertab',
label: 'Tab'
});
addSublistAndFields(itemSubForm, recordID, maxLinesParam);
itemSubForm.addSubmitButton({
label: 'Save'
});
itemSubForm.clientScriptModulePath = runtime.getCurrentScript().getParameter({
name: 'custscript_sl_client_script_path'
});
scriptContext.response.writePage(itemSubForm);
} else if (scriptContext.request.method === http.Method.POST) {
const recordID = scriptContext.request.parameters.sales_order_id;
const subsidiary = scriptContext.request.parameters.sales_order_subsidiary;
const location = scriptContext.request.parameters.sales_order_location;
const inventoryNumbersSearch = runtime.getCurrentScript().getParameter({
name: 'custscript_inv_nums_search'
});
const account = runtime.getCurrentScript().getParameter({
name: 'custscript_inv_adj_account'
});
const sublistLength = scriptContext.request.getLineCount({
group: 'so_lines_sublist'
});
let arrItemsInfo = [];
let arrSOLines = [];
const objSavedSearch = search.load({
id: inventoryNumbersSearch
});
for (let i = 0; i < sublistLength; i++) {
let originalItem = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_id',
line: i
});
let substituteItem = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_id',
line: i
});
let substituteItemQty = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_qty',
line: i
});
let serialNumberField = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_serial_nums',
line: i
});
let salesOrderLine = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'sales_order_line',
line: i
});
arrSOLines.push(salesOrderLine);
let arrSerialNumbersIDs = [];
if (!isEmpty(serialNumberField)) {
let serialNumbersSplit = serialNumberField.split(',');
arrSerialNumbersIDs = getSerialNumbersIDs(serialNumbersSplit, objSavedSearch, arrSerialNumbersIDs);
}
let objectItemInfo = {};
objectItemInfo.originalItem = originalItem;
objectItemInfo.substituteItem = substituteItem;
objectItemInfo.substituteItemQty = substituteItemQty;
objectItemInfo.arrSerialNumbersIDs = arrSerialNumbersIDs;
arrItemsInfo.push(objectItemInfo);
}
const remainingUnitsBeforeCreation = runtime.getCurrentScript().getRemainingUsage();
log.debug({
title: stLogTitle,
details: 'remainingUnitsBeforeCreation: ' + remainingUnitsBeforeCreation
});
createInventoryAdjustments(subsidiary, location, account, arrItemsInfo);
const salesOrderRec = record.load({
type: record.Type.SALES_ORDER,
id: recordID,
isDynamic: true
});
for (let i = 0; i < arrSOLines.length; i++) {
salesOrderRec.selectLine({
sublistId: 'item',
line: i
});
salesOrderRec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_line_processed_substitute',
value: true
});
salesOrderRec.commitLine({
sublistId: 'item'
});
}
const salesOrderUpdated = salesOrderRec.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
log.audit({
title: stLogTitle,
details: 'Sales Order Updated: ' + salesOrderUpdated
});
const remainingUnitsEND = runtime.getCurrentScript().getRemainingUsage();
log.debug({
title: stLogTitle,
details: 'remainingUnitsEND: ' + remainingUnitsEND
});
}
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function addSublistAndFields(form, recordID, maxLines) {
const stLogTitle = 'addSublistAndFields';
try {
const salesOrderID = form.addField({
id: 'sales_order_id',
type: ui.FieldType.TEXT,
label: 'Sales Order ID'
});
const salesOrderSubsidiary = form.addField({
id: 'sales_order_subsidiary',
type: ui.FieldType.TEXT,
label: 'Sales Order Subsidiary'
});
const salesOrderLocation = form.addField({
id: 'sales_order_location',
type: ui.FieldType.TEXT,
label: 'Sales Order Location'
});
salesOrderID.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
salesOrderSubsidiary.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
salesOrderLocation.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
const substituteItemsSublist = form.addSublist({
id: 'so_lines_sublist',
type: ui.SublistType.LIST,
tab: 'headertab',
label: 'Substitute Items'
});
const itemName = substituteItemsSublist.addField({
id: 'item_name',
type: ui.FieldType.TEXT,
label: 'Item'
});
const itemID = substituteItemsSublist.addField({
id: 'item_id',
type: ui.FieldType.TEXT,
label: 'Item ID'
});
const itemQty = substituteItemsSublist.addField({
id: 'item_qty',
type: ui.FieldType.FLOAT,
label: 'Item Qty'
});
const itemSubstitute = substituteItemsSublist.addField({
id: 'item_substitute',
type: ui.FieldType.TEXT,
label: 'Item Substitute'
});
const itemSubstituteID = substituteItemsSublist.addField({
id: 'item_substitute_id',
type: ui.FieldType.TEXT,
label: 'Item Substitute ID'
});
const itemSubstituteQty = substituteItemsSublist.addField({
id: 'item_substitute_qty',
type: ui.FieldType.FLOAT,
label: 'Substitute Quantity'
});
itemSubstituteQty.updateDisplayType({
displayType: ui.FieldDisplayType.ENTRY
});
const itemSubstituteSerialNumbers = substituteItemsSublist.addField({
id: 'item_substitute_serial_nums',
type: ui.FieldType.TEXT,
label: 'Substitute Serial Numbers'
});
itemSubstituteSerialNumbers.updateDisplayType({
displayType: ui.FieldDisplayType.ENTRY
});
const salesOrderLine = substituteItemsSublist.addField({
id: 'sales_order_line',
type: ui.FieldType.TEXT,
label: 'Sales Order Line'
});
populateSublistAndFields(salesOrderSubsidiary, salesOrderLocation, salesOrderID, substituteItemsSublist, recordID, maxLines);
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function populateSublistAndFields(subsidiaryField, locationField, salesOrderIDField, sublist, recordID, maxLines) {
const stLogTitle = 'populateSublistAndFields';
try {
const soRecord = record.load({
type: record.Type.SALES_ORDER,
id: recordID,
isDynamic: true
});
salesOrderIDField.defaultValue = recordID;
subsidiaryField.defaultValue = soRecord.getValue('subsidiary');
locationField.defaultValue = soRecord.getValue('location');
const countLines = soRecord.getLineCount({
sublistId: 'item'
});
let line = 0;
for (let i = 0; i < countLines; i++) {
if (line < maxLines) {
soRecord.selectLine({
sublistId: 'item',
line: i
});
let lineAlreadyProcessed = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_line_processed_substitute'
});
if (lineAlreadyProcessed === 'F' || lineAlreadyProcessed === false) {
let itemQty = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantityavailable'
});
let orderQty = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});
if (itemQty < orderQty) {
let item = soRecord.getCurrentSublistText({
sublistId: 'item',
fieldId: 'item'
});
sublist.setSublistValue({
id: 'item_name',
line: line,
value: item
});
let itemID = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'item'
});
sublist.setSublistValue({
id: 'item_id',
line: line,
value: itemID
});
sublist.setSublistValue({
id: 'item_qty',
line: line,
value: itemQty
});
let itemSubstitute = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_item_substitute'
});
if (!isEmpty(itemSubstitute)) {
sublist.setSublistValue({
id: 'item_substitute',
line: line,
value: itemSubstitute
});
let itemSubstituteID = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_item_id'
});
sublist.setSublistValue({
id: 'item_substitute_id',
line: line,
value: itemSubstituteID
});
}
sublist.setSublistValue({
id: 'sales_order_line',
line: line,
value: i
});
line++;
}
}
}
}
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function getSerialNumbersIDs(serialNumbersSplit, objSavedSearch, arrSerialNumbersIDs) {
const stLogTitle = 'getSerialNumbersIDs';
try {
let namesFilter = [];
let index = 0;
for (let i = 0; i < serialNumbersSplit.length; i++) {
namesFilter.push(['inventorynumber', 'IS', serialNumbersSplit[i]]);
if (index !== (serialNumbersSplit.length - 1)) {
namesFilter.push('OR');
}
index++;
}
const results = extendedSearch('inventorynumber', objSavedSearch, namesFilter);
for (let i = 0; i < results.length; i++) {
let obj = new Object();
obj.internalID = results[i].id;
obj.serialName = results[i].getValue('inventorynumber');
arrSerialNumbersIDs.push(obj);
}
return arrSerialNumbersIDs;
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function extendedSearch(stRecordType, objSavedSearch, arrSearchFilter, arrSearchColumn) {
if (stRecordType === null && objSavedSearch === null) {
error.create({
name: 'SSS_MISSING_REQD_ARGUMENT',
message: 'search: Missing a required argument. Either stRecordType or objSavedSearch should be provided.',
notifyOff: false
});
}
let arrReturnSearchResults = new Array();
const maxResults = 1000;
if (objSavedSearch !== null) {
objSavedSearch.filters = [];
if (arrSearchFilter !== null) {
if (arrSearchFilter[0] instanceof Array || (typeof arrSearchFilter[0] === 'string')) {
objSavedSearch.filterExpression = objSavedSearch.filterExpression.concat(arrSearchFilter);
} else {
objSavedSearch.filters = objSavedSearch.filters.concat(arrSearchFilter);
}
}
if (arrSearchColumn !== null) {
objSavedSearch.columns = objSavedSearch.columns.concat(arrSearchColumn);
}
} else {
let objSavedSearch = search.create({
type: stRecordType
});
if (arrSearchFilter !== null) {
if (arrSearchFilter[0] instanceof Array || (typeof arrSearchFilter[0] === 'string')) {
objSavedSearch.filterExpression = arrSearchFilter;
} else {
objSavedSearch.filters = arrSearchFilter;
}
}
if (arrSearchColumn !== null) {
objSavedSearch.columns = arrSearchColumn;
}
}
const objResultset = objSavedSearch.run();
let intSearchIndex = 0;
let arrResultSlice = null;
do {
arrResultSlice = objResultset.getRange(intSearchIndex, intSearchIndex + maxResults);
if (arrResultSlice === null) {
break;
}
arrReturnSearchResults = arrReturnSearchResults.concat(arrResultSlice);
intSearchIndex = arrReturnSearchResults.length;
} while (arrResultSlice.length >= maxResults);
return arrReturnSearchResults;
}
function createInventoryAdjustments(subsidiary, location, account, arrItemsInfo) {
const stLogTitle = 'createInventoryAdjustments';
try {
let inventoryAdjustmentDeletion = record.create({
type: record.Type.INVENTORY_ADJUSTMENT,
isDynamic: true
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'subsidiary',
value: subsidiary
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'adjlocation',
value: location
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'account',
value: account
});
for (let j = 0; j < arrItemsInfo.length; j++) {
let objectItemInfo = arrItemsInfo[j];
inventoryAdjustmentDeletion.selectNewLine({
sublistId: 'inventory'
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'item',
value: objectItemInfo.substituteItem
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'location',
value: location
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'adjustqtyby',
value: (objectItemInfo.substituteItemQty * (-1))
});
let itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: objectItemInfo.substituteItem,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
let itemType = itemTypeLookup.recordtype;
if (itemType === 'serializedinventoryitem') {
if (objectItemInfo.arrSerialNumbersIDs.length > 0) {
let inventoryDetailIA = inventoryAdjustmentDeletion.getCurrentSublistSubrecord({
sublistId: 'inventory',
fieldId: 'inventorydetail'
});
for (let index = 0; index < objectItemInfo.arrSerialNumbersIDs.length; index++) {
inventoryDetailIA.selectNewLine({
sublistId: 'inventoryassignment',
});
let line = inventoryDetailIA.getCurrentSublistIndex({
sublistId: 'inventoryassignment'
});
inventoryDetailIA.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'issueinventorynumber',
value: JSON.parse(objectItemInfo.arrSerialNumbersIDs[index].internalID)
});
inventoryDetailIA.commitLine({
sublistId: 'inventoryassignment'
});
}
}
}
}
inventoryAdjustmentDeletion.commitLine({
sublistId: 'inventory'
});
}
const invAdjIDDeletion = inventoryAdjustmentDeletion.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
log.audit({
title: 'Inventory Adjustment deletion: ',
details: invAdjIDDeletion
});
let inventoryAdjustmentAddition = record.create({
type: record.Type.INVENTORY_ADJUSTMENT,
isDynamic: true
});
inventoryAdjustmentAddition.setValue({
fieldId: 'subsidiary',
value: subsidiary
});
inventoryAdjustmentAddition.setValue({
fieldId: 'adjlocation',
value: location
});
inventoryAdjustmentAddition.setValue({
fieldId: 'account',
value: account
});
for (let k = 0; k < arrItemsInfo.length; k++) {
let objectItemInfoAdd = arrItemsInfo[k];
inventoryAdjustmentAddition.selectNewLine({
sublistId: 'inventory'
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'item',
value: objectItemInfoAdd.originalItem
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'location',
value: location
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'adjustqtyby',
value: objectItemInfoAdd.substituteItemQty
});
let itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: objectItemInfoAdd.originalItem,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
let itemType = itemTypeLookup.recordtype;
if (itemType === 'serializedinventoryitem') {
if (objectItemInfoAdd.arrSerialNumbersIDs.length > 0) {
let inventoryDetailIAAddition = inventoryAdjustmentAddition.getCurrentSublistSubrecord({
sublistId: 'inventory',
fieldId: 'inventorydetail'
});
for (let index1 = 0; index1 < objectItemInfoAdd.arrSerialNumbersIDs.length; index1++) {
inventoryDetailIAAddition.selectNewLine({
sublistId: 'inventoryassignment',
});
inventoryDetailIAAddition.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'receiptinventorynumber',
value: objectItemInfoAdd.arrSerialNumbersIDs[index1].serialName
});
inventoryDetailIAAddition.commitLine({
sublistId: 'inventoryassignment'
});
}
}
}
}
inventoryAdjustmentAddition.commitLine({
sublistId: 'inventory'
});
}
const invAdjIDAddition = inventoryAdjustmentAddition.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
log.audit({
title: 'Inventory Adjustment addition: ',
details: invAdjIDAddition
});
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
return {
onRequest: onRequest
};
});
Build the Script
You can write the user event script using a step-by-step approach that includes the following:
The code snippets included below do not account for indentation. Refer to The Complete Script for suggested indentation.
Start with required opening lines
JSDoc comments and a define
function are required at the top of the script file. The JSDoc comments in this script indicate that it is a SuiteScript 2.1 Suitelet script. The script six five SuiteScript modules specified in the define
statement:
-
N/ui/serverWidget
-allows you to work with the user interface within NetSuite -
N/runtime
– provides access to runtime settings for company, script, session, system, user, and version -
N/record
– allows you to work with NetSuite records -
N/search
– allows you to create and run on-demand or saved searches and analyze and iterate through the search results. -
N/http
– allows you to make http calls -
N/log
– allows you to log execution details
Start a new script file using any text editor and place the following JSDoc comments and define
function at the top of the file:
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(['N/ui/serverWidget', 'N/runtime', 'N/record', 'N/search', 'N/http', 'N/log'], (widget, runtime, record, search, http, log) => {
});
Create the entry point function
This script is triggered on the onRequest
entry point function when the Suitelet is requested through an HTTP request. A try-catch block is used to log any errors that might occur during script execution. Most of the script code will be placed in the try block.
Add the following function definition and initial try-catch block statements at the top of the define
function:
function onRequest(scriptContext) {
const stLogTitle = 'onRequest';
try {
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
}
Create the Item Substitution form on a GET request
The Suitelet form allows the user to select a substitute item when there is insufficient quantity of an item in their order. The Suitelet receives a GET request from the helper client script (see Step 3: Write the Item Substitution Button Helper Script) when the user clicks the Fulfill With Substitutes button on the sales order.
Add the following code at the top of the try block:
if (scriptContext.request.method === http.Method.GET) {
log.debug({
title: stLogTitle,
details: 'get'
});
}
Get script parameters
This Suitelet uses a script parameter to specify the maximum number of items on a sales order. It also uses the record ID included in the GET request.
Add the following code within the if
block that is processing the GET request:
const maxLinesParam = runtime.getCurrentScript().getParameter({
name: 'custscript_max_lines'
});
const recordID = scriptContext.request.parameters.record_id;
Create and display the Suitelet form
The form created in this Suitelet includes a header tab, a submit button, and several sublists and fields.
Add the following code within the if
block that is processing the GET request:
let itemSubForm = widget.createForm({
title: 'Item Substitution'
});
const headerTab = itemSubForm.addTab({
id: 'headertab',
label: 'Tab'
});
addSublistAndFields(itemSubForm, recordID, maxLinesParam);
itemSubForm.addSubmitButton({
label: 'Save'
});
itemSubForm.clientScriptModulePath = runtime.getCurrentScript().getParameter({
name: 'custscript_sl_client_script_path'
});
scriptContext.response.writePage(itemSubForm);
Create the inventory adjustment record on a POST request
This Suitelet creates inventory adjustment records when it receives a POST request, which is sent when the user clicks the Save button on the Suitelet. Note that data is validated on the form before the POST request is sent.
As part of the processing of the POST request, you will add code for the following:
To begin, add the following code as the else
clause of the if
statement added to process the GET request:
else if (scriptContext.request.method === http.Method.POST) {
}
Get the record ID and script parameters
This Suitelet uses the data passed as parameters in the POST request.
Add the following code at the top of the else if
block that is processing the POST request:
const recordID = scriptContext.request.parameters.sales_order_id;
const subsidiary = scriptContext.request.parameters.sales_order_subsidiary;
const location = scriptContext.request.parameters.sales_order_location;
const inventoryNumbersSearch = runtime.getCurrentScript().getParameter({
name: 'custscript_inv_nums_search'
});
const account = runtime.getCurrentScript().getParameter({
name: 'custscript_inv_adj_account'
});
Get the number of lines on the sublist
This Suitelet uses the number of lines in the sublist which is specified by a script parameter.
Add the following code within the else if
block that is processing the POST request:
const sublistLength = scriptContext.request.getLineCount({
group: 'so_lines_sublist'
});
Initialize variable arrays to store sales order data
This Suitelet stores data in two arrays: one for the sublist items and one for the sales order lines.
Add the following code within the else if
block that is processing the POST request:
let arrItemsInfo = [];
let arrSOLines = [];
Load the saved search
This Suitelet loads the inventory number search specified as a parameter in the POST request.
Add the following code within the else if
block that is processing the POST request:
const objSavedSearch = search.load({
id: inventoryNumbersSearch
});
Process sublist data
This Suitelet uses a for
loop to process all the sublist data included in the POST request. This processing includes the following:
To begin, add the following code within the else if
block that is processing the POST request:
for (let i = 0; i < sublistLength; i++) {
}
Get sublist data from the POST request
This Suitelet receives sublist data as a parameter in the POST request. This data includes the item ID, the substitute item ID, quantity, and serial numbers, and the sales order line.
Add the following code at the top of the for
loop:
let originalItem = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_id',
line: i
});
let substituteItem = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_id',
line: i
});
let substituteItemQty = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_qty',
line: i
});
let serialNumberField = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'item_substitute_serial_nums',
line: i
});
let salesOrderLine = scriptContext.request.getSublistValue({
group: 'so_lines_sublist',
name: 'sales_order_line',
line: i
});
arrSOLines.push(salesOrderLine);
Get serial numbers from the POST request
This Suitelet receives serial numbers as a parameter in the POST request. The serial numbers are included as one CSV string that needs to be split apart into individual serial numbers.
Add the following code within the for
loop:
let arrSerialNumbersIDs = [];
if (!isEmpty(serialNumberField)) {
let serialNumbersSplit = serialNumberField.split(',');
arrSerialNumbersIDs = getSerialNumbersIDs(serialNumbersSplit, objSavedSearch, arrSerialNumbersIDs);
}
Save the data from the POST request
This Suitelet saves all the data from the POST request into an array.
Add the following code within the for
loop:
let objectItemInfo = {};
objectItemInfo.originalItem = originalItem;
objectItemInfo.substituteItem = substituteItem;
objectItemInfo.substituteItemQty = substituteItemQty;
objectItemInfo.arrSerialNumbersIDs = arrSerialNumbersIDs;
arrItemsInfo.push(objectItemInfo);
Log remaining governance units
This Suitelet logs the amount of remaining governance units at this point in the Suitelet execution.
Add the following code after the end of the for
loop. Ensure that you add this code within the else if
block that is processing the POST request.
const remainingUnitsBeforeCreation = runtime.getCurrentScript().getRemainingUsage();
log.debug({
title: stLogTitle,
details: 'remainingUnitsBeforeCreation: ' + remainingUnitsBeforeCreation
});
Create inventory adjustments
This Suitelet creates inventory adjustment records when a substitute item is used to fulfill a sales order. One record is created to remove the substitute items from inventory, and one record is created to add the original items back into inventory.
Add the following code within the else if
block that is processing the POST request:
createInventoryAdjustments(subsidiary, location, account, arrItemsInfo);
Update and save the sales order
This Suitelet uses a for
loop to process each line on the sales order. After the order is processed, it is saved.
Add the following code within the else if
block that is processing the POST request:
const salesOrderRec = record.load({
type: record.Type.SALES_ORDER,
id: recordID,
isDynamic: true
});
for (let i = 0; i < arrSOLines.length; i++) {
salesOrderRec.selectLine({
sublistId: 'item',
line: i
});
salesOrderRec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_line_processed_substitute',
value: true
});
salesOrderRec.commitLine({
sublistId: 'item'
});
}
const salesOrderUpdated = salesOrderRec.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
Log remaining governance units
This Suitelet logs the amount of remaining governance units at this point in the Suitelet execution.
Add the following code within the else if
block that is processing the POST request:
log.audit({
title: stLogTitle,
details: 'Sales Order Updated: ' + salesOrderUpdated
});
const remainingUnitsEND = runtime.getCurrentScript().getRemainingUsage();
log.debug({
title: stLogTitle,
details: 'remainingUnitsEND: ' + remainingUnitsEND
});
Create the addSublistAndFields function
This Suitelet defines an addSublistAndFields
function to complete the Item Substitution form. Within this function, you will add code for the following:
To begin, add the following code after the end of the onRequest
function:
function addSublistAndFields(form, recordID, maxLines) {
}
Add a try-catch block
This function uses a global variable to set the title of a log message and adds the sublists and fields to the Item Substitution form using a try-catch block. Using a try-catch block provides simple and efficient error handling. In this case, if an error occurs, the script logs the error to the console.
Add the following code at the top of the addSublistAndFields
function:
const stLogTitle = 'addSublistAndFields';
try {
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
Add sales order fields to the Suitelet form
The Suitelet form includes hidden fields for the sales order ID, the sales order subsidiary, and the sales order location.
Add the following code at the top of the try block of the addSublistAndFields function:
const salesOrderID = form.addField({
id: 'sales_order_id',
type: ui.FieldType.TEXT,
label: 'Sales Order ID'
});
const salesOrderSubsidiary = form.addField({
id: 'sales_order_subsidiary',
type: ui.FieldType.TEXT,
label: 'Sales Order Subsidiary'
});
const salesOrderLocation = form.addField({
id: 'sales_order_location',
type: ui.FieldType.TEXT,
label: 'Sales Order Location'
});
salesOrderID.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
salesOrderSubsidiary.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
salesOrderLocation.updateDisplayType({
displayType: ui.FieldDisplayType.HIDDEN
});
Add substitute item sublist data and the sales order line to the Suitelet form
The Suitelet form includes a sublist for substitute items. This sublist includes fields for the Item, the item ID, the item quantity, the item substitute, the ID of the item substitute, the item substitute quantity, the serial numbers for the substitute item, and the sales order line. The user can enter data into the item substitute quantity and serial number fields.
Add the following code within the try block of the addSublistAndFields function:
const substituteItemsSublist = form.addSublist({
id: 'so_lines_sublist',
type: ui.SublistType.LIST,
tab: 'headertab',
label: 'Substitute Items'
});
const itemName = substituteItemsSublist.addField({
id: 'item_name',
type: ui.FieldType.TEXT,
label: 'Item'
});
const itemID = substituteItemsSublist.addField({
id: 'item_id',
type: ui.FieldType.TEXT,
label: 'Item ID'
});
const itemQty = substituteItemsSublist.addField({
id: 'item_qty',
type: ui.FieldType.FLOAT,
label: 'Item Qty'
});
const itemSubstitute = substituteItemsSublist.addField({
id: 'item_substitute',
type: ui.FieldType.TEXT,
label: 'Item Substitute'
});
const itemSubstituteID = substituteItemsSublist.addField({
id: 'item_substitute_id',
type: ui.FieldType.TEXT,
label: 'Item Substitute ID'
});
const itemSubstituteQty = substituteItemsSublist.addField({
id: 'item_substitute_qty',
type: ui.FieldType.FLOAT,
label: 'Substitute Quantity'
});
itemSubstituteQty.updateDisplayType({
displayType: ui.FieldDisplayType.ENTRY
});
const itemSubstituteSerialNumbers = substituteItemsSublist.addField({
id: 'item_substitute_serial_nums',
type: ui.FieldType.TEXT,
label: 'Substitute Serial Numbers'
});
itemSubstituteSerialNumbers.updateDisplayType({
displayType: ui.FieldDisplayType.ENTRY
});
const salesOrderLine = substituteItemsSublist.addField({
id: 'sales_order_line',
type: ui.FieldType.TEXT,
label: 'Sales Order Line'
});
Populate the sublist and fields on the Suitelet form
This Suitelet populates the sublist and fields on the Item Substitution form.
Add the following code within the try block:
populateSublistAndFields(salesOrderSubsidiary, salesOrderLocation, salesOrderID, substituteItemsSublist, recordID, maxLines);
Create the populateSublistAndFields function
This Suitelet defines a populateSublistAndFields
function to populate data on all sublists and fields on the Suitelet form. Within this function, you will add code for the following:
To begin, add the following code after the end of the addSublistAndFields
function:
function populateSublistAndFields(subsidiaryField, locationField, salesOrderIDField, sublist, recordID, maxLines) {
}
Add a try-catch block
This function uses a global variable to set the title of a log message and populates the Item Substitution form using a try-catch block. Using a try-catch block provides simple and efficient error handling. In this case, if an error occurs, the script logs the error to the console.
Add the following code at the top of the populateSublistAndFields function:
const stLogTitle = 'populateSublistAndFields';
try {
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
Load the sales order
This function loads the sales order to populate the fields and sublists, including substitute items.
Add the following code at the top of the try block of the populateSublistAndFields function:
const soRecord = record.load({
type: record.Type.SALES_ORDER,
id: recordID,
isDynamic: true
});
Set default values
This function sets default values for the sales order ID, the subsidiary, and the location.
Add the following code within the try block of the populateSublistAndFields function:
salesOrderIDField.defaultValue = recordID;
subsidiaryField.defaultValue = soRecord.getValue('subsidiary');
locationField.defaultValue = soRecord.getValue('location');
Set variables to iterate through the items on the sales order
This function uses variables to iterate through the items on the sales order.
Add the following code within the try block of the populateSublistAndFields function:
const countLines = soRecord.getLineCount({
sublistId: 'item'
});
let line = 0;
Populate fields and sublists
This function populates the following fields and sublists: Item ID, Item Quantity, Substitute Item, Substitute Item ID, and sales order line.
Add the following code within the try block of the populateSublistAndFields function:
for (let i = 0; i < countLines; i++) {
if (line < maxLines) {
soRecord.selectLine({
sublistId: 'item',
line: i
});
let lineAlreadyProcessed = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_line_processed_substitute'
});
if (lineAlreadyProcessed === 'F' || lineAlreadyProcessed === false) {
let itemQty = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantityavailable'
});
let orderQty = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});
if (itemQty < orderQty) {
let item = soRecord.getCurrentSublistText({
sublistId: 'item',
fieldId: 'item'
});
sublist.setSublistValue({
id: 'item_name',
line: line,
value: item
});
let itemID = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'item'
});
sublist.setSublistValue({
id: 'item_id',
line: line,
value: itemID
});
sublist.setSublistValue({
id: 'item_qty',
line: line,
value: itemQty
});
let itemSubstitute = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_item_substitute'
});
if (!isEmpty(itemSubstitute)) {
sublist.setSublistValue({
id: 'item_substitute',
line: line,
value: itemSubstitute
});
let itemSubstituteID = soRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_item_id'
});
sublist.setSublistValue({
id: 'item_substitute_id',
line: line,
value: itemSubstituteID
});
}
sublist.setSublistValue({
id: 'sales_order_line',
line: line,
value: i
});
line++;
}
}
}
}
Create the getSerialNumbersIDs function
This Suitelet defines a getSerialNumbersIDs
function to split the string of serial numbers included in the POST request into individual serial number values. It then creates a search filter using those numbers. Within this function, you will add code for the following:
To begin, add the following code after the end of the populateSublistAndFields
function:
function getSerialNumbersIDs(serialNumbersSplit, objSavedSearch, arrSerialNumbersIDs) {
}
Add a try-catch block
This function uses a global variable to set the title of a log message and retrieves the IDs of the serial numbers for the substitute items using a try-catch block. Using a try-catch block provides simple and efficient error handling. In this case, if an error occurs, the script logs the error to the console.
Add the following code at the top of the getSerialNumbersIDs
function:
const stLogTitle = 'getSerialNumbersIDs';
try {
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
Initialize search variables
This function uses an array variable and index to store the search filter.
Add the following code at the top of the try block of the getSerialNumbersIDs function:
let namesFilter = [];
let index = 0;
Create the search filter
In this function, a search filter is used to search for inventory numbers that match the IDs of serial numbers of the substitute items.
Add the following code the try block of the getSerialNumbersIDS function:
for (let i = 0; i < serialNumbersSplit.length; i++) {
namesFilter.push(['inventorynumber', 'IS', serialNumbersSplit[i]]);
if (index !== (serialNumbersSplit.length - 1)) {
namesFilter.push('OR');
}
index++;
}
Run the saved search
This function runs a search based on the inventory number and the search filters.
Add the following code after the for
block within the try block:
const results = extendedSearch('inventorynumber', objSavedSearch, namesFilter);
Save and return the IDs of the serial numbers
This function splits apart the search results into an ID and an inventory number. These results are then saved and returned.
Add the following code within the try block of the getSerialNumbersIDs function:
for (let i = 0; i < results.length; i++) {
let obj = new Object();
obj.internalID = results[i].id;
obj.serialName = results[i].getValue('inventorynumber');
arrSerialNumbersIDs.push(obj);
}
return arrSerialNumbersIDs;
Create the extendedSearch function
This Suitelet defines a extendedSearch
function to search for sales order and item data. Within this function, you will add code for the following:
To begin, add the following code after the end of the getSerialNumbersIDs
function:
function extendedSearch(stRecordType, objSavedSearch, arrSearchFilter, arrSearchColumn) {
}
Verify function parameters
This function expects up to four parameters to specify the record type, a saved search, a search filter and a search column. The record type or saved search is required.
Add the following code at the top of the extendedSearch
function:
if (stRecordType === null && objSavedSearch === null) {
error.create({
name: 'SSS_MISSING_REQD_ARGUMENT',
message: 'search: Missing a required argument. Either stRecordType or objSavedSearch should be provided.',
notifyOff: false
});
}
Initialize search variables
This function uses two variables to store the results of the search and to set the maximum number of results accepted.
Add the following code after the if
statement block:
let arrReturnSearchResults = new Array();
const maxResults = 1000;
Build the saved search
This function builds upon the passed in saved search, or creates a new one.
Add the following code:
if (objSavedSearch !== null) {
objSavedSearch.filters = [];
if (arrSearchFilter !== null) {
if (arrSearchFilter[0] instanceof Array || (typeof arrSearchFilter[0] === 'string')) {
objSavedSearch.filterExpression = objSavedSearch.filterExpression.concat(arrSearchFilter);
} else {
objSavedSearch.filters = objSavedSearch.filters.concat(arrSearchFilter);
}
}
if (arrSearchColumn !== null) {
objSavedSearch.columns = objSavedSearch.columns.concat(arrSearchColumn);
}
} else {
let objSavedSearch = search.create({
type: stRecordType
});
if (arrSearchFilter !== null) {
if (arrSearchFilter[0] instanceof Array || (typeof arrSearchFilter[0] === 'string')) {
objSavedSearch.filterExpression = arrSearchFilter;
} else {
objSavedSearch.filters = arrSearchFilter;
}
}
if (arrSearchColumn !== null) {
objSavedSearch.columns = arrSearchColumn;
}
}
Run the saved search
This function runs the search after it is built.
Add the following code:
const objResultset = objSavedSearch.run();
Process and return the search results
This function concatenates the results from the search before the are returned.
Add the following code:
let intSearchIndex = 0;
let arrResultSlice = null;
do {
arrResultSlice = objResultset.getRange(intSearchIndex, intSearchIndex + maxResults);
if (arrResultSlice === null) {
break;
}
arrReturnSearchResults = arrReturnSearchResults.concat(arrResultSlice);
intSearchIndex = arrReturnSearchResults.length;
} while (arrResultSlice.length >= maxResults);
return arrReturnSearchResults;
Create the createInventoryAdjustments function
This Suitelet defines a createInventoryAdjustments
function to create inventory adjustment records when substitute items are selected on a sales order when the sales order is saved. Within this function, you will add code for the following:
To begin, add the following code after the end of the extendedSearch
function:
function createInventoryAdjustments(subsidiary, location, account, arrItemsInfo) {
}
Add a try-catch block
This function uses a global variable to set the title of a log message and adds inventory adjustment records using a try-catch block. Using a try-catch block provides simple and efficient error handling. In this case, if an error occurs, the script logs the error to the console.
Add the following code at the top of the createInventoryAdjustments
function:
const stLogTitle = 'createInventoryAdjustments';
try {
} catch(error) {
log.error({
title: stLogTitle,
details: error
});
}
Create an inventory adjustment (deletion) record
This function creates an inventory adjustment record to reduce the quantity of the substitute item when one had been selected for the sales order. You will write code to do the following:
To begin, add the following code at the top of the try block of the createInventoryAdjustments function:
let inventoryAdjustmentDeletion = record.create({
type: record.Type.INVENTORY_ADJUSTMENT,
isDynamic: true
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'subsidiary',
value: subsidiary
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'adjlocation',
value: location
});
inventoryAdjustmentDeletion.setValue({
fieldId: 'account',
value: account
});
Set the sublist fields and save
This function iterates through all items on the sublist to set values associated with the inventory adjustment record for the substitute item.
Add the following code within the try block of the createInventoryAdjustments function:
for (let j = 0; j < arrItemsInfo.length; j++) {
let objectItemInfo = arrItemsInfo[j];
inventoryAdjustmentDeletion.selectNewLine({
sublistId: 'inventory'
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'item',
value: objectItemInfo.substituteItem
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'location',
value: location
});
inventoryAdjustmentDeletion.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'adjustqtyby',
value: (objectItemInfo.substituteItemQty * (-1))
});
let itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: objectItemInfo.substituteItem,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
let itemType = itemTypeLookup.recordtype;
if (itemType === 'serializedinventoryitem') {
if (objectItemInfo.arrSerialNumbersIDs.length > 0) {
let inventoryDetailIA = inventoryAdjustmentDeletion.getCurrentSublistSubrecord({
sublistId: 'inventory',
fieldId: 'inventorydetail'
});
for (let index = 0; index < objectItemInfo.arrSerialNumbersIDs.length; index++) {
inventoryDetailIA.selectNewLine({
sublistId: 'inventoryassignment',
});
let line = inventoryDetailIA.getCurrentSublistIndex({
sublistId: 'inventoryassignment'
});
inventoryDetailIA.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'issueinventorynumber',
value: JSON.parse(objectItemInfo.arrSerialNumbersIDs[index].internalID)
});
inventoryDetailIA.commitLine({
sublistId: 'inventoryassignment'
});
}
}
}
}
inventoryAdjustmentDeletion.commitLine({
sublistId: 'inventory'
});
}
Save the record and log a message
This function saves the inventory adjustment record and logs an audit message.
Add the following code within the try block of the createInventoryAdjustments function:
const invAdjIDDeletion = inventoryAdjustmentDeletion.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
log.audit({
title: 'Inventory Adjustment deletion: ',
details: invAdjIDDeletion
});
Create an inventory adjustment (addition) record
This function creates an inventory adjustment record to increase the quantity of the original item when a substitute item was instead selected for the sales order. You will write code to do the following:
To begin, add the following code within the try block of the createInventoryAdjustments function:
let inventoryAdjustmentAddition = record.create({
type: record.Type.INVENTORY_ADJUSTMENT,
isDynamic: true
});
inventoryAdjustmentAddition.setValue({
fieldId: 'subsidiary',
value: subsidiary
});
inventoryAdjustmentAddition.setValue({
fieldId: 'adjlocation',
value: location
});
inventoryAdjustmentAddition.setValue({
fieldId: 'account',
value: account
});
Set the sublist fields and save
This function iterates through all items on the sublist to set values associated with the inventory adjustment record for the original item.
Add the following code within the try block of the createInventoryAdjustments function:
for (let k = 0; k < arrItemsInfo.length; k++) {
let objectItemInfoAdd = arrItemsInfo[k];
inventoryAdjustmentAddition.selectNewLine({
sublistId: 'inventory'
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'item',
value: objectItemInfoAdd.originalItem
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'location',
value: location
});
inventoryAdjustmentAddition.setCurrentSublistValue({
sublistId: 'inventory',
fieldId: 'adjustqtyby',
value: objectItemInfoAdd.substituteItemQty
});
let itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: objectItemInfoAdd.originalItem,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
let itemType = itemTypeLookup.recordtype;
if (itemType === 'serializedinventoryitem') {
if (objectItemInfoAdd.arrSerialNumbersIDs.length > 0) {
let inventoryDetailIAAddition = inventoryAdjustmentAddition.getCurrentSublistSubrecord({
sublistId: 'inventory',
fieldId: 'inventorydetail'
});
for (let index1 = 0; index1 < objectItemInfoAdd.arrSerialNumbersIDs.length; index1++) {
inventoryDetailIAAddition.selectNewLine({
sublistId: 'inventoryassignment',
});
inventoryDetailIAAddition.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'receiptinventorynumber',
value: objectItemInfoAdd.arrSerialNumbersIDs[index1].serialName
});
inventoryDetailIAAddition.commitLine({
sublistId: 'inventoryassignment'
});
}
}
}
}
inventoryAdjustmentAddition.commitLine({
sublistId: 'inventory'
});
}
Save the record and log a message
This function saves the inventory adjustment record and logs an audit message.
Add the following code within the try block, following the for
block, f the createInventoryAdjustments function:
const invAdjIDAddition = inventoryAdjustmentAddition.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
log.audit({
title: 'Inventory Adjustment addition: ',
details: invAdjIDAddition
});
Create the isEmpty function
This Suitelet uses a support function to determine if a value is empty/null/undefined.
Add the following code after the end of the createInventoryAdjustments
function:
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
Create the return statement
This script associates the onRequest
function with the onRequest
client script entry point.
Add the following code immediately above the closing });
in your script:
return {
onRequest: onRequest
};
Save your script file
You need to save your script file so you can load it to the NetSuite File Cabinet. Before you save your script file, you may want to adjust the indentation so that the script is readable. Refer to The Complete Script for suggested indentation.
When you are happy with how your script file reads, save it as a .js file (for example, sl_itemSubstitution.js).
Step 5: Write the Validate Item Substitution Script
This script validates the fields on the Suitelet form.
Script Summary
The following table summarizes this script:
Script: Validate Item Substitution |
|
---|---|
Script Type |
|
Modules Used |
|
Entry Points |
For more information about script types and entry points, see SuiteScript 2.x Script Types.
The Complete Script
This tutorial includes the complete script along with individual steps you can follow to build the script in logical sections. The complete script is provided below so that you can copy and paste it into your text editor and save the script file as a .js file ( (for example, , cs_validate_itemSubstitution.js).
If you would rather create this script by adding code in logical sections, follow the steps in Build the Script.
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger
This sample uses SuiteScript 2.1. For more information, see SuiteScript 2.1.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
**/
define(['N/record', 'N/search', 'N/runtime', 'N/ui/dialog', N/log], (record, search, runtime, dialog, log) => {
let itemSearch;
let substAvailQtyEmptyMsg;
function validateLine(scriptContext) {
try {
if (isEmpty(itemSearch) && isEmpty(substAvailQtyEmptyMsg)) {
itemSearch = runtime.getCurrentScript().getParameter({
name: 'custscript_item_id_from_name_ss'
});
substAvailQtyEmptyMsg = runtime.getCurrentScript().getParameter({
name: 'custscript_substit_available_qty_empty'
});
}
const currentRecord = scriptContext.currentRecord;
const salesOrderLocation = currentRecord.getValue({
fieldId: 'location'
});
const quantityAvailable = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantityavailable'
});
const quantityOrdered = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});
if (quantityAvailable < quantityOrdered) {
const itemSubstituteName = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_item_substitute'
});
const itemID = getItemID(itemSearch, itemSubstituteName);
if (!isEmpty(itemID)) {
currentRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_item_id',
value: itemID
});
const itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: itemID,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
const itemType = itemTypeLookup.recordtype;
const itemRecord = record.load({
type: itemType,
id: itemID,
isDynamic: true
});
const countLines = itemRecord.getLineCount({
sublistId: 'locations'
});
for (let i = 0; i < countLines; i++) {
itemRecord.selectLine({
sublistId: 'locations',
line: i
});
let lineLocation = itemRecord.getCurrentSublistValue({
sublistId: 'locations',
fieldId: 'location'
});
if (lineLocation === salesOrderLocation) {
let availableQty = itemRecord.getCurrentSublistValue({
sublistId: 'locations',
fieldId: 'quantityavailable'
});
if (!isEmpty(availableQty)) {
currentRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_avail_qty',
value: availableQty
});
} else {
dialog.alert({
title: "Alert",
message: substAvailQtyEmptyMsg
});
}
}
}
}
}
}
} catch(error) {
console.log(stLogTitle, error);
}
return true;
}
function getItemID(itemsSavedSearch, itemSubstituteName) {
try {
let itemID;
const itemSearch = search.load({
id: itemsSavedSearch
});
const nameFilter = search.createFilter({
name: 'itemid',
operator: search.Operator.IS,
values: itemSubstituteName
});
itemSearch.filters.push(nameFilter);
const itemResult = itemSearch.run().getRange({
start:0,
end: 1
});
if (!isEmpty(itemResult) && itemResult.length > 0) {
itemID = itemResult[0].getValue('internalid');
}
return itemID;
} catch(error) {
console.log('getItemID', error);
}
}
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
return {
validateLine: validateLine
};
});
Build the Script
You can write the script using a step-by-step approach that includes the followingt:
The code snippets included below do not account for indentation. Refer to The Complete Script for suggested indentation.
Start with required opening lines
JSDoc comments and a define
function are required at the top of the script file. The JSDoc comments in this script indicate that it is a SuiteScript 2.1 client script. The script uses five SuiteScript modules specified in the define
statement:
-
N/record
-allows you to work with NetSuite records -
N/search
– allows you to create and run on-demand or saved searches and analyze and iterate through the search results -
N/runtime
– provides access to runtime settings for company, script, session, system, user, or version -
N/ui/dialog
– allows you to create a modal dialog that is displayed until a button on the dialog is pressed -
N/log
– allows you to log execution details
Start a new script file using any text editor and place the following JSDoc comments and define
function at the top of the file:
This tutorial script uses the define
function, which is required for an entry point script (a script you attach to a script record and deploy). You must use the require
function if you want to copy the script into the SuiteScript Debugger and test it. For more information, see SuiteScript Debugger.
/**
* @NApiVersion 2.x
* @NScriptType ClientScript
* @NModuleScope SameAccount
*/
define(['N/record', 'N/search', 'N/runtime', 'N/ui/dialog', 'N/log'], (record, search, runtime, dialog, log) => {
});
Create the entry point function
This script is triggered on the validateLine
entry point when the Suitelet is called when the user clicks the Open Suitelet button on a sales order form. A try-catch block is used to log any errors that might occur during script execution. Most of the script code will be placed in the try block.
Add the following function definition and initial try-catch block statements at the top of the define
function:
let itemSearch;
let substAvailQtyEmptyMsg;
function validateLine(scriptContext) {
try {
} catch(error) {
console.log(stLogTitle, error);
}
return true;
}
Complete the validateLine function
This client script defines a validateLine
function to validate data entered on the sales order. This validation includes determining if there is sufficient quantity of an item at the location of the sales order. Within this function, you will add code for the following:
Get the script parameters
This function uses script parameters to specify the search and a user message, if they are defined.
Add the following code at the top of the try block:
if (isEmpty(itemSearch) && isEmpty(substAvailQtyEmptyMsg)) {
itemSearch = runtime.getCurrentScript().getParameter({
name: 'custscript_item_id_from_name_ss'
});
substAvailQtyEmptyMsg = runtime.getCurrentScript().getParameter({
name: 'custscript_substit_available_qty_empty'
});
}
Get data from the sales order
This function gets the data entered by the user on the sales order. This data includes the location, the available quantity, and the quantity ordered for the individual item.
Add the following code after the code within the try block:
const currentRecord = scriptContext.currentRecord;
const salesOrderLocation = currentRecord.getValue({
fieldId: 'location'
});
const quantityAvailable = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantityavailable'
});
const quantityOrdered = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});
Process data from the sales order
This function retrieves the primary substitute item for any item in which there is insufficient quantity and then determines if there is enough quantity of that substitute item. This is a large chuck of code,, so Look for inline comments in the code below for a description of the processing.
Add the following code after the code within the try block:
if (quantityAvailable < quantityOrdered) {
const itemSubstituteName = currentRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_item_substitute'
});
const itemID = getItemID(itemSearch, itemSubstituteName);
if (!isEmpty(itemID)) {
currentRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_item_id',
value: itemID
});
cpnst itemTypeLookup = search.lookupFields({
type: search.Type.ITEM,
id: itemID,
columns: 'recordtype'
});
if (!isEmpty(itemTypeLookup)) {
const itemType = itemTypeLookup.recordtype;
const itemRecord = record.load({
type: itemType,
id: itemID,
isDynamic: true
});
const countLines = itemRecord.getLineCount({
sublistId: 'locations'
});
for (let i = 0; i < countLines; i++) {
itemRecord.selectLine({
sublistId: 'locations',
line: i
});
let lineLocation = itemRecord.getCurrentSublistValue({
sublistId: 'locations',
fieldId: 'location'
});
if (lineLocation === salesOrderLocation) {
let availableQty = itemRecord.getCurrentSublistValue({
sublistId: 'locations',
fieldId: 'quantityavailable'
});
if (!isEmpty(availableQty)) {
currentRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'custcol_sub_avail_qty',
value: availableQty
});
} else {
dialog.alert({
title: "Alert",
message: substAvailQtyEmptyMsg
});
}
}
}
}
}
}
Create the getItemID function
This client script defines a getItemID
function to get the ID of each substitute item on the sales order. Within this function, you will add code for the following:
To begin, add the following code after the end of the validateLine
function:
function getItemID(itemsSavedSearch, itemSubstituteName) {
}
Add a try-catch block
This function performs a search using a try-catch block. Using a try-catch block provides simple and efficient error handling. In this case, if an error occurs, the script logs the error to the console.
Add the following code at the top of the getItemID
function:
try {
} catch(error) {
console.log('getItemID', error);
}
Create a global variable
This function uses a global variable to store the substitute item ID.
Add the following code at the top of the try block:
let itemID;
Load, update, and run the saved search
This function runs a saved search to retrieve the item IDs.
Add the following code within the try block:
const itemSearch = search.load({
id: itemsSavedSearch
});
const nameFilter = search.createFilter({
name: 'itemid',
operator: search.Operator.IS,
values: itemSubstituteName
});
itemSearch.filters.push(nameFilter);
const itemResult = itemSearch.run().getRange({
start:0,
end: 1
});
Check and return the search results
This function returns the search results, which is a substitute item ID.
Add the following code within the try block:
if (!isEmpty(itemResult) && itemResult.length > 0) {
itemID = itemResult[0].getValue('internalid');
}
return itemID;
Create the isEmpty function
This client script uses a support function to determine if a value is empty/null/undefined.
Add the following code after the end of the getItemID
function:
function isEmpty(stValue) {
return stValue === '' || stValue === null || stValue === undefined;
}
Create the return statement
This script associates the validateLine
function with the validateLine
client script entry point.
Add the following code immediately above the closing });
in your script:
return {
validateLine: validateLine
};
Save your script file
You need to save your script file so you can load it to the NetSuite File Cabinet. Before you save your script file, you may want to adjust the indentation so that the script is readable. Refer to The Complete Script for suggested indentation.
When you are happy with how your script file reads, save it as a .js file (for example, cs_validate_itemSubstitution.js).
Step 6: Create the Script Records
After you create your scripts, you need to create script records for each one:
-
Create the script record for the Add Fulfill with Substitutes Button user event script
-
Create the script record for the Item Substitution Button Helper client script
-
Create the script record for the Item Substitution Suitelet script
-
Create the script record for the Item Substitution Suitelet script
For more information about creating script records, see Creating a Script Record.
Create the script record for the Add Fulfill with Substitutes Button user event script
To create the script record for the Add Fulfill with Substitutes Button user event script:
-
Upload your script to the NetSuite File Cabinet.
-
Go to Customization > Scripting > Scripts > New.
-
Select your script from the Script File list and click Create Script Record. The Script page is displayed.
-
On the Script page, enter the following values:
Field
Value
Name
Add Fulfill with Substitutes Button
ID
_ue_item_substitution
NetSuite prepends ‘customscript’ to this ID.
Description
This script adds a custom button to the sales order record to allow a user to fulfill the sales order with substitute items.
-
Optionally set any other fields on the script record as desired.
-
Click Save.
Create the script record for the Item Substitution Button Helper client script
To create the script record for the Item Substitution Button Helper client script:
-
Upload your script to the NetSuite File Cabinet.
-
Go to Customization > Scripting > Scripts > New.
-
Select your script from the Script File list and click Create Script Record. The Script page is displayed.
-
On the Script page, enter the following values:
Field
Value
Name
Item Substitution Button Helper
ID
_cs_item_substitution_helper
NetSuite prepends ‘customscript’ to this ID.
Description
This script calls the Suitelet when the user clicks the Fulfill With Substitutes custom button and when the user saves the data on the Suitelet form.
-
Optionally set any other fields on the script record as desired.
-
Click Save.
Create the script record for the Item Substitution Suitelet script
To create the script record for the Item Substitution Suitelet script:
-
Upload your script to the NetSuite File Cabinet.
-
Go to Customization > Scripting > Scripts > New.
-
Select your script from the Script File list and click Create Script Record. The Script page is displayed.
-
On the Script page, enter the following values:
Field
Value
Name
Item Substitution Suitelet
ID
_sl_item_substitution
NetSuite prepends ‘customscript’ to this ID.
Description
This script creates the form for users to select substitute items on a sales order.
-
Optionally set any other fields on the script record as desired.
-
Click Save.
Create the script record for the Validate Item Substitution client script
To create the script record for the Validate Item Substitution client script:
-
Upload your script to the NetSuite File Cabinet.
-
Go to Customization > Scripting > Scripts > New.
-
Select your script from the Script File list and click Create Script Record. The Script page is displayed.
-
On the Script page, enter the following values:
Field
Value
Name
Validate Item Substitution
ID
_cs_valid_item_substitution
NetSuite prepends ‘customscript’ to this ID.
Description
This script validates the data the user enters into the Fulfill With Substitutes Suitelet form.
-
Optionally set any other fields on the script record as desired.
-
Click Save.
Step 7: Deploy the Scripts
After you create the script record for each of the scripts, you can create script deployment records for them. A script deployment record determines how, when, and for whom the script runs.
For more information about script deployment records, see Script Deployment.
Deploy the Add Fulfill with Substitutes Button user event script
To deploy the Add Fulfill with Substitutes Button user event script:
-
Complete the steps in Step 6: Create the Script Records for your user event script.
-
Go to Customization > Scripting > Scripts.
-
Find your user event script in the list of scripts and click Deployments. The Script Deployments page appears.
-
Click New Deployment. The Script Deployment page appears.
-
On the Script Deployment page, enter the following values:
Field
Value
Applies To
Sales Order
ID
_ue_item_substitution_btn
NetSuite prepends 'custdeploy' to this ID.
Status
Testing
The Testing status allows the script owner to test the script without affecting other users in the account.
Log Level
Debug
The Debug level will write all
log.debug
statements in the script to the Execution Log tab of the script deployment record as well as all errors.Execute As Role
Current Role
It is normally best practice to have scripts execute with the user’s current role to avoid granting unwanted access.
Audience > Roles
Check Select All
-
Click Save.
Deploy the Item Substitution Button Helper client script
To deploy the Item Substitution Button Helper client script:
-
Complete the steps in Step 6: Create the Script Records for your client script.
-
Go to Customization > Scripting > Scripts.
-
Find your client script in the list of scripts and click Deployments. The Script Deployments page appears.
-
Click New Deployment. The Script Deployment page appears.
-
On the Script Deployment page, enter the following values:
Field
Value
Applies To
Sales Order
ID
_cs_item_substitution_helper
NetSuite prepends 'custdeploy' to this ID.
Status
Testing
The Testing status allows the script owner to test the script without affecting other users in the account.
Log Level
Debug
The Debug level will write all
log.debug
statements in the script to the Execution Log tab of the script deployment record as well as all errors.Audience > Roles
Check Select All
-
Click Save.
Deploy the Item Substitution Suitelet script
To deploy the Item Substitution Suitelet script:
-
Complete the steps in Step 6: Create the Script Records for your Suitelet script.
-
Go to Customization > Scripting > Scripts.
-
Find your Suitelet script in the list of scripts and click Deployments. The Script Deployments page appears.
-
Click New Deployment. The Script Deployment page appears.
-
On the Script Deployment page, enter the following values:
Field
Value
Title
Item Substitution Suitelet
ID
_sl_item_substitution
NetSuite prepends 'custdeploy' to this ID.
Status
Testing
The Testing status allows the script owner to test the script without affecting other users in the account.
Log Level
Debug
The Debug level will write all
log.debug
statements in the script to the Execution Log tab of the script deployment record as well as all errors.Execute As Role
Current Role
It is normally best practice to have scripts execute with the user’s current role to avoid granting unwanted access.
Audience > Roles
Check Select All
-
Click Save.
Deploy the Validate Item Substitution client script
To deploy the Validate Item Substitution client script:
-
Complete the steps in Step 6: Create the Script Records for your client script.
-
Go to Customization > Scripting > Scripts.
-
Find your client script in the list of scripts and click Deployments. The Script Deployments page appears.
-
Click New Deployment. The Script Deployment page appears.
-
On the Script Deployment page, enter the following values:
Field
Value
Applies To
Sales Order
ID
_cs_validate_item_subst
NetSuite prepends 'custdeploy' to this ID.
Status
Testing
The Testing status allows the script owner to test the script without affecting other users in the account.
Log Level
Debug
The Debug level will write all
log.debug
statements in the script to the Execution Log tab of the script deployment record as well as all errors.Audience > Roles
Check Select All
-
Click Save.
Step 8: Create and Set the Script Parameters
This solution uses several script parameters in the user event script, the Suitelet, and the client script.
-
Create and set the script parameter for the Add Fulfill with Substitutes Button user event script
-
Create and set the script parameters for the Item Substitution Suitelet script
-
Create and set the script parameters for the Validate Item Substitution client script
For more information about creating and setting script parameters, see Creating Script Parameters (Custom Fields).
Create and set the script parameter for the Add Fulfill with Substitutes Button user event script
The user event script uses a script parameter to specify the location of the client script that helps process the user click on the Fulfill with Substitutes button.
To create the script parameter:
-
Go to Customization > Scripting > Scripts.
-
Locate your script in the list of scripts and click Edit next to the script name.
-
On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.
-
On the Script Field page, enter the following values:
Label
ID
Type
Description
Preference
Client Script Path
_client_script_path
NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_<value>’ is used in the script.
Free-Form text
The path (location) to the client script.
Leave blank to set the parameter value on the script deployment.
-
Click Save to save the script parameter. The Script page appears.
-
Click Save to save the script record with the updated script parameter.
To set the script parameter:
-
Go to Customization > Scripting > Script Deployments.
-
Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.
-
On the Parameters tab, enter the client script location in the Client Script Path field. This is the location of the helper client script. For example, ‘SuiteScripts/cs_itemSubstution_helper.js’ if you uploaded the script to the SuiteScripts folder in the File Cabinet.
-
Click Save.
Create and set the script parameters for the Item Substitution Suitelet script
The Suitelet uses script parameters to specify the location of the client script that populates the field on the Suitelet, to specify the maximum number of lines on the Suitelet, and to specify the IDs for the custom search and inventory adjustment account.
To create the script parameter:
-
Go to Customization > Scripting > Scripts.
-
Locate your script in the list of scripts and click Edit next to the script name.
-
On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.
-
On the Script Field page, enter the following values. You can click Save & New to create subsequent parameters.
Script Parameter
Label
ID
Type
Description
Preference
Max Lines
Max Lines
_max_lines
NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. Integer Number
The maximum number of item lines on the form.
Leave blank to set the parameter value on the script deployment.
Client Script Path
Client Script Path for Populate Item
_
sl_client_script_path NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. Free-Form Text
The path to the client script.
Inventory Numbers Search
Inventory Numbers Search
_
inv_nums_search NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. Free-Form Text
The name of the inventory numbers search.
Inventory Adjustment Account
Inventory Adjustment Account
_
inv_adj_account NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. Free-Form Text
The account ID of the Inventory Adjustment account.
-
Click Save to save the script parameters. The Script page appears.
-
Click Save to save the script record with the updated script parameters.
To set the script parameters:
-
Go to Customization > Scripting > Script Deployments.
-
Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.
-
On the Parameters tab, enter the values for each script parameter as shown in the following table.
Script Parameter
Value
Max Lines
(Script ID: custscript_max_lines)
Any reasonable value, such as 10.
Validate Client Script Path
(Script ID: custscript_sl_client_script_path)
This is the location of the client script. For example, ‘SuiteScripts/cs_itemSubstitution.js’ if you uploaded the script to the SuiteScripts folder in the File Cabinet.
Inventory Numbers Search
(Script ID: custscript_inv_nums_search)
customsearch_inventory_numbers
Inventory Adjustment Account
(Script ID: custscript_inv_adj_account)
Set this value to your account ID for your Inventory Adjustment account.
-
Click Save.
Create and set the script parameters for the Validate Item Substitution client script
The client script uses script parameters to specify the saved search and to define an alert message.
To create the script parameters:
-
Go to Customization > Scripting > Script Deployments.
-
Locate your script in the list of scripts and click Edit next to the script name.
-
On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.
-
On the Script Field page, enter the following values. You can click Save & New to create subsequent parameters.
Parameter
Label field
ID field
Type field
Description
Preference field
Item ID from Name Saved Search
Item ID from Name
NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. _
item_id_from_name_ss Free-Form Text
The name of the id from name search.
Leave blank to set the parameter value on the script deployment.
Substitute Available Qty Empty
Substitute Available Qty Empty
NetSuite prepends ‘custscript’ to this ID. The value ‘custscript_
<value>’ is used in the script. _
sublist_available_qty_empty Free-Form Text
Alert message displayed when a substitute item has 0 quantity.
-
Click Save to save the script parameters. The Script page appears.
-
Click Save to save the script record with the updated script parameters.
To set the script parameters:
-
Go to Customization > Scripts > Script Deployments.
-
Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.
-
On the Parameters tab, enter the values for each script parameter as shown in the following table.
Script Parameter
Value
Item ID from Name Saved Search
(Script ID: custscript_item_id_from_name_ss)
customsearch_itemid_from_name
Substitute Available Qty Empty
(Script ID: custscript_substit_available_qty_empty)
Sorry, that item is not available.
-
Click Save.
Step 9: Test the Solution
After you create the script records and deploy your scripts, you can test your solution by testing each script and then testing the entire solution by creating a new sales order and selecting substitute items for the order.
To test the Add Fulfill with Substitutes Button user event script:
-
Access an existing sales order in edit mode.
-
Verify the Fulfill With Substitutes button is displayed on the Sales Order page.
To test the Item Substitution Button Helper client script:
-
Access an existing sales order in edit mode.
-
Click the Fulfill With Substitute button.
-
Verify the Suitelet form is displayed.
To test the Item Substitution Suitelet script:
-
Access a sales order, in edit mode, that has the item sublist pre-populated with:
-
Line items from the sales order
-
Ordered quantity of the line items
-
Each line item’s Primary Substitute item
-
-
This same sublist will allow the following information to be entered. Enter the information:
-
The number of Substitute Items to fulfill
-
The serial numbers of the Substitute item(s)
-
-
Click Save.
-
Verify an Inventory Adjustment record, moving inventory from the Substitute Item to the originally ordered item, using the serial numbers entered into the Suitelet.
-
Verify that the Inventory Detail of the original item’s subrecord is populated with the newly entered serial numbers.
-
Verify the sales order was saved and you are re-directed to the Item Fulfillment record.
To test the Validate Item Substitution client script:
-
Add a line item to a sales order.
-
Verify the Substitute Available Qty column is automatically populated.
-
Verify the Primary Substitute and Internal # fields are updated by sourcing (not by script).
-
Verify that you are alerted if any of the fields pulled from the Item record are empty or null.
-
Verify that the line can be committed and the fields can be left blank.
You should first test each script before testing the entire solution.
To test the entire solution:
-
The following test conditions can be used to test the entire solution:
-
Original item inventory is on hand and available to commit when being added to a sales order
-
Original item has 0 units available to commit and substitute items have enough units to commit when being added to a sales order
-
Original item has 0 units available to commit and substitute items do not have any units available to commit when being added to a sales order
-
Original item has 0 units available to commit and substitute items only has partial units available to commit when being added to a sales order
-
Original item has partial units available to commit and substitute items have enough units to commit
-
Original item has partial units available to commit and substitute items do not have any units available to commit
-
Original item has partial units available to commit and substitute items only has partial units available to commit
-
-
Create a sales order meeting the particular test condition.
-
Edit that sales order to add substitute items per the test condition.
-
Save the sales order.
-
Verify the saved sales order and any inventory adjustment records that may have been created.
Related Topics
- SuiteCloud Customization Tutorials
- Add Custom Button to Execute a Suitelet
- Calculate Commission on Sales Orders
- Copy a Value to the Item Column
- Disable Tax Fields
- Hide a Column in a Sublist
- Set a Default Posting Period in a Custom Field
- Set Purchase Order Exchange Rate
- Set the Item Amount to Zero for Marketing Orders
- Set Default Values in a Sublist
- Track Customer Deposit Balances
- Validate Order on Entry