NetSuite – Calculating Shipping Costs in Sandbox

Why can’t I calculate shipping in sandbox? How can I test that shipping rates change when I change the shipping method? In sandbox, I get this nebulous error: “Could not calculate cost for the selected Realtime rate. UPS did not return any rates for the specified origin and destination addresses.”

There are two places to configure shipping in NetSuite. I’m referencing these from the Administrator role.

  • Setup >> Accounting >> Shipping – where you define your shipping carrier information
  • Lists >> Accounting >> Shipping Items – where you define the delivery methods (like ground, next day air, etc.)

When you refresh your sandbox, most of the carrier info gets transferred, but not all of it. And it is possible that when you setup your carrier info in sandbox, you’ll need special accounts (test accounts) from your carriers.

Since sandbox is not production, but you still need to test, you can simply set unique rates for each of your shipping items. That way, when you change from UPS Ground to 2nd Day air, shipping cost will change. It won’t be correct, but who cares? It’s sandbox! Here is where you’ll do that under the shipping item.

SuiteScript & The Calculate Shipping Button

My job is to find ways to make other people’s job easier. One of my tasks involved calculating shipping from a custom Suitelet. This presented multiple obstacles. Here are my notes. I’m betting this will save both of us time if we ever need to come back to calculating shipping.

First, the code that calculates shipping works like this. It’s client-side code that builds an XML request which is sent to the server to do the heavy lifting. All of NetSuite’s client-side code is written in SuiteScript 1.0. It all ends with a server call that looks like this…

nlapiServerCall("app/common/shipping/dynshippingxml.nl", "getShippingRates", [xmlStr], null, 'POST');

As you can see, the client side code sends a request to the server where it passes xmlStr to getShippingRates. However, all the code which builds xmlStr is buried deep in the standard estimate Edit screen and not available in a custom Suitelet, unless you put it there yourself.

Trick number one, how to find the NetSuite’s client side code that handles building the xmlStr. Using Chrome Developer Tools, you can inspect the button and see which event it fires.

However, searching in Chrome Dev Tools to find calculateRates was futile. So here’s how I found it.

  • Edit an estimate (or sales order)
  • Open Dev Tools
  • In the console tab, type the following and hit enter. It exposes the calculateRates function, along with all the other code which builds the xmlStr parameter.

Once I could see the JavaScript, I could cut and paste it into a text file where I was able to rewrite the SuiteScript 1.0 code and integrate it into my client side JavaScript in my custom Suitelet.

Here is my sample code written in SuiteScript 2.0. It was derived from NetSuite standard “Calculate Shipping” code written in SuiteScript 1.0. If you jump to the bottom and review the calculateRate function, this is where it all begins. And please remember, THIS IS CLIENT-SIDE CODE. It will ultimately be fired by a button click or some other event in your custom Suitelet.

function isValEmpty(val) {
    if (val === null || val === undefined)
        return true;

    val = String(val);
    return (val.length === 0) || !/\S/.test(val);
}


function getExchangeRate(rec) {
    var dExchangeRate = rec.getValue('exchangerate');
    if (isNaN(dExchangeRate) || dExchangeRate.length < 1 || dExchangeRate < 0) {
        dExchangeRate = 1;
    }
    return parseFloat(dExchangeRate);
}

function isFulfillableItem(sItemType, bFulfillable) {
    var bIsFulfillableItem = false;
    if ('Assembly' == sItemType || 'InvtPart' == sItemType || 'Group' == sItemType || 'Description' == sItemType) {
        bIsFulfillableItem = true;
    } else if (('Service' == sItemType || 'OthCharge' == sItemType || 'GiftCert' == sItemType || 'NonInvtPart' == sItemType || 'Kit' == sItemType || 'DwnLdItem' == sItemType) && bFulfillable == 'T') {
        bIsFulfillableItem = true;
    }
    return bIsFulfillableItem;
}

