Revenue leakage is measurable, and if you run NetSuite the measurement doesn't need a new system — it needs three queries against transaction data you already have. Here they are, with what each one tells you and how to size the result.
Finding revenue leakage in NetSuite means querying transaction-level sales and cost data for variance — price dispersion, sub-margin lines and cost drift — because the problem is a distribution, and every summary report you own averages that distribution away.
There is a reason revenue leakage survives in businesses with perfectly competent finance functions and good reporting. Every standard report aggregates, and leakage is not an average problem. Your gross margin on a product line can sit exactly on target while a quarter of the transactions inside it are unprofitable, because the good ones are carrying the bad ones and the report only shows you the sum.
So the method is always the same: go to the transaction grain, look at the shape of the distribution rather than its centre, and quantify the tail. What follows is three queries that do that, in ascending order of how uncomfortable the answer usually is.
Two decisions determine whether this analysis is worth anything, and both need making before you run a single query.
First, decide what "price" means. The rate on the sales order line is not what the customer paid. Settlement discounts, retrospective rebates, freight absorbed rather than recharged and credit notes all sit between the order line and the cash. If those are material in your business, analysing order-line rate will systematically overstate your realised price — and the customers with the worst terms will look like your best ones. Start with order-line rate because it's available, but know which way the error points.
Second, decide what "cost" means. NetSuite's costestimate on a
transaction line, standard cost on the item record, and actual landed cost are three different numbers
and they routinely disagree by several points of margin. Pick one, state which, and use it
consistently. The queries below use the transaction line's own cost estimate because it is the version
captured at the moment of sale, which is the right basis for asking whether a specific decision was
sound.
If you get these two definitions wrong, the analysis will still produce confident-looking numbers. That's the danger. Write down your definition of price and cost before you start, and put it at the top of whatever you present.
The single most revealing view in most distribution and manufacturing businesses: for each product, how wide is the spread of prices actually paid, and how much volume sits at the bottom of it?
-- Price dispersion by item over the last 12 months. -- Compares each item's median realised price against its lower quartile. WITH lines AS ( SELECT tl.item AS item_id, BUILTIN.DF(tl.item) AS item_name, t.entity AS customer_id, ABS(tl.quantity) AS qty, tl.rate AS unit_price FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'SalesOrd' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.taxline = 'F' AND tl.rate > 0 AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -12) ) SELECT item_name, COUNT(*) AS line_count, COUNT(DISTINCT customer_id) AS customers, ROUND(MEDIAN(unit_price), 2) AS median_price, ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY unit_price), 2) AS lower_quartile, ROUND(MIN(unit_price), 2) AS min_price, ROUND(MAX(unit_price), 2) AS max_price, ROUND( (MEDIAN(unit_price) - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY unit_price)) * SUM(qty) * 0.25, 2) AS indicative_uplift FROM lines GROUP BY item_name HAVING COUNT(*) >= 20 AND COUNT(DISTINCT customer_id) >= 5 ORDER BY indicative_uplift DESC
The HAVING thresholds matter. An item sold four times to two customers has a meaningless
"spread". Twenty lines across five customers is a reasonable floor; raise it if you have the volume.
What indicative_uplift is doing. It estimates what you'd gain by moving
the bottom quartile of transactions up to the median, assuming a quarter of volume sits there. It is
deliberately conservative and it is an indication, not a forecast — it assumes no volume loss,
which is never true. Treat it as a way of ranking which items are worth investigating, not as a number
to put in a budget.
Reading it. Sort descending and look at the top 20 rows. For each, the question is whether the customers at the bottom of the range have a reason to be there — volume, contract, strategic account. Often three or four do and the rest are historical accident. That subset is your actionable list.
Dispersion tells you about price. This tells you about the decisions people are actually making, which is more actionable because it has a name attached to it.
-- Sales order lines whose realised gross margin falls below a target floor. SELECT t.tranid AS order_ref, t.trandate, BUILTIN.DF(t.entity) AS customer, BUILTIN.DF(tl.item) AS item, ABS(tl.quantity) AS qty, tl.rate AS unit_price, tl.costestimate AS line_cost, ROUND( ((tl.rate * ABS(tl.quantity)) - tl.costestimate) / NULLIF(tl.rate * ABS(tl.quantity), 0) * 100, 1) AS gm_pct, ROUND((tl.rate * ABS(tl.quantity)) - tl.costestimate, 2) AS gross_profit FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'SalesOrd' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.taxline = 'F' AND tl.rate > 0 AND tl.costestimate > 0 AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -12) AND ((tl.rate * ABS(tl.quantity)) - tl.costestimate) / NULLIF(tl.rate * ABS(tl.quantity), 0) < 0.20 -- your floor ORDER BY gross_profit ASC
Set 0.20 to your actual target floor. Sorting ascending by gross_profit
rather than by percentage puts the largest absolute losses first, which is where the money is —
a −40% margin on a £30 line matters less than a +4% margin on a £90,000 one.
The pattern to look for is repetition. One bad line is a bad day. The same customer appearing thirty times, or the same item appearing across twelve customers, is a pricing rule that's wrong. Group the output by customer and by item and count — the counts tell you far more than the individual rows.
This is also the query that surfaces the discount-authority problem described in the five types of revenue leakage: if your approval rules are expressed as percentage-off-list rather than as a margin floor, this query is where that shows up as a systematic bias rather than an occasional lapse.
The most insidious of the three, because nobody did anything at all. Your purchase cost rose, your sales price didn't, and the margin quietly compressed on every transaction since.
-- Items whose average purchase cost rose materially between the -- prior 6 months and the most recent 6 months. WITH purch AS ( SELECT tl.item AS item_id, CASE WHEN t.trandate >= ADD_MONTHS(CURRENT_DATE, -6) THEN 'recent' ELSE 'prior' END AS period, tl.rate AS cost_rate FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'VendBill' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.item IS NOT NULL AND tl.rate > 0 AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -12) ), sold AS ( SELECT tl.item AS item_id, AVG(tl.rate) AS avg_sell, SUM(ABS(tl.quantity)) AS qty_sold FROM transaction t JOIN transactionline tl ON tl.transaction = t.id WHERE t.type = 'SalesOrd' AND t.voided = 'F' AND tl.mainline = 'F' AND tl.taxline = 'F' AND tl.rate > 0 AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -6) GROUP BY tl.item ) SELECT BUILTIN.DF(p.item_id) AS item, ROUND(AVG(CASE WHEN p.period = 'prior' THEN p.cost_rate END), 2) AS cost_prior, ROUND(AVG(CASE WHEN p.period = 'recent' THEN p.cost_rate END), 2) AS cost_recent, ROUND(s.avg_sell, 2) AS avg_sell_price, s.qty_sold, ROUND((AVG(CASE WHEN p.period = 'recent' THEN p.cost_rate END) - AVG(CASE WHEN p.period = 'prior' THEN p.cost_rate END)) * s.qty_sold, 2) AS margin_lost FROM purch p JOIN sold s ON s.item_id = p.item_id GROUP BY p.item_id, s.avg_sell, s.qty_sold HAVING AVG(CASE WHEN p.period = 'recent' THEN p.cost_rate END) > AVG(CASE WHEN p.period = 'prior' THEN p.cost_rate END) * 1.03 ORDER BY margin_lost DESC
The 1.03 is a 3% materiality threshold on the cost increase. margin_lost
is the annualised cost of not having passed that increase through.
This query tends to produce the shortest list and the least argument. A supplier put their price up, it's in your own purchase records, and your sell price is unchanged. There's no interpretation required — only a decision about whether to pass it on.
The temptation at this point is to add up every indicative_uplift and present the total.
Don't. It will be a large number, it will not survive contact with a sceptical CFO, and losing that
argument once will close the topic for a year.
A number that holds up is built like this:
A verified £180,000 that you can walk through line by line is worth more than a modelled £900,000 that nobody believes. The first one gets you a mandate; the second one gets you a debate about methodology.
Mixing units of measure. If the same item is sold in eaches and in boxes of twelve, comparing unit price across those lines produces spectacular and entirely fictional dispersion. Check the units on your top findings before you present anything.
Ignoring the reason for the discount. The customer at the bottom of the price range might be there because they take full pallets, pay in seven days, and collect their own goods. Price dispersion is a question, not a verdict.
Using the wrong cost. If costestimate isn't populated reliably in your
account — and in many accounts it isn't — the margin query will produce confident nonsense. Spot-check
ten lines against what you know the item actually costs before trusting the other ten thousand.
Analysing orders rather than invoices. These queries use SalesOrd
because it captures the pricing decision at the point it was made. If you want realised revenue
including credits and returns, run the same logic against CustInvc and net off
CustCred. The two answers differ, and knowing which question you're asking matters.
Running these three queries once gives you a snapshot: here is roughly where the margin is going. That is genuinely useful and it's usually enough to justify doing something.
What it doesn't give you is control. Prices drift continuously, new customers get quoted every week, and suppliers put costs up on their own schedule. A snapshot ages badly — within a quarter it describes a business that no longer exists.
The step that changes outcomes is turning the check into a standing one: same logic, run weekly, ranked by value, with findings routed to whoever can act on them. That's a different exercise from an analysis, and it's worth being honest with yourself about which one you're committing to before you start.
Revaion Price runs these checks continuously across every SKU and customer in your NetSuite account, ranks findings by margin at stake, and shows the transactions behind each one.
See how Revaion Price works →Book a 30-minute discovery call. We'll walk through what these queries return in your NetSuite environment — and what a profit opportunity report would look like for your business.
Or email [email protected]
We use your email only to reply to this enquiry. See our privacy policy.