Skip to main content

Suitelet PDF Generation

Create custom PDF documents programmatically using SuiteScript.


Overview

SUITELET PDF GENERATION
═══════════════════════════════════════════════════════════════════════════════

┌─────────────────────────────────────────────────────────────────────────────┐
│ What It Is: │
│ • Script-driven PDF generation using N/render module │
│ • Full control over data sources and layout │
│ • Can pull data from searches, records, or external sources │
│ • User-triggered or automated generation │
│ │
│ Best For: │
│ • Custom reports not tied to single record │
│ • Dynamic data exports │
│ • PDF download buttons on forms │
│ • Multi-record summaries │
│ • Complex data transformations │
└─────────────────────────────────────────────────────────────────────────────┘

Basic PDF Suitelet

Simple PDF Generation

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/render', 'N/file'], function(render, file) {

function onRequest(context) {
if (context.request.method === 'GET') {

// Create simple PDF from XML
var xmlContent = `<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style>
body { font-family: sans-serif; font-size: 12pt; }
h1 { color: #2c3e50; }
</style>
</head>
<body>
<h1>Hello World PDF</h1>
<p>Generated: ${new Date().toLocaleDateString()}</p>
</body>
</pdf>`;

// Generate PDF from XML
var pdfFile = render.xmlToPdf({
xmlString: xmlContent
});

// Return PDF to browser
context.response.writeFile({
file: pdfFile,
isInline: true // Display in browser (false = download)
});
}
}

return { onRequest: onRequest };
});

Rendering Transactions

Render Existing Record as PDF

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/render', 'N/record'], function(render, record) {

function onRequest(context) {
var recordId = context.request.parameters.id;
var recordType = context.request.parameters.type || 'invoice';

// Render transaction to PDF using default template
var pdfFile = render.transaction({
entityId: parseInt(recordId),
printMode: render.PrintMode.PDF
});

// Optional: Rename the file
pdfFile.name = recordType.toUpperCase() + '_' + recordId + '.pdf';

context.response.writeFile({
file: pdfFile,
isInline: true
});
}

return { onRequest: onRequest };
});

Custom Report PDF

Search-Based Report

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @description Generate sales report PDF from search results
*/
define(['N/render', 'N/search', 'N/format'], function(render, search, format) {

function onRequest(context) {
if (context.request.method === 'GET') {

// Get date range from parameters
var startDate = context.request.parameters.startDate || getFirstDayOfMonth();
var endDate = context.request.parameters.endDate || getTodayDate();

// Run sales search
var salesData = getSalesData(startDate, endDate);

// Build PDF XML
var xmlContent = buildReportXml(salesData, startDate, endDate);

// Generate PDF
var pdfFile = render.xmlToPdf({
xmlString: xmlContent
});

pdfFile.name = 'Sales_Report_' + startDate + '_to_' + endDate + '.pdf';

context.response.writeFile({
file: pdfFile,
isInline: true
});
}
}

function getSalesData(startDate, endDate) {
var results = [];

var salesSearch = search.create({
type: search.Type.TRANSACTION,
filters: [
['type', 'anyof', 'CustInvc'],
'AND',
['trandate', 'within', startDate, endDate],
'AND',
['mainline', 'is', 'T']
],
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'trandate', sort: search.Sort.ASC }),
search.createColumn({ name: 'entity' }),
search.createColumn({ name: 'total' }),
search.createColumn({ name: 'status' })
]
});

salesSearch.run().each(function(result) {
results.push({
tranid: result.getValue('tranid'),
date: result.getValue('trandate'),
customer: result.getText('entity'),
total: parseFloat(result.getValue('total')) || 0,
status: result.getText('status')
});
return true;
});

return results;
}