function createRatesRequestXml(requestSrc, ratesCarrier, entityId, destCity, destState, destZip, destCountry, shipMethod, salesOrderId, isResidential, isThirdPartyAcct, thirdPartyCarrier, isPackagesReload, currency, tranFxRate, subsidiary, bIsItemLineRates, testId, nexusId, packages, items, isDefaultRequest, overrideShippingCost, isDynamicScriptingRequest, shipmentParameters) {
    var ratesReqXml = '<RatesRequest>';
    ratesReqXml += createXMLElement('RequestSrc', requestSrc);
    ratesReqXml += createXMLElement('RatesCarrier', ratesCarrier);
    ratesReqXml += createXMLElement('EntityId', entityId);
    ratesReqXml += createXMLElement('IsItemLineRates', bIsItemLineRates);
    ratesReqXml += createXMLElement('DestCity', destCity);
    ratesReqXml += createXMLElement('DestState', destState);
    ratesReqXml += createXMLElement('DestZip', destZip);
    ratesReqXml += createXMLElement('DestCountry', destCountry);
    ratesReqXml += createXMLElement('ShipMethod', shipMethod);
    ratesReqXml += createXMLElement('SalesOrderId', salesOrderId);
    ratesReqXml += createXMLElement('IsResidential', isResidential);
    ratesReqXml += createXMLElement('IsThirdPartyAcct', isThirdPartyAcct);
    ratesReqXml += createXMLElement('ThirdPartyCarrier', thirdPartyCarrier);
    ratesReqXml += createXMLElement('IsPackagesReload', isPackagesReload);
    ratesReqXml += createXMLElement('IsDefaultRequest', isDefaultRequest);
    ratesReqXml += createXMLElement('Currency', currency);
    ratesReqXml += createXMLElement('TransactionFxRate', tranFxRate);
    ratesReqXml += createXMLElement('Subsidiary', subsidiary);
    ratesReqXml += createXMLElement('TestId', testId);
    ratesReqXml += createXMLElement('NexusId', nexusId);
    ratesReqXml += createXMLElement('OverrideShippingCost', overrideShippingCost);
    ratesReqXml += createXMLElement('IsDynamicScriptingRequest', isDynamicScriptingRequest);
    ratesReqXml += getAdditionalShipmentXML(shipmentParameters);
    ratesReqXml += getPackagesXML(packages);
    ratesReqXml += getItemsXML(items);
    ratesReqXml += '</RatesRequest>';
    return ratesReqXml;
}

function createXMLElement(nodeName, nodeValue) {
    if (!isValEmpty(nodeValue)) { return ('<' + nodeName + '>' + xml.escape(nodeValue.toString()) + '</' + nodeName + '>'); } else { return '<' + nodeName + '/>'; }
}

function getAdditionalShipmentXML(parameters) {
    var result = '';
    if (parameters != null) {
        for (var paramKey in parameters) {
            result += createXMLElement(paramKey, parameters[paramKey]);
        }
    }

    return result;
}

function getPackagesXML(packages) {
    var packagesXml = '<Packages>';
    if (packages != null && packages.length > 0) {
        for (var i = 0; i < packages.length; i++) {
            var pkg = packages[i];
            if (pkg != null && pkg != undefined) {
                packagesXml += '<Package>';
                packagesXml += createXMLElement('PackageNumber', pkg['PackageNumber']);
                packagesXml += createXMLElement('PackageLength', pkg['PackageLength']);
                packagesXml += createXMLElement('PackageWidth', pkg['PackageWidth']);
                packagesXml += createXMLElement('PackageHeight', pkg['PackageHeight']);
                packagesXml += createXMLElement('PackageWeight', pkg['PackageWeight']);
                packagesXml += createXMLElement('PackageType', pkg['PackageType']);
                packagesXml += createXMLElement('PackageInsuredValue', pkg['PackageInsuredValue']);
                packagesXml += createXMLElement('PackageSignatureOption', pkg['PackageSignatureOption']);
                packagesXml += createXMLElement('AdditionalHandling', pkg['AdditionalHandling']);
                packagesXml += createXMLElement('UseCOD', pkg['UseCOD']);
                packagesXml += createXMLElement('CODAmount', pkg['CODAmount']);
                packagesXml += createXMLElement('CODMethod', pkg['CODMethod']);
                packagesXml += createXMLElement('DeliveryConfirmation', pkg['DeliveryConfirmation']);
                packagesXml += createXMLElement('DryIceWeight', pkg['DryIceWeight']);
                packagesXml += createXMLElement('DryIceUnit', pkg['DryIceUnit']);
                packagesXml += createXMLElement('CODTransportationCharges', pkg['CODTransportationCharges']);
                packagesXml += createXMLElement('CODOtherCharge', pkg['CODOtherCharge']);
                packagesXml += '</Package>';
            }
        }
    }
    packagesXml += '</Packages>';
    return packagesXml;
}

