Scheduled script to Automatically Route Sales orders to particular Warehouse based on ShipState.

The objective of this task is to develop a SuiteScript that evaluates Sales Orders created in NetSuite based on the SKU and Ship-To State.

This script will ensure that the appropriate warehouse is selected for order routing based on predefined criteria, and the correct region flags are applied for reporting. This script runs on a daily basis to route sales orders that were not properly handled in real time.

/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 *************************************************************************************************************************
 * Touchstone Merchandise Group-USA-NS
 *
 * TSMG-694 : Automated Order Routing for Power Pony in NetSuite
 *
 *********************************************************************************************************************
 * Author: Jobin & Jismi
 *
 * Date Created : 25-November-2025
 *
 * COPYRIGHT © 2024 Jobin & Jismi. All rights reserved.This script is a proprietary product of Jobin & Jismi IT Services LLP and is protected by copyright law and international treaties.
 * Unauthorized reproduction or distribution of this script, or any portion of it, may result in severe civil and criminal penalties and will be prosecuted to the maximum extent possible under law.
 *
 * Description : The objective of this task is to develop a SuiteScript that evaluates Sales Orders created in NetSuite based on the SKU and Ship-To State.
 * This script will ensure that the appropriate warehouse is selected for order routing based on predefined criteria, and the correct region flags are applied for reporting.
 * This script runs on a daily basis to route sales orders that were not properly handled in real time.
 *
 * REVISION HISTORY
 *
 * @version 1.0 TSMG-694: 25-November-2025 : Created the initial build by JJ0054
 *
 **************************************************************************************************************************/
