suitescript-tests-suitelet-pdf
title: SuiteScript Tests - Suitelet & PDF description: QuickMart Suitelet and PDF-related SuiteScript test cases
SuiteScript Tests - Suitelet & PDF
This page contains QuickMart Suitelet, Advanced PDF, and Suitelet-based reporting test cases.
Test Case #23: Suitelet - Delivery Manifest Generator
Objective: Custom UI to generate delivery manifest PDF.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*
* QuickMart - Delivery Manifest Generator
*/
define(['N/ui/serverWidget', 'N/search', 'N/render', 'N/record', 'N/file'],
function(serverWidget, search, render, record, file) {
function onRequest(context) {
if (context.request.method === 'GET') {
showForm(context);
} else {
generateManifest(context);
}
}
function showForm(context) {
var form = serverWidget.createForm({ title: 'Delivery Manifest Generator' });
form.addField({
id: 'custpage_date',
type: serverWidget.FieldType.DATE,
label: 'Delivery Date'
}).defaultValue = new Date();
form.addField({
id: 'custpage_location',
type: serverWidget.FieldType.SELECT,
label: 'Warehouse',
source: 'location'
});
form.addSubmitButton({ label: 'Generate Manifest' });
context.response.writePage(form);
}
function generateManifest(context) {
var deliveryDate = context.request.parameters.custpage_date;
var locationId = context.request.parameters.custpage_location;
// Find orders to deliver
var orders = findOrdersForDelivery(deliveryDate, locationId);
// Generate PDF using render module
var xml = buildManifestXml(orders, deliveryDate);
var pdf = render.xmlToPdf({ xmlString: xml });
context.response.writeFile({
file: pdf,
isInline: true
});
}
function findOrdersForDelivery(date, locationId) {
var orders = [];
search.create({
type: search.Type.SALES_ORDER,
filters: [
['shipdate', 'on', date],
'AND',
['status', 'anyof', 'SalesOrd:B'], // Pending Fulfillment
'AND',
['mainline', 'is', 'T']
],
columns: ['tranid', 'entity', 'shipaddress', 'custbody_priority_order']
}).run().each(function(result) {
orders.push({
orderNumber: result.getValue('tranid'),
customer: result.getText('entity'),
address: result.getValue('shipaddress'),
priority: result.getValue('custbody_priority_order')
});
return true;
});
return orders;
}
function buildManifestXml(orders, date) {
var xml = '<?xml version="1.0"?>\n';
xml += '<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">\n';
xml += '<pdf>\n<body>\n';
xml += '<h1>Delivery Manifest - ' + date + '</h1>\n';
xml += '<table>\n';
xml += '<tr><th>Order</th><th>Customer</th><th>Address</th><th>Priority</th></tr>\n';
orders.forEach(function(order) {
xml += '<tr>';
xml += '<td>' + order.orderNumber + '</td>';
xml += '<td>' + order.customer + '</td>';
xml += '<td>' + (order.address || '-') + '</td>';
xml += '<td>' + (order.priority ? 'RUSH' : 'Normal') + '</td>';
xml += '</tr>\n';
});
xml += '</table>\n';
xml += '</body>\n</pdf>';
return xml;
}
return { onRequest: onRequest };
});
Test Case #24: Suitelet - Purchase Request Form
Objective: Custom entry form for Purchase Requests.
See SuiteBuilder Test #3-4 for the record structure.
Test Case #25: Advanced PDF - Custom Invoice Template
Objective: Create branded invoice with loyalty info.
Template Configuration
Navigation: Customization > Forms > Advanced PDF/HTML Templates > New
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 10pt; }
.header { background-color: #1a365d; color: white; padding: 10px; }
.customer-info { margin: 20px 0; }
.loyalty-badge {
background: #f6e05e;
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
}
table.items { width: 100%; border-collapse: collapse; margin: 20px 0; }
table.items th { background: #e2e8f0; padding: 8px; text-align: left; }
table.items td { padding: 8px; border-bottom: 1px solid #e2e8f0; }
.total { text-align: right; font-size: 14pt; font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>QuickMart Supplies</h1>
<p>Your Office Supplies Partner</p>
</div>
<h2>INVOICE ${record.tranid}</h2>
<div class="customer-info">
<strong>Bill To:</strong><br/>
${record.entity}<br/>
${record.billaddress}
</div>
<#-- Customer Loyalty Badge -->
<#if record.entity.custentity_loyalty_record?has_content>
<div class="loyalty-badge">
${record.entity.custentity_loyalty_record.custrecord_cl_tier} Member
| ${record.entity.custentity_loyalty_record.custrecord_cl_points} Points
</div>
</#if>
<table class="items">
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
<#list record.item as item>
<tr>
<td>${item.item}</td>
<td>${item.description!''}</td>
<td>${item.quantity}</td>
<td>${item.rate}</td>
<td>${item.amount}</td>
</tr>
</#list>
</table>
<div class="total">
Subtotal: ${record.subtotal}<br/>
Tax: ${record.taxtotal}<br/>
<strong>Total: ${record.total}</strong>
</div>
<#if record.discountrate?has_content && record.discountrate != 0>
<p><em>Loyalty Discount Applied: ${record.discountrate}%</em></p>
</#if>
<hr/>
<p style="text-align: center; font-size: 8pt;">
Thank you for your business! | www.quickmart.com
</p>
</body>
</pdf>
Link to Form
Customization > Forms > Transaction Forms > Invoice - Online > Edit
> Printing Tab
> Advanced PDF Template: [Custom Invoice - QuickMart]
Test Case #26: Suitelet PDF - Daily Sales Report
Objective: Generate sales summary PDF from Suitelet.
See Test Case #23 for similar pattern using N/render.xmlToPdf().
Test Case #30: Advanced PDF - Packing Slip with Barcodes
Objective: Generate warehouse-friendly packing slips with bin locations and scannable barcodes.
Scenario
David (Warehouse) needs a readable pick list with barcodes so he can scan items while picking.
Template Configuration
Navigation: Customization > Forms > Advanced PDF/HTML Templates > New
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 9pt; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #ddd; padding: 6px; }
th { background: #f0f0f0; text-align: left; }
</style>
</head>
<body>
<h2>Packing Slip - ${record.tranid}</h2>
<table>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Bin</th>
<th>Barcode</th>
</tr>
<#list record.item as item>
<tr>
<td>${item.item}</td>
<td>${item.quantity}</td>
<td>${item.location!''}</td>
<td><barcode codetype="code128" showtext="true">${item.itemid}</barcode></td>
</tr>
</#list>
</table>
</body>
</pdf>
Expected Result
- Packing slip prints from Item Fulfillment or Sales Order
- Barcode renders for each line item
- Bin location is visible for picking
Test Case #32: Advanced PDF - Connect Template to Custom Form (Step-by-Step)
Objective: Verify a QuickMart invoice custom form always prints with the assigned Advanced PDF template.
Scenario
Lisa (Admin) needs a reliable setup where users print branded QuickMart invoices from one specific custom form.
Prerequisites
- Invoice transaction type is enabled.
- You can create/edit transaction forms and Advanced PDF templates.
- Test invoice exists (example:
INV-TEST-001) with at least 2 line items.
Step-by-Step Guide
- Create the Advanced PDF template.
Customization > Forms > Advanced PDF/HTML Templates > New
Name: QuickMart Invoice - Retail PDF
Type: Transaction
Transaction: Invoice
- Paste a starter template and save.
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 10pt; }
.brand { font-size: 18pt; color: #1a365d; font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border-bottom: 1px solid #d9d9d9; padding: 6px; }
th { background: #f2f2f2; text-align: left; }
.right { text-align: right; }
</style>
</head>
<body>
<div class="brand">QuickMart Supplies</div>
<p>Invoice: ${record.tranid}</p>
<p>Customer: ${record.entity}</p>
<table>
<tr>
<th>Item</th>
<th>Qty</th>
<th class="right">Rate</th>
<th class="right">Amount</th>
</tr>
<#list record.item as item>
<tr>
<td>${item.item}</td>
<td>${item.quantity}</td>
<td class="right">${item.rate}</td>
<td class="right">${item.amount}</td>
</tr>
</#list>
</table>
<p class="right"><strong>Total: ${record.total}</strong></p>
</body>
</pdf>
- Build or clone the invoice custom form.
Customization > Forms > Transaction Forms > Invoice > Customize
Form Name: QuickMart Invoice - Retail
- Connect template to the custom form (critical step).
Open the custom form > Printing tab
Advanced PDF/HTML Template: [QuickMart Invoice - Retail PDF]
Show Print Button: checked
Save form
- Assign form usage.
Custom form > Roles tab
Add role(s): Sales Rep, AR Clerk (or your test role)
Optional: Set as Preferred for role if this should be default
- Execute the test.
Transactions > Sales > Create Invoices (or open existing test invoice)
Select Custom Form: QuickMart Invoice - Retail
Open invoice in View mode
Click Print
-
Validate expected behavior.
-
PDF header shows QuickMart branding from your custom template.
-
Printed layout matches XML (not standard NetSuite invoice).
-
Line item count and totals match the invoice record.
-
Reopen the same invoice with a different form and confirm output changes accordingly.
Pass/Fail Criteria
- Pass: printing from
QuickMart Invoice - Retailalways usesQuickMart Invoice - Retail PDF. - Fail: output falls back to default template or uses wrong layout.
Test Case #33: User Event + Suitelet - Button to Render XML PDF
Objective: Add a button from beforeLoad (User Event), call a Suitelet, fetch record data, and pass that data into an XML template for PDF rendering.
Scenario
David (Warehouse) opens a Sales Order and clicks Print. The button launches a Suitelet that renders a pick ticket PDF from XML using live Sales Order data.
Architecture
Sales Order (View)
-> User Event beforeLoad resolves Suitelet URL
-> User Event appends recordtype, id, employee to URL
-> User Event adds Print button with window.open(url)
-> Suitelet loads record + line data from URL params
-> Suitelet passes data to XML template
-> N/render generates PDF
Script 1: User Event (beforeLoad) adds button
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/runtime', 'N/url'], function(runtime, url) {
function beforeLoad(scriptContext) {
if (scriptContext.type !== 'view' && scriptContext.type !== scriptContext.UserEventType.VIEW) return;
var currentuserid = runtime.getCurrentUser().id;
var rec = scriptContext.newRecord;
var internalId = rec.id;
var recType = rec.type;
var suiteletUrl = url.resolveScript({
scriptId: 'customscript_oas_if_print_sl',
deploymentId: 'customdeploy_oas_if_print_sl',
returnExternalUrl: false
});
suiteletUrl += '&recordtype=' + encodeURIComponent(recType);
suiteletUrl += '&id=' + encodeURIComponent(internalId);
suiteletUrl += '&employee=' + encodeURIComponent(currentuserid);
log.debug('url', suiteletUrl);
scriptContext.form.addButton({
id: 'custpage_PrintBtn',
label: 'Print',
functionName: "window.open('" + suiteletUrl + "');"
});
}
return { beforeLoad: beforeLoad };
});
Script 2: Suitelet loads data and renders XML template to PDF
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/render', 'N/record', 'N/file', 'N/error'], function(render, record, file, error) {
function onRequest(context) {
var recordId = context.request.parameters.id || context.request.parameters.soid;
var recordType = context.request.parameters.recordtype || record.Type.SALES_ORDER;
var employeeId = context.request.parameters.employee || '';
if (!recordId) {
throw error.create({
name: 'MISSING_RECORD_ID',
message: 'Record ID is required.'
});
}
var trx = record.load({
type: recordType,
id: recordId
});
var lines = [];
var count = trx.getLineCount({ sublistId: 'item' });
for (var i = 0; i < count; i++) {
lines.push({
item: trx.getSublistText({ sublistId: 'item', fieldId: 'item', line: i }) || '',
quantity: trx.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }) || 0,
location: trx.getSublistText({ sublistId: 'item', fieldId: 'location', line: i }) || '',
amount: trx.getSublistValue({ sublistId: 'item', fieldId: 'amount', line: i }) || 0
});
}
var templateFile = file.load({
id: 'SuiteScripts/QuickMart/templates/qm_pick_ticket.xml'
});
var renderer = render.create();
renderer.templateContent = templateFile.getContents();
renderer.addRecord({
templateName: 'record',
record: trx
});
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: 'data',
data: {
printedOn: new Date().toISOString().slice(0, 10),
printedBy: employeeId,
lines: lines
}
});
var pdf = renderer.renderAsPdf();
pdf.name = 'QM_PickTicket_' + trx.getValue({ fieldId: 'tranid' }) + '.pdf';
context.response.writeFile({
file: pdf,
isInline: true
});
}
return { onRequest: onRequest };
});
XML Template (SuiteScripts/QuickMart/templates/qm_pick_ticket.xml)
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 9pt; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #ddd; padding: 6px; }
th { background: #f0f0f0; text-align: left; }
.right { text-align: right; }
</style>
</head>
<body>
<h2>QuickMart Pick Ticket</h2>
<p>Sales Order: ${record.tranid}</p>
<p>Customer: ${record.entity}</p>
<p>Printed On: ${data.printedOn}</p>
<table>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Location</th>
<th class="right">Amount</th>
</tr>
<#list data.lines as line>
<tr>
<td>${line.item}</td>
<td>${line.quantity}</td>
<td>${line.location}</td>
<td class="right">${line.amount}</td>
</tr>
</#list>
</table>
</body>
</pdf>
Deployment and Wiring
- Deploy the Suitelet (
customscript_oas_if_print_sl) in Released status. - Deploy the User Event on Sales Order.
- Upload XML template to
SuiteScripts/QuickMart/templates/qm_pick_ticket.xml. - Confirm test role has permission to Sales Orders and script deployments.
Test Steps
- Open an existing Sales Order in View mode.
- Confirm button Print appears.
- Click button and confirm Suitelet opens in a new tab.
- Confirm PDF renders with Sales Order header and all line items.
- Confirm values in PDF match the source Sales Order.
- Optional negative test: open Suitelet URL without
idand confirm validation error.
Expected Result
- Button is visible only in View mode.
- Suitelet receives
recordtype,id, andemployeeand loads data correctly. - XML template receives
recordanddata.linesand renders without errors. - PDF opens inline with filename format
QM_PickTicket_<tranid>.pdf.
Test Case #34: Suitelet UI Report - Date Range + Download Excel
Objective: Build a Suitelet report page where user selects Date From and Date To, views the report, and clicks Download Excel to export results.
Scenario
Sarah (Accounting) needs a date-filtered transaction summary and a one-click export that can be opened in Excel.
Architecture
Suitelet GET
-> Show filter UI (Date From, Date To)
-> Show buttons (Show Data, Download Excel)
Suitelet POST
-> Validate date range
-> Query report rows
-> If action=preview: render rows in sublist
-> If action=download: write CSV file (Excel-compatible) to response
Script 1: Suitelet (customscript_qm_sales_report_sl)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/search', 'N/file'], function(serverWidget, search, file) {
function onRequest(context) {
if (context.request.method === 'GET') {
return context.response.writePage(buildForm(context, []));
}
var params = context.request.parameters;
var dateFrom = params.custpage_date_from;
var dateTo = params.custpage_date_to;
var action = params.custpage_action || 'preview';
if (!dateFrom || !dateTo) {
throw new Error('Date From and Date To are required.');
}
var rows = getSalesRows(dateFrom, dateTo);
if (action === 'download') {
var csv = buildCsv(rows);
var reportFile = file.create({
name: 'QuickMart_Sales_Report_' + dateFrom + '_to_' + dateTo + '.csv',
fileType: file.Type.CSV,
contents: csv
});
context.response.writeFile({
file: reportFile,
isInline: false
});
return;
}
context.response.writePage(buildForm(context, rows));
}
function buildForm(context, rows) {
var form = serverWidget.createForm({ title: 'QuickMart Sales Report' });
form.clientScriptModulePath = 'SuiteScripts/QuickMart/qm_sales_report_cs.js';
var dateFromField = form.addField({
id: 'custpage_date_from',
type: serverWidget.FieldType.DATE,
label: 'Date From'
});
dateFromField.isMandatory = true;
dateFromField.defaultValue = context.request.parameters.custpage_date_from || '';
var dateToField = form.addField({
id: 'custpage_date_to',
type: serverWidget.FieldType.DATE,
label: 'Date To'
});
dateToField.isMandatory = true;
dateToField.defaultValue = context.request.parameters.custpage_date_to || '';
var actionField = form.addField({
id: 'custpage_action',
type: serverWidget.FieldType.TEXT,
label: 'Action'
});
actionField.updateDisplayType({
displayType: serverWidget.FieldDisplayType.HIDDEN
});
actionField.defaultValue = 'preview';
form.addButton({
id: 'custpage_btn_show',
label: 'Show Data',
functionName: 'showReport'
});
form.addButton({
id: 'custpage_btn_download',
label: 'Download Excel',
functionName: 'downloadExcel'
});
var sublist = form.addSublist({
id: 'custpage_report',
type: serverWidget.SublistType.LIST,
label: 'Report Results'
});
sublist.addField({ id: 'custpage_col_tranid', type: serverWidget.FieldType.TEXT, label: 'Document No' });
sublist.addField({ id: 'custpage_col_date', type: serverWidget.FieldType.TEXT, label: 'Date' });
sublist.addField({ id: 'custpage_col_customer', type: serverWidget.FieldType.TEXT, label: 'Customer' });
sublist.addField({ id: 'custpage_col_total', type: serverWidget.FieldType.CURRENCY, label: 'Total' });
for (var i = 0; i < rows.length; i++) {
sublist.setSublistValue({ id: 'custpage_col_tranid', line: i, value: rows[i].tranid || '-' });
sublist.setSublistValue({ id: 'custpage_col_date', line: i, value: rows[i].trandate || '-' });
sublist.setSublistValue({ id: 'custpage_col_customer', line: i, value: rows[i].customer || '-' });
sublist.setSublistValue({ id: 'custpage_col_total', line: i, value: String(rows[i].total || 0) });
}
return form;
}
function getSalesRows(dateFrom, dateTo) {
var rows = [];
search.create({
type: search.Type.TRANSACTION,
filters: [
['type', 'anyof', 'CustInvc'], 'AND',
['mainline', 'is', 'T'], 'AND',
['trandate', 'within', dateFrom, dateTo]
],
columns: ['tranid', 'trandate', 'entity', 'total']
}).run().each(function(r) {
rows.push({
tranid: r.getValue('tranid'),
trandate: r.getValue('trandate'),
customer: r.getText('entity'),
total: parseFloat(r.getValue('total')) || 0
});
return true;
});
return rows;
}
function buildCsv(rows) {
var out = ['Document No,Date,Customer,Total'];
rows.forEach(function(row) {
out.push([
csvSafe(row.tranid),
csvSafe(row.trandate),
csvSafe(row.customer),
row.total
].join(','));
});
return out.join('\n');
}
function csvSafe(value) {
var text = String(value || '');
return '"' + text.replace(/"/g, '""') + '"';
}
return { onRequest: onRequest };
});
Script 2: Client Script for report actions
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/currentRecord'], function(currentRecord) {
function submitWithAction(action) {
var rec = currentRecord.get();
rec.setValue({
fieldId: 'custpage_action',
value: action
});
document.forms[0].submit();
}
function showReport() {
submitWithAction('preview');
}
function downloadExcel() {
submitWithAction('download');
}
return {
showReport: showReport,
downloadExcel: downloadExcel
};
});
Test Steps
- Deploy Suitelet and Client Script in Released status.
- Open Suitelet URL.
- Fill
Date FromandDate To. - Click Show Data and verify rows appear in sublist.
- Click Download Excel and verify file is downloaded.
- Open downloaded file in Excel and verify data/columns match UI results.
Expected Result
- Date fields are mandatory and filter results correctly.
- Download action returns Excel-compatible file with same filtered dataset.
- No script error when result set is empty (file still downloads with header row).
Test Case #35: Suitelet Tabulator Report (Like issu_laporan_mutasi_barang_sl.js)
Objective: Implement a Tabulator-based Suitelet report UI with date filters and client-side export, following the same approach as issu_laporan_mutasi_barang_sl.js.
Scenario
David (Warehouse) needs a rich report grid (sorting, pagination, filtering) and fast Excel export from the browser.
Pattern from Reference
Based on:
C:\Users\Dell\Documents\Office\github\issu\src\FileCabinet\SuiteScripts\Report Laporan Mutasi Barang\issu_laporan_mutasi_barang_sl.js
Key approach:
- Build filter form in Suitelet
- Use INLINEHTML to inject Tabulator CSS/JS
- Generate server-side data, pass JSON to browser
- Use Tabulator + SheetJS for XLSX download button
Suitelet Example
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/search'], function(serverWidget, search) {
function onRequest(context) {
var form = serverWidget.createForm({ title: 'QuickMart Stock Movement (Tabulator)' });
var dateFrom = form.addField({
id: 'custpage_date_from',
type: serverWidget.FieldType.DATE,
label: 'Date From'
});
dateFrom.isMandatory = true;
dateFrom.defaultValue = context.request.parameters.custpage_date_from || '';
var dateTo = form.addField({
id: 'custpage_date_to',
type: serverWidget.FieldType.DATE,
label: 'Date To'
});
dateTo.isMandatory = true;
dateTo.defaultValue = context.request.parameters.custpage_date_to || '';
form.addSubmitButton({ label: 'Show Data' });
var rows = [];
if (context.request.method === 'POST') {
rows = getMovementRows(
context.request.parameters.custpage_date_from,
context.request.parameters.custpage_date_to
);
}
var inline = form.addField({
id: 'custpage_tabulator_html',
type: serverWidget.FieldType.INLINEHTML,
label: 'Tabulator'
});
inline.defaultValue = buildTabulatorHtml(rows);
context.response.writePage(form);
}
function getMovementRows(dateFrom, dateTo) {
var rows = [];
search.create({
type: search.Type.TRANSACTION,
filters: [
['mainline', 'is', 'F'], 'AND',
['trandate', 'within', dateFrom, dateTo]
],
columns: ['trandate', 'tranid', 'item', 'quantity', 'location']
}).run().each(function(r) {
rows.push({
trandate: r.getValue('trandate'),
tranid: r.getValue('tranid'),
item: r.getText('item'),
quantity: parseFloat(r.getValue('quantity')) || 0,
location: r.getText('location') || ''
});
return true;
});
return rows;
}
function buildTabulatorHtml(rows) {
return [
'<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator.min.css">',
'<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>',
'<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>',
'<div style="margin-top:16px;">',
' <button type="button" onclick="downloadXlsx()">Download Excel</button>',
'</div>',
'<div id="qm-tabulator" style="margin-top:12px;"></div>',
'<script>',
' var tableData = ' + JSON.stringify(rows) + ';',
' var table = new Tabulator("#qm-tabulator", {',
' data: tableData,',
' layout: "fitColumns",',
' pagination: "local",',
' paginationSize: 50,',
' columns: [',
' { title: "Date", field: "trandate" },',
' { title: "Document", field: "tranid" },',
' { title: "Item", field: "item" },',
' { title: "Qty", field: "quantity", hozAlign: "right" },',
' { title: "Location", field: "location" }',
' ]',
' });',
' function downloadXlsx(){',
' table.download("xlsx", "QuickMart_Stock_Movement.xlsx", {sheetName:"Movement"});',
' }',
'</script>'
].join('\n');
}
return { onRequest: onRequest };
});
Test Steps
- Deploy Suitelet and open URL.
- Fill
Date FromandDate To, then click Show Data. - Verify Tabulator grid appears with pagination and sortable columns.
- Click Download Excel.
- Open downloaded
.xlsxand verify row count and key values.
Expected Result
- Report grid loads data correctly from selected date range.
- Tabulator features (sort/pagination) work in browser.
- XLSX file downloads without server-side file generation.
Test Case #36: Suitelet Tabulator Report - Purchase Request Queue (QuickMart)
Objective: Show pending Purchase Requests in a browser grid and allow one-click Excel download.
Scenario
Sarah (Procurement) checks pending purchase requests every morning and sends the list to her manager.
QuickMart Schema Used
- Header:
customrecord_purch_req - Line:
customrecord_purch_req_line - Key fields:
custrecord_pr_number,custrecord_pr_date,custrecord_pr_status,custrecord_pr_requestor,custrecord_pr_dept,custrecord_pr_total
Script Implementation
Script 1: Suitelet (customscript_qm_pr_queue_sl)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/search'], function(serverWidget, search) {
function onRequest(context) {
var params = context.request.parameters;
var form = serverWidget.createForm({ title: 'QuickMart Purchase Request Queue' });
form.clientScriptModulePath = 'SuiteScripts/QuickMart/qm_pr_queue_cs.js';
var fldDept = form.addField({
id: 'custpage_dept',
type: serverWidget.FieldType.SELECT,
label: 'Department',
source: 'department'
});
fldDept.isMandatory = true;
fldDept.defaultValue = params.custpage_dept || '';
var fldAsOf = form.addField({
id: 'custpage_as_of_date',
type: serverWidget.FieldType.DATE,
label: 'As Of Date'
});
fldAsOf.isMandatory = true;
fldAsOf.defaultValue = params.custpage_as_of_date || '';
var fldStatus = form.addField({
id: 'custpage_status',
type: serverWidget.FieldType.SELECT,
label: 'Status'
});
fldStatus.addSelectOption({ value: '', text: '- All -' });
fldStatus.addSelectOption({ value: 'pending', text: 'Pending Approval' });
fldStatus.addSelectOption({ value: 'approved', text: 'Approved' });
fldStatus.addSelectOption({ value: 'rejected', text: 'Rejected' });
fldStatus.defaultValue = params.custpage_status || '';
form.addSubmitButton({ label: 'Show Data' });
var rows = [];
if (context.request.method === 'POST') {
rows = getPurchaseRequestRows(
params.custpage_dept,
params.custpage_as_of_date,
params.custpage_status
);
}
var inline = form.addField({
id: 'custpage_pr_grid',
type: serverWidget.FieldType.INLINEHTML,
label: 'Report Grid'
});
inline.defaultValue = buildGridHtml(rows);
context.response.writePage(form);
}
function getPurchaseRequestRows(deptId, asOfDate, statusKey) {
var rows = [];
var filters = [
['custrecord_pr_dept', 'anyof', deptId], 'AND',
['custrecord_pr_date', 'onorbefore', asOfDate]
];
var statusLabelMap = {
pending: 'Pending Approval',
approved: 'Approved',
rejected: 'Rejected'
};
var selectedStatusLabel = statusLabelMap[statusKey] || '';
search.create({
type: 'customrecord_purch_req',
filters: filters,
columns: [
'custrecord_pr_number',
'custrecord_pr_date',
'custrecord_pr_status',
'custrecord_pr_requestor',
'custrecord_pr_dept',
'custrecord_pr_total'
]
}).run().each(function(result) {
var row = {
prNumber: result.getValue('custrecord_pr_number') || '-',
requestDate: result.getValue('custrecord_pr_date') || '-',
status: result.getText('custrecord_pr_status') || '-',
requestor: result.getText('custrecord_pr_requestor') || '-',
department: result.getText('custrecord_pr_dept') || '-',
total: parseFloat(result.getValue('custrecord_pr_total')) || 0
};
if (selectedStatusLabel && row.status !== selectedStatusLabel) {
return true;
}
rows.push(row);
return true;
});
return rows;
}
function buildGridHtml(rows) {
var safeJson = JSON.stringify(rows).replace(/</g, '\\u003c');
return [
'<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator.min.css">',
'<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>',
'<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>',
'<div style="margin-top:12px;">',
' <button type="button" onclick="downloadPrQueue()">Download Report</button>',
'</div>',
'<div id="qm-pr-grid" style="margin-top:12px;"></div>',
'<script>',
' var tableData = ' + safeJson + ';',
' var table = new Tabulator("#qm-pr-grid", {',
' data: tableData,',
' layout: "fitColumns",',
' pagination: "local",',
' paginationSize: 25,',
' columns: [',
' { title: "PR Number", field: "prNumber" },',
' { title: "Request Date", field: "requestDate" },',
' { title: "Status", field: "status" },',
' { title: "Requestor", field: "requestor" },',
' { title: "Department", field: "department" },',
' { title: "Total", field: "total", hozAlign: "right" }',
' ]',
' });',
' function downloadPrQueue() {',
' table.download("xlsx", "QuickMart_PR_Queue.xlsx", { sheetName: "PR Queue" });',
' }',
'</script>'
].join('\n');
}
return { onRequest: onRequest };
});
Script 2: Client Script (qm_pr_queue_cs.js)
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/ui/dialog'], function(dialog) {
function saveRecord(context) {
var rec = context.currentRecord;
var dept = rec.getValue({ fieldId: 'custpage_dept' });
var asOfDate = rec.getValue({ fieldId: 'custpage_as_of_date' });
if (!dept) {
dialog.alert({ title: 'Validation Error', message: 'Department is required.' });
return false;
}
if (!asOfDate) {
dialog.alert({ title: 'Validation Error', message: 'As Of Date is required.' });
return false;
}
return true;
}
return { saveRecord: saveRecord };
});
Test Steps
- Open the Purchase Request Queue Suitelet.
- Fill
Department,As Of Date, andStatus, then click Show Data. - Confirm the grid shows PR Number, Request Date, Requestor, Department, and Total.
- Use search text box to filter one request number.
- Click Download Report.
- Open the downloaded
.xlsxfile and confirm rows match the visible grid.
Expected Result
- Mandatory filter validation works before submit.
- Tabulator grid loads data correctly after submit.
- Search and pagination work in the browser.
- Excel download is successful and matches filtered results.
Test Case #37: Suitelet Server Export - Customer Loyalty Summary (QuickMart)
Objective: Download a filtered customer loyalty summary file from Suitelet.
Scenario
Maya (Customer Service) downloads weekly loyalty-member data to prepare promo targeting.
QuickMart Schema Used
- Record:
customrecord_cust_loyalty - List:
customlist_loyalty_tier - Key fields:
custrecord_cl_customer,custrecord_cl_tier,custrecord_cl_points,custrecord_cl_totalspent,custrecord_cl_joindate,custrecord_cl_lastredem
Script Implementation
Script: Suitelet (customscript_qm_loyalty_summary_sl)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/runtime', 'N/url', 'N/search', 'N/file', 'N/encode'],
function(serverWidget, runtime, url, search, file, encode) {
function onRequest(context) {
var params = context.request.parameters;
var form = buildForm(params);
if (context.request.method === 'POST') {
var scriptObj = runtime.getCurrentScript();
var suiteletUrl = url.resolveScript({
scriptId: scriptObj.id,
deploymentId: scriptObj.deploymentId,
params: {
custpage_tier: params.custpage_tier || '',
custpage_join_start: params.custpage_join_start || '',
custpage_join_end: params.custpage_join_end || '',
check_status: '1'
}
});
var wrapper = form.addField({
id: 'custpage_redirect_wrapper',
type: serverWidget.FieldType.INLINEHTML,
label: 'Redirect'
});
wrapper.defaultValue = '<script>window.open("' + suiteletUrl + '","_blank");</script>';
context.response.writePage(form);
return;
}
if (context.request.method === 'GET' && params.check_status === '1') {
var rows = getLoyaltyRows(params.custpage_tier, params.custpage_join_start, params.custpage_join_end);
var exportFile = buildLoyaltyXls(rows, params.custpage_join_start, params.custpage_join_end);
context.response.writeFile({ file: exportFile, isInline: false });
return;
}
context.response.writePage(form);
}
function buildForm(params) {
var form = serverWidget.createForm({ title: 'QuickMart Customer Loyalty Summary' });
var tier = form.addField({
id: 'custpage_tier',
type: serverWidget.FieldType.SELECT,
label: 'Loyalty Tier',
source: 'customlist_loyalty_tier'
});
tier.defaultValue = params.custpage_tier || '';
var joinStart = form.addField({
id: 'custpage_join_start',
type: serverWidget.FieldType.DATE,
label: 'Join Date Start'
});
joinStart.isMandatory = true;
joinStart.defaultValue = params.custpage_join_start || '';
var joinEnd = form.addField({
id: 'custpage_join_end',
type: serverWidget.FieldType.DATE,
label: 'Join Date End'
});
joinEnd.isMandatory = true;
joinEnd.defaultValue = params.custpage_join_end || '';
form.addSubmitButton({ label: 'Download Data' });
return form;
}
function getLoyaltyRows(tierId, joinStart, joinEnd) {
var rows = [];
var filters = [['custrecord_cl_joindate', 'within', joinStart, joinEnd]];
if (tierId) {
filters.push('AND', ['custrecord_cl_tier', 'anyof', tierId]);
}
search.create({
type: 'customrecord_cust_loyalty',
filters: filters,
columns: [
'custrecord_cl_customer',
'custrecord_cl_tier',
'custrecord_cl_points',
'custrecord_cl_totalspent',
'custrecord_cl_joindate',
'custrecord_cl_lastredem'
]
}).run().each(function(result) {
rows.push({
customer: result.getText('custrecord_cl_customer') || '-',
tier: result.getText('custrecord_cl_tier') || '-',
points: parseInt(result.getValue('custrecord_cl_points'), 10) || 0,
totalSpent: parseFloat(result.getValue('custrecord_cl_totalspent')) || 0,
joinDate: result.getValue('custrecord_cl_joindate') || '-',
lastRedeem: result.getValue('custrecord_cl_lastredem') || '-'
});
return true;
});
return rows;
}
function buildLoyaltyXls(rows, joinStart, joinEnd) {
var xml = [];
xml.push('<?xml version="1.0"?>');
xml.push('<?mso-application progid="Excel.Sheet"?>');
xml.push('<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"');
xml.push(' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">');
xml.push('<Worksheet ss:Name="Loyalty Summary"><Table>');
xml.push('<Row><Cell><Data ss:Type="String">QuickMart Customer Loyalty Summary</Data></Cell></Row>');
xml.push('<Row><Cell><Data ss:Type="String">Period: ' + xmlEscape(joinStart + ' to ' + joinEnd) + '</Data></Cell></Row>');
xml.push('<Row>');
xml.push('<Cell><Data ss:Type="String">Customer</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">Tier</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">Points</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">Total Spent</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">Join Date</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">Last Redemption</Data></Cell>');
xml.push('</Row>');
rows.forEach(function(row) {
xml.push('<Row>');
xml.push('<Cell><Data ss:Type="String">' + xmlEscape(row.customer) + '</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">' + xmlEscape(row.tier) + '</Data></Cell>');
xml.push('<Cell><Data ss:Type="Number">' + row.points + '</Data></Cell>');
xml.push('<Cell><Data ss:Type="Number">' + row.totalSpent + '</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">' + xmlEscape(row.joinDate) + '</Data></Cell>');
xml.push('<Cell><Data ss:Type="String">' + xmlEscape(row.lastRedeem) + '</Data></Cell>');
xml.push('</Row>');
});
xml.push('</Table></Worksheet></Workbook>');
var base64 = encode.convert({
string: xml.join(''),
inputEncoding: encode.Encoding.UTF_8,
outputEncoding: encode.Encoding.BASE_64
});
return file.create({
name: 'QuickMart_Loyalty_Summary_' + joinStart + '_to_' + joinEnd + '.xls',
fileType: file.Type.EXCEL,
contents: base64,
isOnline: false
});
}
function xmlEscape(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\"/g, '"')
.replace(/'/g, ''');
}
return { onRequest: onRequest };
});
Test Steps
- Open the Customer Loyalty Summary Suitelet.
- Fill
Tier,Join Date Start, andJoin Date End. - Click Download Data.
- Confirm a new tab/process returns a downloadable
.xlsfile. - Open the file and verify Customer, Tier, Points, Total Spent, Join Date, and Last Redemption columns.
- Repeat with a different Tier and confirm dataset changes accordingly.
Expected Result
- Required filters are enforced.
- Submit triggers the POST-to-GET export flow correctly.
- Excel file is generated server-side and downloaded successfully.
- Exported rows reflect selected tier and date filters.