function getItemsXML(items) {
    var itemsXml = '<Items>';
    if (items != null && items.length > 0) {
        for (var i = 0; i < items.length; i++) {
            var item = items[i];
            if (item != null && item != undefined) {
                itemsXml += '<Item>';
                itemsXml += createXMLElement('ItemQuantity', item['ItemQuantity']);
                itemsXml += createXMLElement('ItemAmount', item['ItemAmount']);
                itemsXml += createXMLElement('ItemWeight', item['ItemWeight']);
                itemsXml += createXMLElement('ItemKey', item['ItemKey']);
                itemsXml += createXMLElement('ItemLocation', item['ItemLocation']);
                itemsXml += createXMLElement('ItemUnits', item['ItemUnits']);
                itemsXml += createXMLElement('ItemType', item['ItemType']);
                itemsXml += createXMLElement('ItemExcludeFromRateRequest', item['ItemExcludeFromRateRequest']);
                itemsXml += createXMLElement('ItemShipAddrKey', item['ItemShipAddrKey']);
                itemsXml += createXMLElement('ItemShipAddr1', item['ItemShipAddr1']);
                itemsXml += createXMLElement('ItemShipAddr2', item['ItemShipAddr2']);
                itemsXml += createXMLElement('ItemShipCity', item['ItemShipCity']);
                itemsXml += createXMLElement('ItemShipState', item['ItemShipState']);
                itemsXml += createXMLElement('ItemShipZip', item['ItemShipZip']);
                itemsXml += createXMLElement('ItemShipCountry', item['ItemShipCountry']);
                itemsXml += createXMLElement('ItemShipIsResidential', item['ItemShipIsResidential']);
                itemsXml += createXMLElement('ItemShipMethKey', item['ItemShipMethKey']);
                itemsXml += createXMLElement('ItemName', item['ItemName']);
                itemsXml += createXMLElement('ItemDescription', item['ItemDescription']);
                itemsXml += createXMLElement('ItemCountryOfManufacture', item['ItemCountryOfManufacture']);
                itemsXml += createXMLElement('ItemProducer', item['ItemProducer']);
                itemsXml += createXMLElement('ItemExportType', item['ItemExportType']);
                itemsXml += createXMLElement('ItemManufacturerName', item['ItemManufacturerName']);
                itemsXml += createXMLElement('ItemMultManufactureAddr', item['ItemMultManufactureAddr']);
                itemsXml += createXMLElement('ItemManufacturerAddr1', item['ItemManufacturerAddr1']);
                itemsXml += createXMLElement('ItemManufacturerCity', item['ItemManufacturerCity']);
                itemsXml += createXMLElement('ItemManufacturerState', item['ItemManufacturerState']);
                itemsXml += createXMLElement('ItemManufacturerZip', item['ItemManufacturerZip']);
                itemsXml += createXMLElement('ItemManufacturerTaxId', item['ItemManufacturerTaxId']);
                itemsXml += createXMLElement('ItemManufacturerTariff', item['ItemManufacturerTariff']);
                itemsXml += createXMLElement('ItemPreferenceCriterion', item['ItemPreferenceCriterion']);
                itemsXml += createXMLElement('ItemScheduleBNumber', item['ItemScheduleBNumber']);
                itemsXml += createXMLElement('ItemScheduleBQuantity', item['ItemScheduleBQuantity']);
                itemsXml += createXMLElement('ItemScheduleBCode', item['ItemScheduleBCode']);
                itemsXml += createXMLElement('ItemUnitsDisplay', item['ItemUnitsDisplay']);
                itemsXml += createXMLElement('ItemUnitPrice', item['ItemUnitPrice']);
                itemsXml += createXMLElement('ItemLine', item['ItemLine']);
                itemsXml += createXMLElement('ItemTotalQuantity', item['ItemTotalQuantity']);
                itemsXml += createXMLElement('ItemQuantityRemaining', item['ItemQuantityRemaining']);
                itemsXml += createXMLElement('ItemTotalAmount', item['ItemTotalAmount']);
                itemsXml += '</Item>';
            }
        }
    }
    itemsXml += '</Items>';
    return itemsXml;
}


