Home SuiteQL: Budgets Machine
Post
Cancel

SuiteQL: Budgets Machine

Introduction

Budgets are a bit confusing to get using SuiteQL. They are a good example of how NetSuite uses utility tables to store data that doesn’t fit into the main table. I will abbreviate the Budget table to only show three months of data for readability purposes.

Example

Let’s look at an image of a budget in NetSuite:

ACCOUNTJAN 2022FEB 2022MAR 2022…Other MonthsTOTAL
10000 Cash10,000.0020,000.0030,000.00….1,000,000
40000 Revenue1,197,000.00865,000.00(Empty)….3,500,000

Note that ‘Mar 2022’ is an empty month, the other months are not. This will be important later.

There are two main hiccups when trying to get this data using SuiteQL.

First each budget record only contains one account. If you look at the internal id of the record in the UI, say with an id of 1112 and then query the Budget table, you will only get one account even if the UI shows multiple accounts.

When viewing a budget record, NetSuite shows you all the budgets that match the year, location, department, class. In this post, we will imagine getting all the budgets for a certain location which we will call “Location 1” and a certain year which we will call “FY 2022”. We will see the same information as shown in the UI.

The second issue with the data is that We can’t store the amount information in the main Budget table or even in a straightforward detail table.

This is because the columns are not fixed. For 2022, we have the months of 2022 but for 2023, we will have the months of 2023. This is different than the Transaction table where the columns on the TransactionLine table are fixed.

NetSuite uses the BudgetMachine table to store this information.

But first, let’s look at the Budget table.

Budgets

The Budgets table contains all the peripheral information about the budget.

Here are some columns in the Budgets table:

ColumnDescriptionType
yearYear of the Budget, but this points to a period, not and actual yearInteger (internal id)
categoryEither “Budget” or “Forecast”Integer (internal id)
locationLocation of the budgetInteger (internal id)
departmentDepartment of the budgetInteger (internal id)
classClass of the budgetInteger (internal id)
accountAccount of the budgetInteger (internal id)

BudgetMachine

This table represents the budget detail with just three columns:

ColumnDescriptionType
budgetReference to the parent budget (which contains the account)Integer (internal id)
amountAmount of the periodDecimal
periodPeriod of the budgetInteger (internal id)

As you can see, the field for the month isn’t actually a month, rather a period.

Getting the data

Now that we know the tables, let’s get the data. We won’t be getting the data the way it’s displayed in NetSuite, rather we will get multiple rows each one with a different account/period combination. We will write SuiteScript to transform the data into the format NetSuite displays it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
    BudgetsMachine.budget,
    BUILTIN.DF(Budgets.account) as account,
    BUILTIN.DF(Budgets.year) as year,
    BUILTIN.DF(Budgets.location) AS location,
    BudgetsMachine.amount,
    BUILTIN.DF(BudgetsMachine.period) AS period,
FROM
Budgets
 INNER JOIN BudgetsMachine ON Budgets.id = BudgetsMachine.budget
WHERE
    BUILTIN.DF(Budgets.location) = 'Location 1' AND
    BUILTIN.DF(Budgets.year) = 'FY 2022'

This returns all the budgets for Location 1 and FY 2022 and their amounts for the period:

budgetaccountyearlocationamountperiod
111210000 Cash2022Location 110000Jan 2022
111210000 Cash2022Location 120000Feb 2022
111210000 Cash2022Location 130000Mar 2022
111340000 Revenue2022Location 11197000Jan 2022
111340000 Revenue2022Location 1865000Feb 2022
…18 more rows

18 more rows plus the 5 rows above equals 23 rows. But 2 accounts times 12 months equals 24 rows. Where is the missing row?

This is because Mar 2022 is empty for the 40000 Revenue account. Because there is no “Mar 2022” field, only a potential row in the BudgetMachine table, we don’t get a row for this account/period combination.

