NetSuite: Inventory Balance History Suitelet

Published on November 5, 2020.

A few weeks ago, I shared a NetSuite SuiteQL query that I had written that is being used by one of my clients to get an item's inventory balance history.

I recently used a modified version of that query in a Suitelet that can be used to lookup the balance history for an inventory item. Transactions that had an impact on the item's inventory balance are displayed in reverse chronological order, up to and including the specified date. Here's an example.

The Suitelet

I'm making the Suitelet available free of charge, and the SuiteScript is included below. The script is also available as a download.

/**
* @NApiVersion 2.x
* @NScriptType Suitelet
* @NModuleScope Public
*/

/* 

------------------------------------------------------------------------------------------
Script Information
------------------------------------------------------------------------------------------

Name:
Inventory Balance Utility

ID:
_inventory_history

Description
A utility for looking up the ending balance of an item for a specified day.


------------------------------------------------------------------------------------------
Developer(s)
------------------------------------------------------------------------------------------

Tim Dietrich
• timdietrich@me.com
• https://timdietrich.me


------------------------------------------------------------------------------------------
History
------------------------------------------------------------------------------------------

20201105 - Tim Dietrich
• Initial version.

*/


var 
	log,
	query,
	serverWidget,
	historyRows = 100;


define( [ 'N/log', 'N/query', 'N/ui/serverWidget' ], main );


function main( logModule, queryModule, serverWidgetModule ) {

	// Set module references.
	log = logModule;
	query= queryModule;
	serverWidget = serverWidgetModule;				
	
    return {
    
    	onRequest: function( context ) {
    	
			// Create a form.
			var form = serverWidget.createForm(
				{
					title: 'Inventory Balance History',
					hideNavBar: false
				}
			);		
			
			// Add a submit button.
			form.addSubmitButton( { label: 'Get History' } );
			
			// Add an "Item ID" field.
			var itemField = form.addField(
				{
					id: 'custpage_field_itemid',
					type: serverWidget.FieldType.TEXT,
					label: 'Item Name / Number'
				}								
			);			
			
			// Add a "Date" field.
			var dateField = form.addField(
				{
					id: 'custpage_field_date',
					type: serverWidget.FieldType.DATE,
					label: 'Show Ending Balance For Date'
				}								
			);			
			
			// Make both fields mandatory.
			itemField.isMandatory = true;	
			dateField.isMandatory = true;		
											
			// If the form has been submitted...
			if ( context.request.method == 'POST' ) {	
			
				// Set defaults for the item and date field values.
				itemField.defaultValue = context.request.parameters.custpage_field_itemid;
				dateField.defaultValue = context.request.parameters.custpage_field_date;			

				// Process the form.
				formProcess( context, form );
			
			}
			
			// Display the form.
			context.response.writePage( form );	
			
        }
        
    }

}