function getShippingRate(quote_id) {
    var quote = record.load({
        type: record.Type.ESTIMATE,
        id: quote_id,
        isDynamic: true
    });

    var itemlinecount = quote.getLineCount('item');

    var bIsItemLineShippingRequest = (quote.getValue('ismultishipto') == 'T');
    var ratesType = quote.getValue('shipping_cost_function');
    if (!bIsItemLineShippingRequest && (ratesType == null || ratesType.length == 0)) {
        invalidateShippingCharge();
        return true;
    }
    var destCity = quote.getValue('shipcity');
    var destState = quote.getValue('shipstate');
    var destZip = quote.getValue('shipzip');
    var destCountry = quote.getValue('shipcountry');
    var shipMethod = quote.getValue('shipmethod');
    var currency = quote.getValue('currency');
    var isResidential = quote.getValue('shipisresidential');
    var isthirdpartyacct = 'F';
    var thirdpartyacct = '';
    var thirdpartycarrier = '';
    var items = new Array();
    var totalWeight = 0;
    var iladdrcount = -1;
    if (bIsItemLineShippingRequest) {
        iladdrcount = quote.getLineCount('iladdrbook');
    }
    for (var i = 0; i < itemlinecount; i++) {
        var itemtype = quote.getSublistValue('item', 'itemtype', i);
        var bFulfillable = quote.getSublistValue('item', 'fulfillable', i);
        if (!isFulfillableItem(itemtype, bFulfillable)) {
            continue;
        }
        var nkey = quote.getSublistValue('item', 'item', i);
        var weight = quote.getSublistValue('item', 'weightinlb', i);
        var quan = quote.getSublistValue('item', 'quantity', i);
        var amount = quote.getSublistValue('item', 'amount', i);
        var excludeFromRateRequest = quote.getSublistValue('item', 'excludefromraterequest', i);
        var location = parseInt(quote.getSublistValue('item', 'location', i));
        if (isNaN(location)) {
            location = getTranLocation();
        }
        if (weight == null || isNaN(weight) || weight.length < 1 || weight < 0) {
            weight = 0;
        }
        if (quan == null || isNaN(quan) || quan.length < 1 || quan < 0) {
            quan = 0;
        }
        if (amount == null || isNaN(amount) || amount.length < 1 || amount < 0) {
            amount = 0;
        }
        var nQuantity = parseInt(quan);
        if (bIsItemLineShippingRequest) {
            var itemshipaddr1 = '';
            var itemshipaddr2 = '';
            var itemshipcity = '';
            var itemshipstate = '';
            var itemshipzip = '';
            var itemshipcountry = '';
            var itemshipisresidential = '';
            var itemshipaddrkey = quote.getSublistValue('item', 'shipaddress', i);
            for (var iladdridx = 0; iladdridx < iladdrcount; iladdridx++) {
                var iladdrbookkey = quote.getSublistValue('iladdrbook', 'iladdrinternalid', iladdridx);
                if (itemshipaddrkey == iladdrbookkey) {
                    itemshipaddr1 = quote.getSublistValue('iladdrbook', 'iladdrshipaddr1', iladdridx);
                    itemshipaddr2 = quote.getSublistValue('iladdrbook', 'iladdrshipaddr2', iladdridx);
                    itemshipcity = quote.getSublistValue('iladdrbook', 'iladdrshipcity', iladdridx);
                    itemshipstate = quote.getSublistValue('iladdrbook', 'iladdrshipstate', iladdridx);
                    itemshipzip = quote.getSublistValue('iladdrbook', 'iladdrshipzip', iladdridx);
                    itemshipcountry = quote.getSublistValue('iladdrbook', 'iladdrshipcountry', iladdridx);
                    itemshipisresidential = quote.getSublistValue('iladdrbook', 'iladdrshipisresidential', iladdridx);
                    break;
                }
            }
            var itemshipmethkey = quote.getSublistValue('item', 'shipmethod', i);
        }
        var nUnits = 0;
        var nWeight = parseFloat(weight);
        if (!isNaN(nQuantity) && !isNaN(nWeight)) {
            weight = nWeight * nQuantity;
            totalWeight += weight;
        }
        if (amount && quan) {
            var itm = {};
            itm['ItemQuantity'] = quan;
            itm['ItemAmount'] = amount;
            itm['ItemWeight'] = weight;
            itm['ItemKey'] = nkey;
            itm['ItemLocation'] = location;
            itm['ItemUnits'] = nUnits;
            itm['ItemType'] = itemtype;
            itm['ItemExcludeFromRateRequest'] = excludeFromRateRequest;
            if (bIsItemLineShippingRequest) {
                itm['ItemShipAddrKey'] = itemshipaddrkey;
                itm['ItemShipAddr1'] = itemshipaddr1;
                itm['ItemShipAddr2'] = itemshipaddr2;
                itm['ItemShipCity'] = itemshipcity;
                itm['ItemShipState'] = itemshipstate;
                itm['ItemShipZip'] = itemshipzip;
                itm['ItemShipCountry'] = itemshipcountry;
                itm['ItemShipIsResidential'] = itemshipisresidential;
                itm['ItemShipMethKey'] = itemshipmethkey;
            }
            itm['Name'] = nlapiGetLineItemText('item', 'item', i);
            itm['ItemDescription'] = quote.getSublistValue('item', 'description', i);
            itm['Quantity'] = quote.getSublistValue('item', 'quantity', i);
            itm['ItemUnitPrice'] = quote.getSublistValue('item', 'rate', i);
            items.push(itm);
        }
    }
    if (ratesType == 'fedexRealTimeRate') {
        var fedexServiceName = quote.getValue('fedexservicename');
        if (totalWeight > getFedExMaxPackageWeight(fedexServiceName)) {
            if (!(fedexServiceName == 'FEDEX1DAYFREIGHT') && !(fedexServiceName == 'FEDEX2DAYFREIGHT') && !(fedexServiceName == 'FEDEX3DAYFREIGHT') && !(fedexServiceName == 'FIRSTOVERNIGHTFREIGHT') && !(fedexServiceName == 'INTERNATIONALECONOMYFREIGHT') && !(fedexServiceName == 'INTERNATIONALPRIORITYFREIGHT')) {
                alert(getFedExMaxWeightExceededMsg(fedexServiceName));
            }
        }
    }
    var subsidiary = '';
    var tranFxRate = getExchangeRate(quote);
    var entityId = quote.getValue('entity');
    var testId = quote.getValue('testid');
    var nexusId = quote.getValue('nexus');
    var isDefaultRequest = quote.getValue('isdefaultshippingrequest');
    var overrideShippingCost = quote.getValue('overrideshippingcost');
    var shipmentParameters = {};
    var reqXml = createRatesRequestXml('SALESORDER', ratesType, entityId, destCity, destState, destZip, destCountry, shipMethod, null, isResidential, isthirdpartyacct, thirdpartycarrier, false, currency, tranFxRate, subsidiary, bIsItemLineShippingRequest, testId, nexusId, null, items, isDefaultRequest, overrideShippingCost, false, shipmentParameters);

    return reqXml

}