function buildReportXml(data, startDate, endDate) {
// Calculate totals
var grandTotal = data.reduce(function(sum, row) {
return sum + row.total;
}, 0);

// Build rows HTML
var rowsHtml = data.map(function(row, index) {
return `<tr class="${index % 2 === 0 ? 'even' : 'odd'}">
<td>${row.tranid}</td>
<td>${row.date}</td>
<td>${row.customer}</td>
<td class="right">$${row.total.toFixed(2)}</td>
<td>${row.status}</td>
</tr>`;
}).join('');

return `<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style>
body {
font-family: sans-serif;
font-size: 10pt;
padding: 20px;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.report-info {
background: #f8f9fa;
padding: 10px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: #2c3e50;
color: white;
padding: 8px;
text-align: left;
}
td {
padding: 8px;
border-bottom: 1px solid #ddd;
}
.even { background: #f8f9fa; }
.right { text-align: right; }
.total-row {
font-weight: bold;
background: #ecf0f1;
}
.footer {
margin-top: 20px;
text-align: center;
font-size: 9pt;
color: #666;
}
</style>
</head>
<body size="Letter">
<h1>Sales Report</h1>

<div class="report-info">
<strong>Period:</strong> ${startDate} to ${endDate}<br/>
<strong>Generated:</strong> ${new Date().toLocaleString()}<br/>
<strong>Total Transactions:</strong> ${data.length}
</div>

<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Date</th>
<th>Customer</th>
<th class="right">Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${rowsHtml}
<tr class="total-row">
<td colspan="3">Grand Total</td>
<td class="right">$${grandTotal.toFixed(2)}</td>
<td></td>
</tr>
</tbody>
</table>

<div class="footer">
Confidential - Internal Use Only
</div>
</body>
</pdf>`;
}

function getFirstDayOfMonth() {
var date = new Date();
return format.format({
value: new Date(date.getFullYear(), date.getMonth(), 1),
type: format.Type.DATE
});
}

function getTodayDate() {
return format.format({
value: new Date(),
type: format.Type.DATE
});
}

return { onRequest: onRequest };
});

Template-Based PDF

Using File Cabinet Template

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/render', 'N/file', 'N/record', 'N/search'],
function(render, file, record, search) {

function onRequest(context) {
var recordId = context.request.parameters.id;

// Load template from File Cabinet
var templateFile = file.load({
id: 'SuiteScripts/templates/custom_report.xml'
});

// Load record data
var invoiceRecord = record.load({
type: record.Type.INVOICE,
id: recordId
});

// Create renderer
var renderer = render.create();

// Set template content
renderer.templateContent = templateFile.getContents();

// Add data sources
renderer.addRecord({
templateName: 'record',
record: invoiceRecord
});

// Add custom data
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: 'custom',
data: {
generatedDate: new Date().toLocaleDateString(),
reportTitle: 'Custom Invoice Report',
companyNote: 'Thank you for your business!'
}
});

// Render PDF
var pdfFile = renderer.renderAsPdf();
pdfFile.name = 'Invoice_' + recordId + '.pdf';

context.response.writeFile({
file: pdfFile,
isInline: true
});
}

return { onRequest: onRequest };
});

Template File (custom_report.xml)

