Skip to main content

Script Portlets

Create powerful custom portlets using SuiteScript 2.x with full access to NetSuite APIs.


Complete List Portlet Example

/**
* @NApiVersion 2.1
* @NScriptType Portlet
* @NModuleScope SameAccount
*/
define(['N/search', 'N/url', 'N/runtime'],
(search, url, runtime) => {

const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Open Sales Orders';

// Define columns
portlet.addColumn({
id: 'view',
type: 'url',
label: ' ',
align: 'CENTER'
});

portlet.addColumn({
id: 'tranid',
type: 'text',
label: 'Order #',
align: 'LEFT'
});

portlet.addColumn({
id: 'entity',
type: 'text',
label: 'Customer',
align: 'LEFT'
});

portlet.addColumn({
id: 'trandate',
type: 'date',
label: 'Date',
align: 'CENTER'
});

portlet.addColumn({
id: 'status',
type: 'text',
label: 'Status',
align: 'CENTER'
});

portlet.addColumn({
id: 'total',
type: 'currency',
label: 'Total',
align: 'RIGHT'
});

// Execute search
const salesOrderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', ['SalesOrd:A', 'SalesOrd:B', 'SalesOrd:D', 'SalesOrd:E']]
],
columns: [
search.createColumn({ name: 'tranid', sort: search.Sort.DESC }),
search.createColumn({ name: 'entity' }),
search.createColumn({ name: 'trandate' }),
search.createColumn({ name: 'status' }),
search.createColumn({ name: 'total' })
]
});

let rowCount = 0;
const maxRows = 10;

salesOrderSearch.run().each((result) => {
if (rowCount >= maxRows) return false;

const recordUrl = url.resolveRecord({
recordType: 'salesorder',
recordId: result.id,
isEditMode: false
});

portlet.addRow({
view: `<a href="${recordUrl}">View</a>`,
tranid: result.getValue('tranid'),
entity: result.getText('entity'),
trandate: result.getValue('trandate'),
status: result.getText('status'),
total: result.getValue('total')
});

rowCount++;
return true;
});

// Add "See All" link
const searchUrl = url.resolveScript({
scriptId: 'customscript_view_all_so',
deploymentId: 'customdeploy_view_all_so'
});

portlet.addLine({
text: `<a href="${searchUrl}">View All Open Orders</a>`
});
};

return { render };
});

Form Portlet with Submission

/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/search', 'N/ui/serverWidget', 'N/url', 'N/runtime'],
(search, serverWidget, url, runtime) => {

const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Quick Item Lookup';

// Item search field
const itemField = portlet.addField({
id: 'custpage_item',
type: 'select',
label: 'Item',
source: 'item'
});

// Location filter
portlet.addField({
id: 'custpage_location',
type: 'select',
label: 'Location',
source: 'location'
});

// Quantity field (display only after submission)
portlet.addField({
id: 'custpage_qty',
type: 'text',
label: 'Available Qty'
}).updateDisplayType({
displayType: serverWidget.FieldDisplayType.INLINE
});

// Submit button
portlet.setSubmitButton({
url: url.resolveScript({
scriptId: 'customscript_item_lookup_sl',
deploymentId: 'customdeploy_item_lookup_sl'
}),
label: 'Check Inventory'
});
};

return { render };
});

HTML Portlet with Charts

/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/search'],
(search) => {

const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Sales This Week';

// Get sales data
const salesData = getSalesData();

// Generate chart HTML
portlet.html = `
<style>
.chart-container {
padding: 15px;
}
.bar-chart {
display: flex;
align-items: flex-end;
height: 150px;
gap: 10px;
border-bottom: 2px solid #ccc;
padding-bottom: 5px;
}
.bar {
flex: 1;
background: linear-gradient(180deg, #3498db 0%, #2980b9 100%);
border-radius: 4px 4px 0 0;
position: relative;
min-height: 10px;
transition: all 0.3s ease;
}
.bar:hover {
background: linear-gradient(180deg, #2ecc71 0%, #27ae60 100%);
}
.bar-value {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
font-weight: bold;
color: #333;
}
.bar-labels {
display: flex;
gap: 10px;
margin-top: 8px;
}
.bar-label {
flex: 1;
text-align: center;
font-size: 11px;
color: #666;
}
.summary {
display: flex;
justify-content: space-around;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.summary-item {
text-align: center;
}
.summary-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
}
.summary-label {
font-size: 12px;
color: #666;
}
</style>

<div class="chart-container">
<div class="bar-chart">
${salesData.days.map((day, i) => `
<div class="bar" style="height: ${(day.amount / salesData.max) * 100}%;">
<span class="bar-value">$${formatNumber(day.amount)}</span>
</div>
`).join('')}
</div>
<div class="bar-labels">
${salesData.days.map(day => `
<div class="bar-label">${day.name}</div>
`).join('')}
</div>

<div class="summary">
<div class="summary-item">
<div class="summary-value">$${formatNumber(salesData.total)}</div>
<div class="summary-label">Total Sales</div>
</div>
<div class="summary-item">
<div class="summary-value">${salesData.orderCount}</div>
<div class="summary-label">Orders</div>
</div>
<div class="summary-item">
<div class="summary-value">$${formatNumber(salesData.average)}</div>
<div class="summary-label">Avg Order</div>
</div>
</div>
</div>
`;
};