function calculateRate(quote_id) {
    var xmlStr = getShippingRate(quote_id);

    var parameters = JSON.stringify({
        method: 'remoteObject.getShippingRates',
        params: [xmlStr]
    });

    https.post.promise({
        url: '/app/common/shipping/dynshippingxml.nl',
        body: parameters
    })
        .then(
            function (httpsResponse) {
                var objResult = JSON.parse(httpsResponse.body);
                var parser = new DOMParser();
                var xmlDoc = parser.parseFromString(objResult.result, "text/xml");
                var shippingRate = xmlDoc.getElementsByTagName('ShippingRate')[0].childNodes[0].nodeValue;
                alert(shippingRate);
            }
        )
        .catch(
            function onRejected(reason) {
                alert(reason);
            }
        )
}

SuiteScript – Shipping Carriers and Methods Unraveled

Like almost all my posts, this is a simple concept that quickly turned into brain surgery. So let’s dumb this down.

I needed a way to select and set the shipping method on an estimate from a Suitelet. Before I even started coding, I noticed that I had different shipping methods based on my selection of shipping carriers. What I mean by this is that when editing an estimate, when you change the shipping carrier, it blanks the shipping methods dropdown and the choices change. So how does NetSuite know which methods (also known as shipitem) are associated with each of the two carriers? Great question!

Setup >> Accounting >> Shipping — Shows carriers

List >> Accounting >> Shipping Items — Shows shipping methods

I’ll share some code written in SuiteScript 2.1 that can be dropped into debugger. When you run it, you’ll see the list of all shipping methods. It is not limited by carrier and in fact the carrier field is blank. Why? Who knows?

However, what I learned was that any custom shipping methods (shipitem) gets associated with the FedEx/USPS/more carrier. AND… when you change the shipping method, it automatically changes the carrier. So set to one of the UPS methods and the carrier magically becomes UPS. Set to any method not prefixed with UPS, and the carrier becomes FedEx/USPS/more.

In an estimate, after changing the method to any method other than one of the UPS methods, and inspect it using SuiteScript Field Explorer (a Chrome plugin), you’ll see the carrier is set to “nonups”.

