Building Customer-Specific Pricing Automation in NetSuite: A Technical Guide for Distributors
The Business Case Pricing in distribution is rarely simple. A single SKU might have a different price depending on the customer relationship, the...
Sign up to hear about Snapshot's latest news and projects!
4 min read
Steve Springer
:
Apr 16, 2026 2:31:45 PM
In any multi-location distribution operation, the gap between "what's in the catalog" and "what's at this warehouse" creates a persistent source of order errors. Sales reps work fast. They know the product line. But they don't always know or remember which items are stocked at which fulfillment locations. And NetSuite's native transaction forms don't enforce that relationship out of the box.
The cost of these errors compounds quickly. A single misrouted item can trigger an inter-warehouse transfer, a shipment delay, a customer notification, and a manual order correction. Multiply that across dozens of daily orders, and you've got a meaningful operational drag that's entirely preventable.
The solution is a SuiteScript 2.x client script deployed on Sales Orders and Quotes. Client scripts run in the browser, which means validation happens instantly as reps interact with the form — no waiting for a save or a server round-trip.
We rely on three key event handlers:
The fieldChanged handler watches the fulfillment location field on the order header. When it changes, the script iterates through all existing line items and checks each one against the new location.
The validateLine handler fires every time a user commits a line item. It reads the item's allowed locations (stored as a multi-select custom field on the item record) and checks whether the currently selected fulfillment location is in that list.
The saveRecord handler acts as a final safety net, running a complete revalidation of all lines before the transaction is committed to the database.
The foundation of this approach is a multi-select field on the item record — let's call it custitem_valid_locations. This field holds the list of locations where the item is available for fulfillment. Maintaining this field is straightforward: as inventory is stocked or removed from locations, the field is updated accordingly. Some companies manage this manually; others automate it based on inventory levels or warehouse management rules.
When the script needs to validate, it reads this field from the current line's item and checks whether the transaction's fulfillment location appears in the list. Here's the core validation pattern:
function isItemValidForLocation(itemId, locationId) {
var itemRecord = record.load({
type: record.Type.INVENTORY_ITEM,
id: itemId
});
var validLocations = itemRecord.getValue({
fieldId: 'custitem_valid_locations'
});
// Multi-select returns an array of internal IDs
return validLocations.indexOf(locationId.toString()) !== -1;
}
In practice, you'd want to cache item lookups to avoid redundant record loads when revalidating multiple lines against a location change. A simple in-memory object keyed by item ID works well for this since the data doesn't change during a single transaction edit.
The trickiest part of the implementation is handling location changes on the order header. When a user selects a new fulfillment location, you can't just validate and move on — you need to check every existing line, and if any fail, you need to revert the location change and tell the user why.
The fieldChanged handler doesn't natively support blocking a change the way validateField does. So the approach is to store the previous location value before the change, run the validation, and if it fails, set the field back to the previous value programmatically:
function fieldChanged(context) {
if (context.fieldId === 'location') {
var newLocation = context.currentRecord.getValue('location');
var invalidLines = [];
var lineCount = context.currentRecord.getLineCount('item');
for (var i = 0; i < lineCount; i++) {
var itemId = context.currentRecord.getSublistValue({
sublistId: 'item',
fieldId: 'item',
line: i
});
if (!isItemValidForLocation(itemId, newLocation)) {
invalidLines.push(i + 1);
}
}
if (invalidLines.length > 0) {
dialog.alert({
title: 'Invalid Location',
message: 'Lines ' + invalidLines.join(', ') +
' contain items not available at this location.'
});
context.currentRecord.setValue({
fieldId: 'location',
value: previousLocation
});
} else {
previousLocation = newLocation;
}
}
}
Note the use of a module-scoped previousLocation variable, set during pageInit or the last successful location change.
Real-world operations need escape hatches. Special orders, in-transit inventory, temporary stocking arrangements — there are legitimate reasons to bypass validation. We handle this with a custom checkbox field on the transaction (custbody_override_location_check). When checked, all validation logic is skipped.
The key design decision here is who can check that box. You can control this through role-based field permissions in NetSuite, restricting the override to managers or specific roles. This gives you the flexibility without opening the door to everyone ignoring the validation entirely.
A few things we've learned from implementing this pattern across multiple clients:
Performance matters. Loading full item records inside a loop will make the form feel sluggish, especially on orders with 50+ lines. Use search.lookupFields for targeted field reads, or better yet, run a single saved search up front to get all items' valid locations in one call.
Multi-select field handling has quirks. The getValue call on a multi-select field returns an array of strings, not numbers. Make sure you're comparing string-to-string when checking location IDs.
Don't forget about edit mode. When a user opens an existing order, the pageInit handler should capture the current location for the revert logic. Without this, your first location change won't have a fallback value.
Test with item groups and kits. If your client uses these, you'll need to decide whether validation applies to the parent item, the member items, or both — and handle the sublist differences accordingly.
You might wonder why we chose a client script over a beforeSubmit user event. The answer is user experience. A user event script would catch the error on save, after the rep has finished building the entire order. That's a frustrating experience. Client scripts catch the problem line by line, in real time, with immediate feedback. The user knows exactly which item is the issue and can fix it before moving on.
That said, we still include a saveRecord check as a belt-and-suspenders measure. It catches edge cases like programmatic line additions or scenarios where the client script might not fire.
If your team is dealing with fulfillment errors rooted in item-location mismatches, this kind of validation is one of the highest-ROI customizations you can make in NetSuite. It's not complex to build, it doesn't require any third-party tools, and the operational improvement is immediate.
At Snapshot Design, we build these kinds of practical, targeted customizations for mid-market distributors and manufacturers every day. If you'd like to talk through how this would work in your environment, we're always happy to have that conversation.
The Business Case Pricing in distribution is rarely simple. A single SKU might have a different price depending on the customer relationship, the...
The Comfortable Trip
For businesses strategically focused on expansion and sustained growth, a seamless and efficient payment infrastructure is essential. It directly...