4 min read

Building Customer-Specific Pricing Automation in NetSuite: A Technical Guide for Distributors

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 product category, and the warehouse fulfilling the order. When these variables are managed manually — through spreadsheets, memorized rates, or fragile saved search workarounds — errors creep in. Margins erode quietly. Reps lose confidence in the system and start bypassing it.

For mid-market distributors running NetSuite, the challenge is that the platform's native pricing tools — price levels, quantity schedules, and item-specific pricing — don't natively support a location dimension. When your pricing varies by ship-from warehouse or branch, you need a different approach.

This post walks through how we built a SuiteScript-based solution that automates customer/product/location pricing lookups on transactions, ensuring every line item gets the right price without manual intervention.

 

Architecture: Custom Record + Client Script

The solution has two components: a custom record that stores pricing data and a client script that queries it in real time during order entry.

The Pricing Record

We created a custom record type — let's call it "Customer Location Pricing" — with the following key fields:

  • Customer (list/record field linked to the Customer record)
  • Item (list/record field linked to the Item record)
  • Location (list/record field linked to the Location record)
  • Price (currency field)
  • Effective Date and Expiration Date (for time-bound pricing)
  • Price Levels: Support customer-specific pricing but lack a location dimension. If the same customer gets different pricing from different warehouses, price levels can't model that.
  • Quantity Pricing Schedules: Useful for volume breaks but not for customer/location combinations.
  • Custom Pricing via Workflows: SuiteFlow can set field values, but the conditional logic for multi-dimensional lookups becomes brittle and hard to maintain. A script gives you full control over the lookup logic, fallback hierarchy, and error handling.
  • Third-Party Pricing Tools: Exist but often introduce licensing costs and integration complexity for a problem that a well-designed custom record and a few hundred lines of SuiteScript can solve cleanly.

This structure gives the pricing team a flat, maintainable table. They can filter by customer, bulk-update by item, and audit changes through NetSuite's standard system notes. No developer involvement required for day-to-day rate changes.

The Client Script

The client script deploys on Sales Orders (and optionally Quotes and Invoices) and hooks into two key events:

postSourcing — fires after a field's dependent values have been set. When a rep selects an item on a line, postSourcing lets us read the item value after NetSuite has finished populating its defaults, then query our custom record for the correct price.

fieldChanged — fires when the customer or location changes at the header level. This triggers a full reprice loop across all existing lines.

Here's a simplified version of the pattern:

/**

* @NApiVersion 2.1

* @NScriptType ClientScript

* @NModuleScope SameAccount

*/

define(['N/search', 'N/currentRecord'], (search, currentRecord) => {

function getCustomerPrice(customerId, itemId, locationId) {

let price = null;

search.create({

type: 'customrecord_cust_loc_pricing',

filters: [

['custrecord_clp_customer', 'anyof', customerId],

'AND',

['custrecord_clp_item', 'anyof', itemId],

'AND',

['custrecord_clp_location', 'anyof', locationId],

'AND',

['custrecord_clp_effective', 'onorbefore', 'today'],

'AND',

['custrecord_clp_expiration', 'onorafter', 'today']

],

columns: ['custrecord_clp_price']

}).run().each(result => {

price = result.getValue('custrecord_clp_price');

return false; // first match wins

});

return price;

}

function postSourcing(context) {

if (context.sublistId === 'item'

&& context.fieldId === 'item') {

const rec = context.currentRecord;

const customerId = rec.getValue('entity');

const locationId = rec.getValue('location');

const itemId = rec.getCurrentSublistValue({

sublistId: 'item', fieldId: 'item'

});

if (customerId && itemId && locationId) {

const price = getCustomerPrice(

customerId, itemId, locationId

);

if (price) {

rec.setCurrentSublistValue({

sublistId: 'item',

fieldId: 'rate',

value: price

});

}

}

}

}

function fieldChanged(context) {

const rec = context.currentRecord;

if (['entity', 'location'].includes(context.fieldId)) {

const customerId = rec.getValue('entity');

const locationId = rec.getValue('location');

const lineCount = rec.getLineCount({ sublistId: 'item' });

for (let i = 0; i < lineCount; i++) {

rec.selectLine({ sublistId: 'item', line: i });

const itemId = rec.getCurrentSublistValue({

sublistId: 'item', fieldId: 'item'

});

const price = getCustomerPrice(

customerId, itemId, locationId

);

if (price) {

rec.setCurrentSublistValue({

sublistId: 'item',

fieldId: 'rate',

value: price

});

rec.commitLine({ sublistId: 'item' });

}

}

}

}

return { postSourcing, fieldChanged };

});

 

Common Pitfalls

1. Using fieldChanged Instead of postSourcing for Line Items

A frequent mistake is using the fieldChanged event to detect item selection on a line. The problem is that fieldChanged fires before NetSuite finishes sourcing dependent fields, so you may be reading stale data. postSourcing is the correct event for reacting to item selection because it fires after all sourcing is complete.

2. Not Handling the "No Match" Case Gracefully

If there's no pricing record for a given combination, the script should fall back to the item's standard rate rather than blanking out the price or throwing an error. Build a clear fallback hierarchy: customer/item/location price → customer/item price → standard rate.

3. Performance on Large Orders

The reprice loop that fires on customer or location change runs a search for every line. On orders with 50+ lines, this can feel slow. Two mitigation strategies: batch the lookups into a single search with an OR filter across all items on the order, or load all relevant pricing into a local object on pageInit and reference it in memory rather than hitting the server per line.

4. Ignoring Date-Based Pricing

Pricing agreements expire. If your custom record doesn't include effective and expiration date fields — or if your script doesn't filter on them — you'll eventually serve stale pricing. Always filter on date range in the search, and consider adding a scheduled script that flags expired records for the pricing team to review.

Why This Approach vs. Alternatives

NetSuite offers several native pricing mechanisms, and it's worth understanding where they fall short for this use case:

The custom record approach wins on maintainability. Business users can manage pricing data directly. Developers maintain the lookup logic. And the two concerns stay cleanly separated.

Getting Started

If your team is dealing with pricing complexity that NetSuite's native tools can't handle cleanly, a custom record plus client script is often the most practical path. It keeps the data accessible to business users, the logic transparent to developers, and the maintenance burden low.

At Snapshot Design, we build these kinds of targeted NetSuite customizations for distributors and manufacturers every week. If you'd like to talk through whether this approach fits your pricing model, we're happy to have that conversation.

 

 

 

Why Your Payment Strategy Matters (and BlueSnap Can Help)

Why Your Payment Strategy Matters (and BlueSnap Can Help)

For businesses strategically focused on expansion and sustained growth, a seamless and efficient payment infrastructure is essential. It directly...

Read More
When Your Team Needs to Understand the Integration, Not Just Trust It

When Your Team Needs to Understand the Integration, Not Just Trust It

Integrating Configurable Products: What Your Team Needs to Know Before Launch Ecommerce integrations for simple products are straightforward. SKU...

Read More
Why Data Integration is Crucial for Shopify & NetSuite Success

1 min read

Why Data Integration is Crucial for Shopify & NetSuite Success

In today's rapidly evolving ecommerce landscape, where the pace of change is relentless and the competition is more intense than ever before,...

Read More