So it would appear that the two carriers (under the hood) are UPS and “Other”!

Here is my code. The commented section at the bottom was my great revelation. Changing the method automatically changes the carrier. Be sure to select script type of 2.1 when you run this in debugger.

require(['N/search', 'N/record'],
    (search, record) => {
        log.debug('Start');

        let mySearch = search.create({
            type: 'shipitem',
            columns: [
                search.createColumn({ name: 'internalid'}),
                search.createColumn({ name: 'carrier', sort: 'ASC'}),
                search.createColumn({ name: 'itemid', sort: 'ASC'})
            ],
            filters: [
                ['isinactive', 'is', false]
            ]
        })

        let myPages = mySearch.runPaged({ pageSize: 1000 });
        for (let i = 0; i < myPages.pageRanges.length; i++) {
            let myPage = myPages.fetch({ index: i });
            myPage.data.forEach(
                function (result) {
                    let internalid = result.getValue('internalid');
                    let carrier = result.getValue('carrier');
                    let itemid = result.getValue('itemid');
                    log.debug(`${carrier}: ${itemid}: ${internalid}`)
                }
            )
        }


        // var rec = record.load({
        //     type: record.Type.ESTIMATE,
        //     id: xxxxxxxx,
        //     isDynamic: false
        // });

        // rec.setValue('shipmethod', xxx);

        // rec.save();

        log.debug('Done');
    }
)

SuiteScript 2.0 – “An unexpected SuiteScript error has occurred” with Array.sort()

Here is another coding challenge that set me back more time than I would have liked. Passing an array of objects to the Array.sort() function worked most of the time, but not always. And when it blew up, the error was completely unhelpful.

Please note this is server-side code. Client-side code runs in a different JavaScript engine. In my case it was running inside a Suitelet written in SuiteScript 2.0. I did NOT try this same problem in SuiteScript 2.1 which gets a completely new JavaScript engine and ES6. Perhaps it might have worked. Moving from 2.0 to 2.1 requires also updating libraries. That was too much work. Here was my basic problem:

var transactions = [
   {
      salesrep: salesrep_id,
      salesorder_total: salesorder_total
   },...
   /* In my case, there were less than 200 
      records in this array when it failed.
      This was far short of the number of 
      records it processed successfully.
      The only thing I noticed about this 
      particular set of records which caused 
      the failure was the number of records 
      where salesorder_total was zero. 
      Not all, but more than a few! */
]

var sortedbyTotalDescending = transactions.sort(
      function(a, b) {
            return a.salesorder_total <= b.salesorder_total ? 1 : -1;
      }
);

This worked correctly for months. However, there were certain times when it blew up. This is what solved the problem. And don’t ask me why. I’m clueless. It should have. Please direct your questions to NetSuite. I believe it is a bug in their SuiteScipt 2.0 JavaScript engine!

/* This one works! */
var sortedbyTotalDescending = transactions.sort(
      function(a, b) {
            if (a.salesorder_total == b.salesorder_total) return 0;
            return a.salesorder_total < b.salesorder_total ? 1 : -1;
      }
);


SuiteScript – Modifying Addresses

Understanding how to edit addresses under a customer turned out to be a bit tricky. Here is how I did it.

First, you can add custom fields, but to do that, you need to go to

Customization >> Forms >> Address Forms

Customize the “Standard Address Form.” Assign a country or countries to your form. This is important if you plan to use custom fields in an address. The form is where you add them. Custom fields will then be available to addresses in the assigned countries.

Next, it is important to understand that addresses are a subrecord. The sublist is called “addressbook” and is indexed by “addressbookaddress”. This will make more sense when you review the code below. You read the sublist, and then the associated subrecord under it. Here are the standard fields in the sublist for “addressbook”.

Label – not unique and if omitted becomes Address Line 1
defaultbilling – must be unique to one address
defaultshipping – must be unique to one address
isresidential – can apply to any or all addresses

Under each address is a subrecord that contains these fields:

Attention
Addressee
Address Line 1
Address Line 2
Address Line 3
City
State
Zip
Any custom fields that you’ve created