But if we want to display it properly we’ll need to add the other months. In this case we can use the months from the 10000 Cash account which is fully poputated but it may not be the case for all budgets that they have an account that is fully populated.

We can use the AccountingPeriod table to get the months for the year. I won’t go into detail regarding the AccountingPeriod table but most of the following query is self explanatory.

1
2
3
4
5
6
7
8
SELECT
  MonthPeriod.periodname as months
FROM
  AccountingPeriod AS YearPeriod
  INNER JOIN AccountingPeriod AS QuarterPeriod ON QuarterPeriod.parent = YearPeriod.id
  INNER JOIN AccountingPeriod AS MonthPeriod ON MonthPeriod.parent = QuarterPeriod.id
WHERE
  YearPeriod.periodname = 'FY 2022'

This returns the months for FY 2022:

Months
Jan 2022
Feb 2022
Mar 2022
Apr 2022

We now have all the data we need to write some SuiteScript to display the data in the same format as NetSuite.

SuiteScript function

The goal is to write a function that takes a location and a year and returns the data in the same format as NetSuite.

This will return a javascript array as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
    {
        'account': '10000 Cash',
        'Jan 2022': 10000,
        'Feb 2022': 20000,
        'Mar 2022': 30000,
        '...Other Months':'...'
    },
    {
        'account': '40000 Revenue',
        'Jan 2022': 1197000,
        'Feb 2022': 865000,
        'Mar 2022': 0,
        '... Other Months':'...'
    }
]

First let’s define the functions that will get the data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function getBudgetData(location /* internal id */, year /* internal id of period */) {
    const theQuery = `
    SELECT
    BudgetsMachine.budget,
    BUILTIN.DF(Budgets.account) as account,
    BUILTIN.DF(Budgets.year) as year,
    BUILTIN.DF(Budgets.location) AS location,
    BudgetsMachine.amount,
    BUILTIN.DF(BudgetsMachine.period) AS period,
    FROM
    Budgets
        INNER JOIN BudgetsMachine ON Budgets.id = BudgetsMachine.budget
    WHERE
    Budgets.location = ${location} AND
    Budgets.year = ${year}
    `
    return query.runSuiteQL(theQuery).asMappedResults()
}

function getBudgetMonths(year /* internal id of period */) {
    const theQuery = `
    SELECT
    MonthPeriod.periodname as months
    FROM
    AccountingPeriod AS YearPeriod
        INNER JOIN AccountingPeriod AS QuarterPeriod ON QuarterPeriod.parent = YearPeriod.id
        INNER JOIN AccountingPeriod AS MonthPeriod ON MonthPeriod.parent = QuarterPeriod.id
    WHERE
    YearPeriod.id = ${year}
    `
    return query.runSuiteQL(theQuery).asMappedResults()
}

And now the function that will return the data in the format we want.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getBudgetDisplayData(year, location){
    const budgetData = getBudgetData(location, year)
    const budgetMonths = getBudgetMonths(year)
    const uniqueAccounts = [...new Set(budgetData.map(item => item.account))] /* [10000 Cash, 40000 Revenue]*/
    const data = uniqueAccounts.map(accountname => {
        return {
            account: accountname,
            ...budgetMonths.reduce((acc, month) => {
                const amount = budgetData.find(line => line.account === accountname && line.period === month.months)?.amount || 0
                return {
                    ...acc,
                    [month.months]: amount
                }
            }, {})
        }

    })

    return data
}

Here is a link to a SuiteScript custom module that implements the above function

Conclusion

A lot of reverse engineering was done to get to this point and I’m sure there are better ways to do this but this is what I came up with. SuiteQL is not fully documented and a lot of writing queries is trial and error.

I nearly had to used saved searches to get the data but luckily I was able to avoid it.

Stay tuned for the next post where I’ll start discussing Bills of Materials and how to get the data using SuiteQL.

This post is licensed under CC BY 4.0 by the author.

SuiteQL: Units Of Measure

SuiteQL: Boms, Assemblies, and Revisions