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:

  • Client SuiteScript - This feature allows you to apply client scripts to records and forms.

  • Server SuiteScript - This feature allows you to attach server scripts to records.

  • File Cabinet - This feature allows you to store your script files in the NetSuite File Cabinet.

  • Serialized Inventory or Lot Tracking – These features allow purchase and sale of inventory using unique serial numbers or lot numbers, respectively.

For more information, see Enabling Features

Required Permissions

You will need a role with access to the following:

  • Scripts – Edit access

  • Script Deployments – Edit access

  • Transaction Body Fields – Create access

  • Transaction Line Fields – Create access

  • Saved Searches - Create access

  • Sales Orders – Edit and Delete access

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:

  1. 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

  2. Click Save.

To create the Substitute Item ID field:

  1. 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

  2. Click Save.

To create the Substitute Available Qty field:

  1. 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

  2. Click Save.

To create the Substitute Processed field:

  1. 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

  2. Click Save.

To create the Inventory Numbers search:

  1. Go to Reports > Saved Searches > All Saved Searches > New.

  2. Click Inventory Numbers.

  3. 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.

  4. Click Save & Run to confirm that your search will run and return the proper results.

To create the Item ID from Name search:

  1. Go to Reports > Saved Searches > All Saved Searches > New.

  2. Click Item.

  3. 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.

  4. 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

SuiteScript 2.x User Event Script Type

Modules Used

  • N/runtime Module

  • N/currentRecord Module - This module is available to all scripts as a provided context object. You do not need to explicitly load this module as a dependency in your define or require statement, however, you may if you want. This tutorial does not explicitly load this module.

  • N/log Module - This module is available to all scripts as a global object. However, you should explicitly load this module to avoid conflicts with other objects that may be named ‘log’.

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.

Note:

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

Important:

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:

Note:

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:

Note:

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

SuiteScript 2.x Client Script Type

Modules Used

  • N/ui/dialog Module

  • N/url Module

  • N/currentRecord Module - This module is available to all scripts as a provided context object. You do not need to explicitly load this module as a dependency in your define or require statement, however, you may if you want. This tutorial explicitly loads this module.

  • N/log Module - This module is available to all scripts as a global object. However, you should explicitly load this module to avoid conflicts with other objects that may be named ‘log’.

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.

Note:

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

Important:

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:

Note:

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:

Note:

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

SuiteScript 2.x 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.

Note:

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

Important:

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:

Note:

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:

Note:

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

SuiteScript 2.x Client Script Type

Modules Used

  • N/record Module

  • N/search Module

  • N/runtime Module

  • N/ui/dialog Module

  • N/currentRecord Module - This module is available to all scripts as a provided context object. You do not need to explicitly load this module as a dependency in your define or require statement, however, you may if you want. This tutorial does not explicitly load this module.

  • N/log Module - This module is available to all scripts as a global object. However, you should explicitly load this module to avoid conflicts with other objects that may be named ‘log’.

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.

Note:

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

Important:

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:

Note:

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:

Note:

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:

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:

  1. Upload your script to the NetSuite File Cabinet.

  2. Go to Customization > Scripting > Scripts > New.

  3. Select your script from the Script File list and click Create Script Record. The Script page is displayed.

  4. 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.

  5. Optionally set any other fields on the script record as desired.

  6. 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:

  1. Upload your script to the NetSuite File Cabinet.

  2. Go to Customization > Scripting > Scripts > New.

  3. Select your script from the Script File list and click Create Script Record. The Script page is displayed.

  4. 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.

  5. Optionally set any other fields on the script record as desired.

  6. Click Save.

Create the script record for the Item Substitution Suitelet script

To create the script record for the Item Substitution Suitelet script:

  1. Upload your script to the NetSuite File Cabinet.

  2. Go to Customization > Scripting > Scripts > New.

  3. Select your script from the Script File list and click Create Script Record. The Script page is displayed.

  4. 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.

  5. Optionally set any other fields on the script record as desired.

  6. 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:

  1. Upload your script to the NetSuite File Cabinet.

  2. Go to Customization > Scripting > Scripts > New.

  3. Select your script from the Script File list and click Create Script Record. The Script page is displayed.

  4. 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.

  5. Optionally set any other fields on the script record as desired.

  6. 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:

  1. Complete the steps in Step 6: Create the Script Records for your user event script.

  2. Go to Customization > Scripting > Scripts.

  3. Find your user event script in the list of scripts and click Deployments. The Script Deployments page appears.

  4. Click New Deployment. The Script Deployment page appears.

  5. 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

  6. Click Save.