Here is a code example that demonstrates how to update addresses. The variable “line” corresponds with a zero-based index of the address as it appears in the Address tab under a customer. The variable “customer_id” is the internal ID of the customer and all assigned values are coming from variables which you’d set prior to running this code.

    /**
    * Load the record to gain access to the Address sublist 
    */
    var rec = record.load({
        type: record.Type.CUSTOMER,
        id: customer_id,
        isDynamic: false
    });

    if (/* you need to insert an address, do it here. You decide! */) {
        rec.insertLine({
            sublistId: 'addressbook',
            line: 0
        });
        line = 0;
    }

    /**
    * Modify those fields present in the Address sublist lines
    */
    rec.setSublistValue({
        sublistId: 'addressbook',
        fieldId: 'label',
        value: label,
        line: line
    });

    rec.setSublistValue({
        sublistId: 'addressbook',
        fieldId: 'defaultbilling',
        value: defaultbilling,
        line: line
    });

    rec.setSublistValue({
        sublistId: 'addressbook',
        fieldId: 'defaultshipping',
        value: defaultshipping,
        line: line
    });

    rec.setSublistValue({
        sublistId: 'addressbook',
        fieldId: 'isresidential',
        value: is_residential,
        line: line
    });


    /**
    * Load the sublist record, which holds all the actual address fields
    */
    var subrec2 = rec.getSublistSubrecord({
        sublistId: 'addressbook',
        fieldId: 'addressbookaddress',
        line: line
    });

    /**
        * Modify all subrecord fields
        */
    subrec2.setValue({
        fieldId: 'attention',
        value: attention
    });

    subrec2.setValue({
        fieldId: 'addressee',
        value: addressee
    })

    subrec2.setValue({
        fieldId: 'addr1',
        value: addr1
    });

    subrec2.setValue({
        fieldId: 'addr2',
        value: addr2
    });

    subrec2.setValue({
        fieldId: 'addr3',
        value: addr3
    })

    subrec2.setValue({
        fieldId: 'city',
        value: city
    });

    subrec2.setValue({
        fieldId: 'state',
        value: state
    });

    subrec2.setValue({
        fieldId: 'zip',
        value: zip
    });


    /**
    * Save the record
    */
    rec.save();

SuiteTalk 2021.2 Deprecating HMAC-SHA1

I recently got this in an email from NetSuite: “You are receiving this notification because you use the Token-based Authentication (TBA) feature in your account for integrations that use HMAC-SHA1 as a signature method. As of NetSuite 2021.2, the use of HMAC-SHA1 will be deprecated. Before your account is upgraded to 2021.2, you must change your integrations to use HMAC-SHA256 as the signature method.

It sounded ominous, but turned out not to be a huge effort. If you’re like me, this TBA stuff captured my attention just long enough to get it working. So getting back into it took a little refamiliarizing. Hopefully this will save you some time.

In my post SuiteTalk TBA Example in C#, you’ll find the following code snippet. Here are the changes required to move from HMAC-SHA1 to HMAC-SHA256. I commented out the old block to highlight what changed.

Problem: Leading Zeros in Server-Side JavaScript

I needed a method that would validate dates in multiple formats. Here are examples of dates that needed to pass validation: ‘6/9/2021’, ‘6/10/2021’, ‘9-Jun-2021′, ’10-Jun-2021′, ’06/09/2021′, ’06/10/2021’. This problem came to me on June 9th, 2021. So ’06/09/2021′ failed, but ’06/10/2021′ passed. Hmm… If you guessed it was not allowing backdates, you’d be wrong.

In my testing I learned that typeof 09 == ‘number’, but parseInt(09) = NaN. How can that be?

The NetSuite (server-side) JavaScript engine treats numbers with leading zeros as base-8 (octal). So be sure to strip leading zeros, even on integer numbers of type ‘number’. Since base-8 only included digits 0 through 7, parseInt(09) is not a number!

Replace NetSuite’s Native Rich Text Editor with TinyMCE

NetSuite’s native rich text editor has some problems. It often accepts or even creates HTML that NetSuite won’t print. Here’s how to replace it with TinyMCE, a rich text editor that works much better.

Start here, Marty Zigman’s wonderful article entitled, Replace NetSuiteā€™s Rich Text Editor with a Custom HTML Editor.

Now that you know all the steps, here’s my method. It’s much shorter and has fewer moving parts, but it breaks a few NetSuite rules regarding not editing the DOM.

It all begins with understanding what make’s NetSuite’s out-of-the-box rich text control work. Here is what the control looks like. In this case, I’m editing a custom fields called custbody_note_internal.

And here is the code that represents this control.

The rubber meets the road here:

<input id="custbody_note_internal" name="custbody_note_internal" type="hidden">