const getSalesData = () => {
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const results = [];
let total = 0;
let orderCount = 0;

// Search for this week's sales
const salesSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'thisweek']
],
columns: [
search.createColumn({
name: 'trandate',
summary: search.Summary.GROUP
}),
search.createColumn({
name: 'amount',
summary: search.Summary.SUM
}),
search.createColumn({
name: 'internalid',
summary: search.Summary.COUNT
})
]
});

const salesByDay = {};

salesSearch.run().each((result) => {
const date = result.getValue({
name: 'trandate',
summary: search.Summary.GROUP
});
const amount = parseFloat(result.getValue({
name: 'amount',
summary: search.Summary.SUM
})) || 0;
const count = parseInt(result.getValue({
name: 'internalid',
summary: search.Summary.COUNT
})) || 0;

salesByDay[new Date(date).getDay()] = amount;
total += amount;
orderCount += count;
return true;
});

// Build array for chart
for (let i = 0; i < 7; i++) {
results.push({
name: days[i],
amount: salesByDay[i + 1] || salesByDay[i] || 0
});
}

const max = Math.max(...results.map(d => d.amount), 1);

return {
days: results,
total,
orderCount,
average: orderCount > 0 ? total / orderCount : 0,
max
};
};

const formatNumber = (num) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toFixed(0);
};

return { render };
});

Role-Based Content

/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/runtime', 'N/search'],
(runtime, search) => {

const render = (params) => {
const portlet = params.portlet;
const user = runtime.getCurrentUser();
const role = user.role;

// Different content based on role
if (role === 3) { // Administrator
renderAdminPortlet(portlet);
} else if (role === 1032) { // Sales Manager
renderSalesPortlet(portlet, user.id);
} else {
renderDefaultPortlet(portlet);
}
};

const renderAdminPortlet = (portlet) => {
portlet.title = 'System Overview';
portlet.html = `
<div style="padding: 15px;">
<h3>Admin Dashboard</h3>
<ul>
<li>Pending User Requests: 5</li>
<li>Failed Scheduled Scripts: 2</li>
<li>Integration Errors: 0</li>
</ul>
</div>
`;
};

const renderSalesPortlet = (portlet, userId) => {
portlet.title = 'My Sales Pipeline';

portlet.addColumn({ id: 'stage', type: 'text', label: 'Stage' });
portlet.addColumn({ id: 'count', type: 'integer', label: 'Count' });
portlet.addColumn({ id: 'value', type: 'currency', label: 'Value' });

// Get pipeline data for current user
const pipelineSearch = search.create({
type: search.Type.OPPORTUNITY,
filters: [
['salesrep', 'is', userId],
'AND',
['probability', 'greaterthan', 0],
'AND',
['probability', 'lessthan', 100]
],
columns: [
search.createColumn({ name: 'entitystatus', summary: search.Summary.GROUP }),
search.createColumn({ name: 'internalid', summary: search.Summary.COUNT }),
search.createColumn({ name: 'projectedtotal', summary: search.Summary.SUM })
]
});

pipelineSearch.run().each((result) => {
portlet.addRow({
stage: result.getText({ name: 'entitystatus', summary: search.Summary.GROUP }),
count: result.getValue({ name: 'internalid', summary: search.Summary.COUNT }),
value: result.getValue({ name: 'projectedtotal', summary: search.Summary.SUM })
});
return true;
});
};

const renderDefaultPortlet = (portlet) => {
portlet.title = 'Quick Links';
portlet.html = `
<div style="padding: 15px;">
<ul>
<li><a href="/app/common/entity/custjob.nl">New Customer</a></li>
<li><a href="/app/accounting/transactions/salesord.nl">New Sales Order</a></li>
<li><a href="/app/center/card.nl">My Profile</a></li>
</ul>
</div>
`;
};

return { render };
});

Refresh and Caching

/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/cache', 'N/search'],
(cache, search) => {

const CACHE_NAME = 'PORTLET_DATA';
const CACHE_TTL = 300; // 5 minutes

const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Cached Summary';

// Try to get cached data
const dataCache = cache.getCache({ name: CACHE_NAME });
let summaryData = dataCache.get({ key: 'summary' });

if (!summaryData) {
// Calculate fresh data
summaryData = JSON.stringify(calculateSummary());

// Store in cache
dataCache.put({
key: 'summary',
value: summaryData,
ttl: CACHE_TTL
});
}

const data = JSON.parse(summaryData);

portlet.html = `
<div style="padding: 15px;">
<p>Orders Today: ${data.ordersToday}</p>
<p>Revenue: $${data.revenue}</p>
<p style="font-size: 10px; color: #999;">
Last updated: ${data.timestamp}
</p>
</div>
`;
};

const calculateSummary = () => {
// Expensive search operation
let ordersToday = 0;
let revenue = 0;

search.create({
type: search.Type.SALES_ORDER,
filters: [['trandate', 'on', 'today'], 'AND', ['mainline', 'is', 'T']],
columns: ['amount']
}).run().each((result) => {
ordersToday++;
revenue += parseFloat(result.getValue('amount')) || 0;
return true;
});

return {
ordersToday,
revenue: revenue.toFixed(2),
timestamp: new Date().toLocaleTimeString()
};
};

return { render };
});

See Also