define(["N/record", "N/search", "N/runtime"], function (
    record,
    search,
    runtime
) {
    const WEST_COST = 1;
    const EAST_COST = 2;
    const MAIN_WAREHOUSE = 7;
    const PP_WEST = 48;
    const CUS_ID = 197279;


    /**
     * Fetch valid SKUs from script parameters
     * @returns {Array}
     */
    function getValidSkusFromParams() {
        try {
            let validItemString = runtime
                .getCurrentScript()
                .getParameter({ name: "custscript_jj_valid_items_tsmg_696" }); //To get the valid items
            let validItemArray = [];
            if (validItemString) {
                validItemArray = validItemString.split(",");
            }
            let inList = "('" + validItemArray.join("','") + "')";
            let formula = `CASE WHEN {name} IN ${inList} THEN 1 ELSE 0 END`;
            let itemSearchObj = search.create({
                type: "item",
                filters: [["formulanumeric: " + formula, "equalto", "1"]],
                columns: [
                    search.createColumn({ name: "internalid", label: "Internal ID" }),
                ],
            });
            let itemIdArr = [];
            itemSearchObj.run().each(function (result) {
                itemIdArr.push(result.id);
                return true;
            });
            return itemIdArr;
        } catch (error) {
            log.error("Error @ getValidSkusFromParams", error);
        }
    }


    /**
     * Validate that all items in the Sales Order have valid SKUs
     * @param salesOrder
     * @param lineCount
     * @param validSkus
     * @returns {Boolean}
     */
    function validateItems(salesOrder, lineCount, validSkus) {
        try {
            let hasValidSku = false;


            for (let i = 0; i < lineCount; i++) {
                let sku = salesOrder.getSublistValue({
                    sublistId: "item",
                    fieldId: "item",
                    line: i,
                });
                let itemType = salesOrder.getSublistValue({
                    sublistId: "item",
                    fieldId: "itemtype",
                    line: i,
                });
                if (itemType === "NonInvtPart") {
                    // allowed, continue
                    continue;
                } else if (validSkus.includes(sku)) {
                    hasValidSku = true;
                } else {
                    // found an invalid item → fail immediately
                    return false;
                }
            }
            // must have at least one valid SKU
            return hasValidSku;
        } catch (error) {
            log.error("Error @ validateItems", error);
            return false;
        }
    }


    /**
     * Get the order region from a custom record based on Ship-To State
     * @param shipToState
     * @returns {Number}
     */
    function getOrderRegionFromCustomRecord(shipToState) {
        try {
            if (!shipToState) return null;
            let customRecordSearch = search.create({
                type: "customrecord_jj_powerpony_state_group",
                filters: [["custrecord_jj_state", "is", shipToState]],
                columns: ["custrecord_jj_destination"],
            });
            let customRecordResult = customRecordSearch
                .run()
                .getRange({ start: 0, end: 1 });
            if (customRecordResult.length > 0) {
                return customRecordResult[0].getValue({
                    name: "custrecord_jj_destination",
                });
            } else {
                log.debug("No matching record found for Ship-To State", shipToState);
                return null;
            }
        } catch (error) {
            log.error("Error @ getOrderRegionFromCustomRecord", error);
            return;
        }
    }


    /**
     * Update the location at the line level for each item
     * @param salesOrder
     * @param location
     */
    function updateLineLevel(salesOrder, location, lineCount) {
        try {
            for (let i = 0; i < lineCount; i++) {
                let currentLineLocation = salesOrder.getSublistValue({
                    sublistId: "item",
                    fieldId: "location",
                    line: i,
                });
                if (currentLineLocation !== location) {
                    salesOrder.setSublistValue({
                        sublistId: "item",
                        fieldId: "location",
                        line: i,
                        value: location,
                    });
                }
            }
        } catch (error) {
            log.error("Error @ updateLineLevel", error);
            return;
        }
    }


    /**
     * Set the location and region on the Sales Order
     * @param salesOrder
     * @param allItemsValid
     * @param loc
     * @param orderRegion
     * @param lineCount
     */
    function setLocationAndRegion(
        salesOrder,
        allItemsValid,
        orderRegion,
        lineCount
    ) {
        try {
            let isWestCoast = orderRegion == WEST_COST ? true : false;
            if (allItemsValid && isWestCoast) {
                //If all items are valid and region is west coast
                salesOrder.setValue({ fieldId: "location", value: PP_WEST });
                salesOrder.setValue({
                    fieldId: "custbody_jj_order_region",
                    value: orderRegion,
                });
                updateLineLevel(salesOrder, PP_WEST, lineCount);
            } else if ((allItemsValid && !isWestCoast) || !allItemsValid) {
                //if all are valid and not west coast region or all items are not valid
                salesOrder.setValue({ fieldId: "location", value: MAIN_WAREHOUSE });
                salesOrder.setValue({
                    fieldId: "custbody_jj_order_region",
                    value: EAST_COST,
                });
                updateLineLevel(salesOrder, MAIN_WAREHOUSE, lineCount);
            }
        } catch (error) {
            log.error("Error @ setLocationAndRegion", error);
            return;
        }
    }


    /**
     * Defines the function that is executed at the beginning of the map/reduce process and generates the input data.
     * @param {Object} inputContext
     * @param {boolean} inputContext.isRestarted - Indicates whether the current invocation of this function is the first
     *     invocation (if true, the current invocation is not the first invocation and this function has been restarted)
     * @param {Object} inputContext.ObjectRef - Object that references the input data
     * @typedef {Object} ObjectRef
     * @property {string|number} ObjectRef.id - Internal ID of the record instance that contains the input data
     * @property {string} ObjectRef.type - Type of the record instance that contains the input data
     * @returns {Array|Object|Search|ObjectRef|File|Query} The input data to use in the map/reduce process
     * @since 2015.2
     */
    const getInputData = (inputContext) => {
        try {
            let soItems = getOrderList();
            return soItems;
        } catch (e) {
            log.error("error @ getInputData", e);
            return [];
        }
    };


    /**
     * Defines the function that is executed when the map entry point is triggered. This entry point is triggered automatically
     * when the associated getInputData stage is complete. This function is applied to each key-value pair in the provided
     * context.
     * @param {Object} mapContext - Data collection containing the key-value pairs to process in the map stage. This parameter
     *     is provided automatically based on the results of the getInputData stage.
     * @param {Iterator} mapContext.errors - Serialized errors that were thrown during previous attempts to execute the map
     *     function on the current key-value pair
     * @param {number} mapContext.executionNo - Number of times the map function has been executed on the current key-value
     *     pair
     * @param {boolean} mapContext.isRestarted - Indicates whether the current invocation of this function is the first
     *     invocation (if true, the current invocation is not the first invocation and this function has been restarted)
     * @param {string} mapContext.key - Key to be processed during the map stage
     * @param {string} mapContext.value - Value to be processed during the map stage
     * @since 2015.2
     */
    const map = (mapContext) => {
        try {
            let salesOrderData = JSON.parse(mapContext.value);
            let orderId = salesOrderData.orderId;


            let shipToState = salesOrderData.shipToState;
            let salesOrder = record.load({
                type: record.Type.SALES_ORDER,
                id: orderId,
            });
            let lineCount = salesOrder.getLineCount({ sublistId: "item" });
            let validSkus = getValidSkusFromParams();
            let allItemsValid = validateItems(salesOrder, lineCount, validSkus);
            let orderRegionFromState = getOrderRegionFromCustomRecord(shipToState);
            let entity = salesOrder.getValue({ fieldId: "entity" });
                setLocationAndRegion(
                    salesOrder,
                    allItemsValid,
                    orderRegionFromState,
                    lineCount
                );


            // Save the updated sales order
            salesOrder.save();
        } catch (e) {
            log.error("error @ map", e);
            return;
        }
    };


    /**
     * Retrieves a list of Sales Orders within a specified date range
     * where the order region is not set.
     *
     * - Filters out tax, COGS, mainline, and shipping lines.
     * - Groups items by Sales Order.
     * - Returns order details including ID, region, location, shipping state, and items.
     *
     * @function getOrderList
     * @returns {Object[]} Array of Sales Order objects with details and items.
     * @throws {Error} Logs and returns an empty array if search execution fails.
     */
    function getOrderList() {
        try {
            var salesorderSearchObj = search.create({
                type: "salesorder",
                filters: [
                    ["type", "anyof", "SalesOrd"],
                    "AND",
                    ["custbody_jj_order_region", "anyof", "@NONE@"],
                    "AND",
                    ["mainline", "is", "T"],
                    "AND",
                    ["datecreated", "onorafter", "11/25/2025 12:00 am"],
                    "AND",
                    ["name", "anyof", CUS_ID]
                ],
                columns: [
                    search.createColumn({ name: "tranid", label: "Document Number" }),
                    search.createColumn({
                        name: "custbody_jj_order_region",
                        label: "Order Region",
                    }),
                    search.createColumn({ name: "location", label: "Location" }),
                    search.createColumn({
                        name: "state",
                        join: "shippingAddress",
                        label: " State",
                    }),
                ],
            });


            let soItemArray = [];
            salesorderSearchObj.run().each(function (result) {
                let orderId = result.id;
                let location = result.getValue({ name: "location" });
                let orderRegion = result.getValue({ name: "custbody_jj_order_region" });
                let shipToState = result.getValue({
                    name: "state",
                    join: "shippingAddress",
                });
                soItemArray.push({
                    orderId: orderId,
                    orderRegion: orderRegion,
                    location: location,
                    shipToState: shipToState,
                });


                return true;
            });
            return soItemArray;
        } catch (e) {
            log.error("Error @ getOrderList", e);
            return [];
        }
    }


    return { getInputData, map };
});


Leave a comment

Your email address will not be published. Required fields are marked *