function formProcess( context, form ) {	

	var theQuery = '';
	theQuery += 'SELECT ';
	theQuery += 'TranDate AS Date, ';
	theQuery += 'Type, ';
	theQuery += '\'<a href="/app/accounting/transactions/transaction.nl?id=\' || TransactionID || \'" target="_new">\' || TranID || \'</a>\' AS TransID, ';	
	theQuery += 'AltName, ';
	theQuery += '( Qty_Balance - Quantity ) AS Beginning_Balance, ';
	theQuery += 'Quantity, ';
	theQuery += 'Qty_Balance AS Ending_Balance ';
	theQuery += 'FROM ( ';
	theQuery += 'SELECT * FROM ( ';
	theQuery += 'SELECT ';
	theQuery += 'TransactionLine.Transaction AS TransactionID, ';
	theQuery += 'Transaction.TranDate, ';
	theQuery += 'Transaction.Type, ';
	theQuery += 'Transaction.TranID, ';
	theQuery += 'TransactionLine.ID AS TransLineID, ';
	theQuery += 'TransactionLine.Rate, ';
	theQuery += 'TransactionLine.NetAmount, ';
	theQuery += 'TransactionLine.Quantity, ';
	theQuery += 'Entity.AltName, ';
	theQuery += 'SUM( TransactionLine.Quantity ) ';
	theQuery += 'OVER ( ';
	theQuery += 'ORDER BY ';
	theQuery += 'Transaction.TranDate, ';
	theQuery += 'Transaction.ID, ';
	theQuery += 'TransactionLine.ID ';
	theQuery += 'RANGE UNBOUNDED PRECEDING ';
	theQuery += ') Qty_Balance ';
	theQuery += 'FROM ';
	theQuery += 'Item ';	
	theQuery += 'INNER JOIN TransactionLine ON ';
	theQuery += '( TransactionLine.Item = Item.ID ) ';
	theQuery += 'INNER JOIN Transaction ON ';
	theQuery += '( Transaction.ID = TransactionLine.Transaction ) ';
	theQuery += 'LEFT OUTER JOIN Entity ON ';
	theQuery += '( Entity.ID = Transaction.Entity ) ';
	theQuery += 'WHERE ';
	theQuery += '( Item.ItemID = \'' + context.request.parameters.custpage_field_itemid + '\' ) ';
	theQuery += 'AND ( TransactionLine.IsInventoryAffecting = \'T\' ) ';
	theQuery += 'AND ( Transaction.Voided = \'F\' ) ';
	theQuery += 'ORDER BY ';
	theQuery += 'Transaction.TranDate, ';
	theQuery += 'Transaction.ID, ';
	theQuery += 'TransactionLine.ID ';
	theQuery += ') ';
	theQuery += 'WHERE ';	
	theQuery += '( TranDate <= TO_DATE( \'' + context.request.parameters.custpage_field_date + '\', \'MM/DD/YYYY\' ) ) ';		
	theQuery += 'ORDER BY ';
	theQuery += 'TranDate DESC, TransactionID DESC, TransLineID DESC ';
	theQuery += ') ';
	theQuery += 'WHERE ( ROWNUM <= ' + historyRows + ' ) ';	
	
	try {

		// Run the query.
		var queryResults = query.runSuiteQL(
			{
				query: theQuery
			}
		); 				
	
		// Get the mapped results.		
		var records = queryResults.asMappedResults();				
	
		// If records were returned...
		if ( records.length > 0 ) {	

			// Create a sublist for the results.
			var resultsSublist = form.addSublist(
				{ 
					id : 'results_sublist', 
					label : 'Balance History', 
					type : serverWidget.SublistType.LIST 
				}
			);

			// Get the column names.
			var columnNames = Object.keys( records[0] );

			// Loop over the column names...
			for ( i = 0; i < columnNames.length; i++ ) {

				// Add the column to the sublist as a field.
				resultsSublist.addField(
					{ 
						id: 'custpage_results_sublist_col_' + i,
						type: serverWidget.FieldType.TEXT,
						label: columnNames[i]
					}
				);

			}

			// Add the records to the sublist...
			for ( r = 0; r < records.length; r++ ) {

				// Get the record.
				var record = records[r];

				// Loop over the columns...
				for ( c = 0; c < columnNames.length; c++ ) {

					// Get the column name.
					var column = columnNames[c];

					// Get the column value.
					var value = record[column];
					
					// If the column has a value...
					if ( value != null ) {
					
						// Get the value as a string.
						value = value.toString();
						
						// If the value is too long to be displayed in the sublist...
						if ( value.length > 300 ) {
						
							// Truncate the value.
							value = value.substring( 0, 297 ) + '...';			
							
						}

						// Add the column value.		
						resultsSublist.setSublistValue(
							{
								id : 'custpage_results_sublist_col_' + c,
								line : r,
								value : value
							}
						);        

					}	
					
				}

			}
			
			// Add an inline HTML field so that JavaScript can be injected.
			var jsField = form.addField(
				{
					id: 'custpage_field_js',
					type: serverWidget.FieldType.INLINEHTML,
					label: 'Javascript'
				}								
			);		
			
			// Add Javascript to make the first row bold, and add a tooltip.
			jsField.defaultValue = '<script>
';
			jsField.defaultValue += 'document.addEventListener(\'DOMContentLoaded\', function() {';
			jsField.defaultValue += 'document.getElementById("results_sublistrow0").style["font-weight"]="bold";
';
			jsField.defaultValue += 'document.getElementById("results_sublistrow0").title="This is the balance as of ' + context.request.parameters.custpage_field_date + '.";
';
			jsField.defaultValue += '}, false);';
			jsField.defaultValue += '</script>';				

		} else {
		
			// Add an "Error" field.
			var errorField = form.addField(
				{
					id: 'custpage_field_error',
					type: serverWidget.FieldType.TEXT,
					label: 'Error'
				}								
			);		
			
			errorField.defaultValue = 'No history found for: ' + context.request.parameters.custpage_field_itemid;	
			
			// Add an inline HTML field so that JavaScript can be injected.
			var jsField = form.addField(
				{
					id: 'custpage_field_js',
					type: serverWidget.FieldType.INLINEHTML,
					label: 'Javascript'
				}								
			);		
			
			// Add Javascript to make the error field red.
			jsField.defaultValue = '<script>
';
			jsField.defaultValue += 'document.addEventListener(\'DOMContentLoaded\', function() {';
			jsField.defaultValue += 'document.getElementById("custpage_field_error").style.background="red";
';
			jsField.defaultValue += 'document.getElementById("custpage_field_error").style.color="white";
';
			jsField.defaultValue += '}, false);';
			jsField.defaultValue += '</script>';					
		
		}

	} catch( e ) {	
	
		var errorField = form.addField(
			{
				id: 'custpage_field_error',
				type: serverWidget.FieldType.LONGTEXT,
				label: 'Error'
			}								
		);		
	
		errorField.defaultValue = e.message;			
		
	}

}

About the Suitelet

You can control the number of rows being returned by adjusting the "historyRows" value in the script, and you can specify that 1 to 5000 rows should be displayed. If you'd like, you can modify the script so that the historyRows value is a script parameter, or add a field so that users can set the value on the form.

The values in the TransID column are links that display the selected transaction in a new tab or window. This can be helpful when you are researching an item's balance history.

The first row of the sublist is styled a little differently than the other rows. Its text is bold, and the row includes a tooltip. I'm using Javascript to manipulate the row.

I hope you find the Suitelet to be helpful. If you have any questions, please feel free to contact me.

About Me

Hello, I’m Tim Dietrich. I design and build custom software for businesses running on NetSuite — from mobile apps and Web portals to Web APIs and integrations.

I’ve created several widely used open-source solutions for the NetSuite community, including the SuiteQL Query Tool and SuiteAPI, which help developers and businesses get more out of their systems.

I’m also the founder of SuiteStep, a NetSuite development studio focused on pushing the boundaries of what’s possible on the platform. Through SuiteStep, I deliver custom software and AI-driven solutions that make NetSuite more powerful, accessible, and future-ready.

Copyright © 2025 Tim Dietrich.