Skip to main content

suitescript-tests-suitelet-pdf

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>
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

  1. Packing slip prints from Item Fulfillment or Sales Order
  2. Barcode renders for each line item
  3. 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

  1. Invoice transaction type is enabled.
  2. You can create/edit transaction forms and Advanced PDF templates.
  3. Test invoice exists (example: INV-TEST-001) with at least 2 line items.

Step-by-Step Guide

  1. Create the Advanced PDF template.
Customization > Forms > Advanced PDF/HTML Templates > New
Name: QuickMart Invoice - Retail PDF
Type: Transaction
Transaction: Invoice
  1. 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>
  1. Build or clone the invoice custom form.
Customization > Forms > Transaction Forms > Invoice > Customize
Form Name: QuickMart Invoice - Retail
  1. 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
  1. 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
  1. 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
  1. Validate expected behavior.

  2. PDF header shows QuickMart branding from your custom template.

  3. Printed layout matches XML (not standard NetSuite invoice).

  4. Line item count and totals match the invoice record.

  5. Reopen the same invoice with a different form and confirm output changes accordingly.

Pass/Fail Criteria

  • Pass: printing from QuickMart Invoice - Retail always uses QuickMart 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

  1. Deploy the Suitelet (customscript_oas_if_print_sl) in Released status.
  2. Deploy the User Event on Sales Order.
  3. Upload XML template to SuiteScripts/QuickMart/templates/qm_pick_ticket.xml.
  4. Confirm test role has permission to Sales Orders and script deployments.

Test Steps

  1. Open an existing Sales Order in View mode.
  2. Confirm button Print appears.
  3. Click button and confirm Suitelet opens in a new tab.
  4. Confirm PDF renders with Sales Order header and all line items.
  5. Confirm values in PDF match the source Sales Order.
  6. Optional negative test: open Suitelet URL without id and confirm validation error.

Expected Result

  1. Button is visible only in View mode.
  2. Suitelet receives recordtype, id, and employee and loads data correctly.
  3. XML template receives record and data.lines and renders without errors.
  4. 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

  1. Deploy Suitelet and Client Script in Released status.
  2. Open Suitelet URL.
  3. Fill Date From and Date To.
  4. Click Show Data and verify rows appear in sublist.
  5. Click Download Excel and verify file is downloaded.
  6. Open downloaded file in Excel and verify data/columns match UI results.

Expected Result

  1. Date fields are mandatory and filter results correctly.
  2. Download action returns Excel-compatible file with same filtered dataset.
  3. 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

  1. Deploy Suitelet and open URL.
  2. Fill Date From and Date To, then click Show Data.
  3. Verify Tabulator grid appears with pagination and sortable columns.
  4. Click Download Excel.
  5. Open downloaded .xlsx and verify row count and key values.

Expected Result

  1. Report grid loads data correctly from selected date range.
  2. Tabulator features (sort/pagination) work in browser.
  3. 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

  1. Open the Purchase Request Queue Suitelet.
  2. Fill Department, As Of Date, and Status, then click Show Data.
  3. Confirm the grid shows PR Number, Request Date, Requestor, Department, and Total.
  4. Use search text box to filter one request number.
  5. Click Download Report.
  6. Open the downloaded .xlsx file and confirm rows match the visible grid.

Expected Result

  1. Mandatory filter validation works before submit.
  2. Tabulator grid loads data correctly after submit.
  3. Search and pagination work in the browser.
  4. 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&apos;');
}

return { onRequest: onRequest };
});

Test Steps

  1. Open the Customer Loyalty Summary Suitelet.
  2. Fill Tier, Join Date Start, and Join Date End.
  3. Click Download Data.
  4. Confirm a new tab/process returns a downloadable .xls file.
  5. Open the file and verify Customer, Tier, Points, Total Spent, Join Date, and Last Redemption columns.
  6. Repeat with a different Tier and confirm dataset changes accordingly.

Expected Result

  1. Required filters are enforced.
  2. Submit triggers the POST-to-GET export flow correctly.
  3. Excel file is generated server-side and downloaded successfully.
  4. Exported rows reflect selected tier and date filters.