Deploy the Item Substitution Button Helper client script

To deploy the Item Substitution Button Helper client script:

  1. Complete the steps in Step 6: Create the Script Records for your client script.

  2. Go to Customization > Scripting > Scripts.

  3. Find your client script in the list of scripts and click Deployments. The Script Deployments page appears.

  4. Click New Deployment. The Script Deployment page appears.

  5. 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

  6. Click Save.

Deploy the Item Substitution Suitelet script

To deploy the Item Substitution Suitelet script:

  1. Complete the steps in Step 6: Create the Script Records for your Suitelet script.

  2. Go to Customization > Scripting > Scripts.

  3. Find your Suitelet script in the list of scripts and click Deployments. The Script Deployments page appears.

  4. Click New Deployment. The Script Deployment page appears.

  5. 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

  6. Click Save.

Deploy the Validate Item Substitution client script

To deploy the Validate Item Substitution client script:

  1. Complete the steps in Step 6: Create the Script Records for your client script.

  2. Go to Customization > Scripting > Scripts.

  3. Find your client script in the list of scripts and click Deployments. The Script Deployments page appears.

  4. Click New Deployment. The Script Deployment page appears.

  5. 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

  6. 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.

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:

  1. Go to Customization > Scripting > Scripts.

  2. Locate your script in the list of scripts and click Edit next to the script name.

  3. On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.

  4. 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.

  5. Click Save to save the script parameter. The Script page appears.

  6. Click Save to save the script record with the updated script parameter.

To set the script parameter:

  1. Go to Customization > Scripting > Script Deployments.

  2. Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.

  3. 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.

  4. 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:

  1. Go to Customization > Scripting > Scripts.

  2. Locate your script in the list of scripts and click Edit next to the script name.

  3. On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.

  4. 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.

  5. Click Save to save the script parameters. The Script page appears.

  6. Click Save to save the script record with the updated script parameters.

To set the script parameters:

  1. Go to Customization > Scripting > Script Deployments.

  2. Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.

  3. 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.

  4. 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:

  1. Go to Customization > Scripting > Script Deployments.

  2. Locate your script in the list of scripts and click Edit next to the script name.

  3. On the Script page, click the Parameters tab and click New Parameter. The Script Field page appears.

  4. 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.

  5. Click Save to save the script parameters. The Script page appears.

  6. Click Save to save the script record with the updated script parameters.

To set the script parameters:

  1. Go to Customization > Scripts > Script Deployments.

  2. Locate your script deployment in the list of script deployments and click Edit next to the script deployment name.

  3. 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.

  4. 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:

  1. Access an existing sales order in edit mode.

  2. Verify the Fulfill With Substitutes button is displayed on the Sales Order page.

To test the Item Substitution Button Helper client script:

  1. Access an existing sales order in edit mode.

  2. Click the Fulfill With Substitute button.

  3. Verify the Suitelet form is displayed.

To test the Item Substitution Suitelet script:

  1. 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

  2. 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)

  3. Click Save.

  4. Verify an Inventory Adjustment record, moving inventory from the Substitute Item to the originally ordered item, using the serial numbers entered into the Suitelet.

  5. Verify that the Inventory Detail of the original item’s subrecord is populated with the newly entered serial numbers.

  6. Verify the sales order was saved and you are re-directed to the Item Fulfillment record.

To test the Validate Item Substitution client script:

  1. Add a line item to a sales order.

  2. Verify the Substitute Available Qty column is automatically populated.

  3. Verify the Primary Substitute and Internal # fields are updated by sourcing (not by script).

  4. Verify that you are alerted if any of the fields pulled from the Item record are empty or null.

  5. 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:

  1. 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

  2. Create a sales order meeting the particular test condition.

  3. Edit that sales order to add substitute items per the test condition.

  4. Save the sales order.

  5. 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

General Notices