This is the control that is read back by the server to update the field in the underlying record. I found that if I swapped this type=”hidden” for a textarea control, the server accepted it just the same! And once you make the swap to a textarea, you can easily turn that textarea into a TinyMCE control.

Per Marty’s article above, you need to include the TinyMCE javascript library. He does this via client-side JavaScript and also adds code to wait for it to load. In my case (and I’m not showing this here), I load the reference along with other JavaScript libraries on the server-side. I download the link as part of the page. I’m still referencing the CDN delivered library. Because I’m delivering the link in the page and not dynamically via client-side JavaScript, I don’t need to wait for the library to load, which saves a step for me.

Next, anyplace client-side where I can run an after-load script tucked away inside a $(document).read() function, I place this code.

Let’s walk through this.

Line 1367: I’m getting a reference to multiple custom fields that all require a rich text edit swap to TinyMCE.
Line 1370: I’m getting the name and ID of the “Rubber meets the road hidden input field.”
Line 1371: I’m getting the initial value of the rich text editor I’m replacing.
Line 1372: I’m replacing the span tag on line 18 of the HTML with a textarea that has the name and ID of my custom field. It also has a class of “tinymce-richtext”.
Line 1379: I’m initializing TinyMCE on all my swapped textareas. I use the class to initialize them all at once.
Line 1384: Here is magic! This is an onchange event fired by TinyMCE. It copies the contents of the TinyMCE editor into the textarea that I created in line1372.

What’s amazing about this is that TinyMCE will convert pasted images into base-64 encoded html tags. These “Image” tags cut and paste just like regular text. so you can paste snippets of screens or any other image from your clipboard into the TinyMCE editor and not only will it save with the underlying record, it prints too!

This solution breaks NetSuite’s rule of not editing the DOM. However, it saves a number of steps outlined in Marty’s article. It comes with no guarantees. Use it at your own risk. And… be sure to test it with each new release of NetSuite. OK… That’s my final disclaimer. Good luck!

SuiteScript – Find Sublist Line by Value

This one eluded me. I needed to update Item Pricing records under a customer. So how to avoid iterating through every item in the sublist in order to find the line to update? Here’s the secret!

  • Load the customer using isDynamic = false (Standard Mode)
  • Lookup the line number of the line you need update using record.findSublistLineWithValue()
  • Then simply update the line just like you always do using the line number (zero based offset).

Here is a code example.

Here is the code in a format that you can cut and paste.

var customer = record.load({
    type: record.Type.CUSTOMER,
    id: kvp.customer_id,
    isDynamic: false
})

updated_kvp.pricing_items.forEach(
    function (pricing_item) {
        if (pricing_item.hasOwnProperty('unit_price')) {

            var lineNumber = customer.findSublistLineWithValue({
                sublistId: 'itempricing',
                fieldId: 'item',
                value: pricing_item.id
            });

            if (lineNumber >= 0) {
                customer.setSublistValue({
                    sublistId: 'itempricing',
                    fieldId: 'price',
                    line: lineNumber,
                    value: pricing_item.unit_price
                });
            }
        }
    }
)

customer.save();

SuiteScript – Accessing a Custom Record Type Sublist

This is documented in several NetSuite and non-NetSuite articles. I read them all and still found it almost impossible to decipher how to programmatically read a custom record type sublist.

Feel free to start with SuiteAnswers article 65795, or Google “Sublist recmach”, or whatever else you can think of… Or just read this! In my example I’m using two custom record types. The first is the parent of the second. The second is a sublist of the first.

Here is the parent custom record type. Nothing is important here except the name: customrecord_logic_software_agreements

Here is the child custom record type where it is very important to check the “Allow Child Record Editing.” Otherwise, nothing works! It is also vitally important to include a reference to the parent table. Here you see my reference to “Software Agreement.” Notice it is of type “List/Record” and the reference is to the custom record type “Software Agreements.”

Next, and also essential, is the definition of the foreign-key relationship between child and parent. In my case, custrecord_logic_sft_line_agreement. It is important to make sure to note that this field is the link to the parent record. You do that by checking “Record is Parent.”

Now, here is where the magic begins. The sublistId is the scriptId of the foreign-key field prefixed with “recmach”. Crazy, I know! But… It works!

I hope this saves you a lot of time. And more importantly, I hope this saves ME a lot lot of time the next time I want to programmatically reference a non-standard record type as a sublist.

Happy coding!