<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style>
body { font-family: sans-serif; }
.title { font-size: 18pt; color: #2c3e50; }
</style>
</head>
<body>
<h1 class="title">${custom.reportTitle}</h1>

<p>Invoice: ${record.tranid}</p>
<p>Customer: ${record.entity}</p>
<p>Total: ${record.total}</p>

<p>Generated: ${custom.generatedDate}</p>
<p>${custom.companyNote}</p>
</body>
</pdf>

PDF with Form Input

User Parameter Form

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', 'N/render', 'N/search', 'N/format'],
function(serverWidget, render, search, format) {

function onRequest(context) {
if (context.request.method === 'GET') {
// Show form
var form = serverWidget.createForm({
title: 'Generate Sales Report'
});

form.addField({
id: 'custpage_startdate',
type: serverWidget.FieldType.DATE,
label: 'Start Date'
}).isMandatory = true;

form.addField({
id: 'custpage_enddate',
type: serverWidget.FieldType.DATE,
label: 'End Date'
}).isMandatory = true;

form.addField({
id: 'custpage_subsidiary',
type: serverWidget.FieldType.SELECT,
label: 'Subsidiary',
source: 'subsidiary'
});

form.addSubmitButton({ label: 'Generate PDF' });

context.response.writePage(form);

} else {
// Generate PDF
var startDate = context.request.parameters.custpage_startdate;
var endDate = context.request.parameters.custpage_enddate;
var subsidiary = context.request.parameters.custpage_subsidiary;

var reportData = getReportData(startDate, endDate, subsidiary);
var pdfFile = generatePdf(reportData, startDate, endDate);

context.response.writeFile({
file: pdfFile,
isInline: false // Force download
});
}
}

function getReportData(startDate, endDate, subsidiary) {
var filters = [
['type', 'anyof', 'CustInvc'],
'AND',
['trandate', 'within', startDate, endDate],
'AND',
['mainline', 'is', 'T']
];

if (subsidiary) {
filters.push('AND', ['subsidiary', 'anyof', subsidiary]);
}

var results = [];
search.create({
type: search.Type.TRANSACTION,
filters: filters,
columns: ['tranid', 'trandate', 'entity', 'total']
}).run().each(function(result) {
results.push({
tranid: result.getValue('tranid'),
date: result.getValue('trandate'),
customer: result.getText('entity'),
total: parseFloat(result.getValue('total')) || 0
});
return true;
});

return results;
}

function generatePdf(data, startDate, endDate) {
// Build XML (similar to previous example)
var xml = buildReportXml(data, startDate, endDate);

var pdfFile = render.xmlToPdf({ xmlString: xml });
pdfFile.name = 'Sales_Report_' + startDate.replace(/\//g, '-') + '.pdf';

return pdfFile;
}

function buildReportXml(data, startDate, endDate) {
// ... (similar to previous example)
return `<?xml version="1.0"?>...`;
}

return { onRequest: onRequest };
});

Save PDF to File Cabinet

/**
* Generate PDF and save to File Cabinet
*/
function generateAndSavePdf(recordId) {
var render = require('N/render');
var file = require('N/file');
var record = require('N/record');

// Generate PDF
var pdfFile = render.transaction({
entityId: recordId,
printMode: render.PrintMode.PDF
});

// Configure for File Cabinet
pdfFile.folder = 123; // Target folder ID
pdfFile.name = 'Invoice_' + recordId + '.pdf';
pdfFile.isOnline = true; // Available without login

// Save to File Cabinet
var fileId = pdfFile.save();

// Optionally attach to record
record.attach({
record: { type: 'file', id: fileId },
to: { type: 'invoice', id: recordId }
});

return fileId;
}

Email PDF Attachment

/**
* Generate and email PDF
*/
function emailPdfReport(recipientEmail, reportData) {
var render = require('N/render');
var email = require('N/email');

// Generate PDF
var pdfFile = render.xmlToPdf({
xmlString: buildReportXml(reportData)
});
pdfFile.name = 'Report.pdf';

// Send email with attachment
email.send({
author: -5, // Current user or employee ID
recipients: recipientEmail,
subject: 'Your Report is Ready',
body: 'Please find the attached report.',
attachments: [pdfFile]
});
}

Error Handling

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/render', 'N/log', 'N/error'], function(render, log, error) {

function onRequest(context) {
try {
var recordId = context.request.parameters.id;

if (!recordId) {
throw error.create({
name: 'MISSING_PARAMETER',
message: 'Record ID is required'
});
}

var pdfFile = render.transaction({
entityId: parseInt(recordId),
printMode: render.PrintMode.PDF
});

context.response.writeFile({
file: pdfFile,
isInline: true
});

} catch (e) {
log.error({
title: 'PDF Generation Error',
details: e.message
});

// Return error page
context.response.write({
output: `<html>
<body>
<h1>Error Generating PDF</h1>
<p>${e.message}</p>
<a href="javascript:history.back()">Go Back</a>
</body>
</html>`
});
}
}

return { onRequest: onRequest };
});

Deployment

<!-- custscript_pdf_report.xml -->
<scriptdeployment scriptid="customdeploy_pdf_report">
<status>RELEASED</status>
<title>PDF Report Generator</title>
<isdeployed>T</isdeployed>
<isonline>F</isonline>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<runasrole>ADMINISTRATOR</runasrole>
<custscriptroles>
<role>ADMINISTRATOR</role>
<role>ACCOUNTANT</role>
</custscriptroles>
</scriptdeployment>

Best Practices

SUITELET PDF BEST PRACTICES
═══════════════════════════════════════════════════════════════════════════════

PERFORMANCE:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ✓ Limit search results (use page size) │
│ ✓ Cache templates in File Cabinet │
│ ✓ Avoid loading full records when only IDs needed │
│ ✓ Use efficient XML (minimize inline styles) │
└─────────────────────────────────────────────────────────────────────────────┘

ERROR HANDLING:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ✓ Always wrap in try-catch │
│ ✓ Validate parameters before processing │
│ ✓ Log errors for debugging │
│ ✓ Provide user-friendly error messages │
└─────────────────────────────────────────────────────────────────────────────┘

SECURITY:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ✓ Restrict deployment to appropriate roles │
│ ✓ Validate user has access to requested records │
│ ✓ Don't expose sensitive data in URLs │
│ ✓ Use isOnline: false for internal PDFs │
└─────────────────────────────────────────────────────────────────────────────┘

Next Steps

GoalGo To
Generate bulk PDFsBulk PDF Generation →
Advanced PDF templatesAdvanced PDF Templates →
Return to PDF overviewPDF Customization →