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
| Goal | Go To |
|---|---|
| Generate bulk PDFs | Bulk PDF Generation → |
| Advanced PDF templates | Advanced PDF Templates → |
| Return to PDF overview | PDF Customization → |