Published on December 11, 2021.
I was recently asked by a NetSuite client to add a new sales report to their custom management portal. This is a client that I first wrote about in October of 2021, when I added timesheets-related functionality to their custom employee portal.
The goal of the requested report is to provide managers with a quick overview of their sales pipeline. For each sales rep, they want to see the total of the rep's open sales orders, the total of their open estimates (where the probability of closing the sale is greater than or equal to 70%), and the sales rep's supervisor.
In this post, I'll share the SuiteQL query that I developed to fulfill this request, and the steps that I went through to create it. I'll also introduce the SQL WITH clause, and explain why and how I used it in this query.
But first, so that you can get a sense of how the query is used in the portal, here's a quick animation that shows the report.
The first query that I developed sums up the sales orders for each rep.
Here's the query.
SELECT Employee AS RepID, SUM( ForeignTotal ) AS ForeignTotal FROM Transaction WHERE ( ( Type = 'SalesOrd' ) AND ( BUILTIN.CF( Status ) = 'SalesOrd:G' ) ) GROUP BY Employee
The results look like this.
As you can see, I'm simply filtering on the Transaction table, and requesting Sales Orders ( Type = 'SalesOrd' ) that are open ( BUILTIN.CF( Status ) = 'SalesOrd:G' ). I'm using the BUILTIN.CF function on the table's Status column to filter for the open orders.
That's a good start. However, we also need the total of each rep's estimates.
To get the total estimates by rep, I made a few changes to the original query.
SELECT Employee AS RepID, SUM( ForeignTotal ) AS ForeignTotal FROM Transaction WHERE ( ( Type = 'Estimate' ) AND ( BUILTIN.CF( Status ) = 'Estimate:A' ) AND ( Probability >= .70 ) ) GROUP BY Employee
In this query, I'm specifying that I want transactions of type "Estimate," that the status of the transactions should be Open, and that I only want estimates where the probability of closing the sale is greater than or equal to 70%. Notice the subtle difference in the Status criteria. In this case, I'm using the BUILTIN.CF on the Status column, and the value that I'm requesting is "Estimate:A" (which is the "Open" status for estimates.
If you're not familiar with transaction statues, take a look at this query.
SELECT DISTINCT Type, Status, BUILTIN.CF( Status ) AS CFStatus, BUILTIN.DF( Status ) AS DFStatus FROM Transaction ORDER BY Type, Status
The results will look something like this.
The table shows the various statuses for each transaction type. The "status" column is essentially a code for the status. The "cfstatus" value is based on the BUILTIN.CF function. It's that value that we can use in the WHERE clause of SuiteQL queries run against the Transactions table. The "dfstatus" value is based on the BUILTIN.DF function, and it's the one that you'll most likely want to use when you want to display transaction statuses in reports.
At this point, we have two queries. One totals the sales orders by reps, and the other totals the estimates by reps. Let's combine those two queries.
SELECT Employee AS RepID, Type, SUM( ForeignTotal ) AS ForeignTotal FROM Transaction WHERE ( ( Type = 'SalesOrd' ) AND ( BUILTIN.CF( Status ) = 'SalesOrd:G' ) ) OR ( ( Type = 'Estimate' ) AND ( BUILTIN.CF( Status ) = 'Estimate:A' ) AND ( Probability >= .70 ) ) GROUP BY Employee, Type
That's a little better. However, we now have potentially two rows for each rep - one with the total sales orders, and the other with the total estimates. What we really want is a single row for each rep, with the two totals as individual columns in each rows. In other words, we sort of want the SQL equivalent of a "pivot."
In order to handle the pivot, I'm going to use a SQL clause that I haven't mentioned in previous posts. It's the WITH clause, and it helps to make complex queries a little easier to develop and manage.
Using the WITH clause, we can create one or more subqueries, and then refer to them in the SELECT statement. It's as if those subqueries become SQL views or virtual tables. And once we've defined them, we can do all sorts of interesting things with them - filter on them, join to them, and so on. These subqueries are often referred to as "common table expressions" (CTEs).
The WITH clause is most helpful when you need to refer to the same subquery multiple times. In this example, I'm using it to simplify the query.
Here's the query.
WITH SalesSummary AS ( SELECT Employee AS RepID, Type, SUM( ForeignTotal ) AS ForeignTotal FROM Transaction WHERE ( ( Type = 'SalesOrd' ) AND ( BUILTIN.CF( Status ) = 'SalesOrd:G' ) ) OR ( ( Type = 'Estimate' ) AND ( BUILTIN.CF( Status ) = 'Estimate:A' ) AND ( Probability >= .70 ) ) GROUP BY Employee, Type ) SELECT RepID, ( Employee.LastName || ', ' || Employee.FirstName ) AS RepName, ( Supervisor.LastName || ', ' || Supervisor.FirstName ) AS Supervisor, SUM ( CASE WHEN Type = 'SalesOrd' THEN ForeignTotal ELSE 0 END ) AS SalesTotal, SUM ( CASE WHEN Type = 'Estimate' THEN ForeignTotal ELSE 0 END ) AS EstimatesTotal FROM SalesSummary INNER JOIN Employee ON ( Employee.ID = SalesSummary.RepID ) INNER JOIN Employee AS Supervisor ON ( Supervisor.ID = Employee.Supervisor ) GROUP BY RepID, ( Employee.LastName || ', ' || Employee.FirstName ), ( Supervisor.LastName || ', ' || Supervisor.FirstName ) ORDER BY RepName
Notice that the query starts with the WITH clause. I'm using it to take the previous query and define it as a subquery named SalesSummary. At that point, I can refer to the subquery just as if it were a table. (Note that you can define multiple subqueries with the WITH clause.)
Once the subquery has been defined, the actual SELECT clause is pretty straightforward. I'm joining to the Employee table twice, once to get the employee record associated with the salesrep, and a second time to get the rep's supervisor. I'm concatenating the first and last names to get the full names of the reps and supervisors.
I'm using CASE statements to determine if a row is a total of sales orders or estimates. I'm grouping by the rep and their supervisor, and sorting the resulting rows by the employee's name.
This was a fun project to work on, and a nice addition to my client's portal. In the portal version of the report, users can click on a sales rep to "drill down" into the transactions that are being summarized.
I hope you find this query to be helpful. Examining it is a good way to learn about a number of SuiteQL-related topics, including transaction statuses and how you can use the BUILTIN.CF function to filter on them, the WITH clause, and how you can simulate pivoting.
Hello, I'm Tim Dietrich. I develop custom software for businesses that are running on NetSuite, including mobile apps, Web portals, Web APIs, and more.
I'm the developer of several popular NetSuite open source solutions, including the SuiteQL Query Tool, SuiteAPI, and more.
I founded SuiteStep, a NetSuite development studio, to provide custom software and AI solutions - and continue pushing the boundaries of what's possible on the NetSuite platform.
Copyright © 2025 Tim Dietrich.