Create an approval workflow for the record.
Then create an Email Template
Next, develop two Suitelet pages: one for confirmation and another for approving or rejecting the record.
define(['N/ui/serverWidget', 'N/redirect'], function (serverWidget, redirect) {
"use strict"
/**
* The main function that handles the Suitelet request
* @param {Object} context - The context object
*/
function onRequest(context) {
try {
if (context.request.method === 'GET') {
let soid = context.request.parameters.soid;
let action = context.request.parameters.action;
let levelValue = context.request.parameters.level;
let form = serverWidget.createForm({ title: 'Confirm Action' });
form.addField({
id: 'custpage_confirmation',
type: serverWidget.FieldType.INLINEHTML,
label: 'Confirmation'
}).defaultValue = 'Are you sure you want to <strong>' + action + '</strong> this Purchase Order?';
form.addSubmitButton({ label: 'Yes, ' + action });
form.addButton({
id: 'cancel',
label: 'Cancel',
functionName: "window.close();"
});
let soidField = form.addField({
id: 'custpage_soid',
type: serverWidget.FieldType.TEXT,
label: 'SO ID'
});
soidField.defaultValue = soid;
soidField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
let actionField = form.addField({
id: 'custpage_action',
type: serverWidget.FieldType.TEXT,
label: 'Action'
});
actionField.defaultValue = action;
actionField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
let levelField = form.addField({
id: 'custpage_level',
type: serverWidget.FieldType.TEXT,
label: 'Level'
});
levelField.defaultValue = levelValue;
levelField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
context.response.writePage(form);
} else if (context.request.method === 'POST') {
let soid = context.request.parameters.custpage_soid;
let action = context.request.parameters.custpage_action;
let level = context.request.parameters.custpage_level;
let suiteletUrl = 'https://8183733-sb1.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=1543&deploy=1&compid=8183733_SB1&ns-at=AAEJ7tMQNMiqlZATT9V-tfJjenXWIT-Mzv8pXt-YMEHq5k4ZYE4&soid=' + soid + '&action=' + action + '&level=' + level;
redirect.redirect({
url: suiteletUrl,
parameters: {
soid: soid,
action: action,
level: level
}
});
}
}
catch (e) {
log.error("error@onRequest", e);
}
}
return {
onRequest: onRequest
};
});
define(['N/record', 'N/ui/message', 'N/email', 'N/render', 'N/search', 'N/ui/serverWidget', 'N/runtime'],
function (record, message, email, render, search, serverWidget, runtime) {
"use strict"
/**
* Submits approval for the purchase order, generates PDF, and sends approval email.
*
* @param {string} purchaseOrderId - The internal ID of the purchase order to be approved.
* @param {string} poNextApprover - The internal ID of the next approver for the purchase order.
* @param {string} createdBy - The internal ID of the user who created the purchase order.
* @param {string} soNumber - The purchase order number to display in the success form.
* @param {Object} context - The SuiteScript context object to pass to display the approval success form.
*
* @throws {Error} If there is an issue with submitting the purchase order, rendering the PDF, or sending the email.
*/
function sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context) {
try {
record.submitFields({
type: record.Type.PURCHASE_ORDER,
id: purchaseOrderId,
values: {
approvalstatus: '2',
orderstatus: "B",
status: "Pending Receipt",
nextapprover: '',
}
});
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 16);
if (poNextApprover) {
sendApprovalEmail(purchaseOrderId, poNextApprover, createdBy, emailContent.emailBody, emailContent.emailSubject, pdfFile);
}
else {
sendApprovalEmail(purchaseOrderId, createdBy, createdBy, emailContent.emailBody, emailContent.emailSubject, pdfFile);
}
displayApprovalSuccessForm(context, soNumber);
}
catch (e) {
log.error("error@sendEmailandApproval", e);
}
}
/**
* Submits approval for the purchase order, generates PDF, and sends approval email.
*
* @param {string} purchaseOrderId - The internal ID of the purchase order to be approved.
* @param {string} poNextApprover - The internal ID of the next approver for the purchase order.
* @param {string} createdBy - The internal ID of the user who created the purchase order.
* @param {string} soNumber - The purchase order number to display in the success form.
* @param {Object} context - The SuiteScript context object to pass to display the approval success form.
*
* @throws {Error} If there is an issue with submitting the purchase order, rendering the PDF, or sending the email.
*/
function sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context) {
try {
record.submitFields({
type: record.Type.PURCHASE_ORDER,
id: purchaseOrderId,
values: {
custbody_jj_approval_status: 'rejected'
}
});
displayRejectForm(context, soNumber);
}
catch (e) {
log.error("error@sendEmailandApproval", e);
}
}
/**
* Submit the purchase order approval status
* @param {number} purchaseOrderId - ID of the purchase order
* @param {string} nextApprover - The ID of the next approver
* @param {number} level - The approval level
*/
function submitApproval(purchaseOrderId, nextApprover, level, appStatus, ordeStatus, actualStatus) {
try {
record.submitFields({
type: record.Type.PURCHASE_ORDER,
id: purchaseOrderId,
values: {
approvalstatus: appStatus,
orderstatus: ordeStatus,
status: actualStatus,
nextapprover: nextApprover,
custbody_jj_approval_level_list: level
}
});
}
catch (e) {
log.error("error@submitApproval", e);
}
}
/**
* Render the PDF file for the purchase order
* @param {number} purchaseOrderId - ID of the purchase order
* @returns {file.File} The rendered PDF file
*/
function renderSalesOrderPDF(purchaseOrderId) {
try {
return render.transaction({
entityId: purchaseOrderId,
printMode: render.PrintMode.PDF
});
}
catch (e) {
log.error("error@renderSalesOrderPDF", e);
}
}
/**
* Merge the email template for the purchase order
* @param {number} purchaseOrderId - ID of the purchase order
* @param {number} templateId - The ID of the email template
* @returns {Object} The merged email content (body and subject)
*/
function mergeEmailTemplate(purchaseOrderId, templateId) {
try {
let mergeResult = render.mergeEmail({
templateId: templateId,
transactionId: purchaseOrderId
});
return {
emailBody: mergeResult.body,
emailSubject: mergeResult.subject
};
}
catch (e) {
log.error("error@mergeEmailTemplate", e);
}
}
/**
* Send the approval email
* @param {number} purchaseOrderId - ID of the purchase order
* @param {number} authorId - The ID of the email author
* @param {string} recipient - The recipient's email address
* @param {string} emailBody - The email body content
* @param {string} emailSubject - The email subject
* @param {file.File} pdfFile - The attached PDF file
*/
function sendApprovalEmail(purchaseOrderId, authorId, recipient, emailBody, emailSubject, pdfFile) {
try {
email.send({
author: authorId,
body: emailBody,
recipients: recipient,
subject: emailSubject,
attachments: [pdfFile],
relatedRecords: {
transactionId: purchaseOrderId
}
});
}
catch (e) {
log.error("error@sendApprovalEmail", e);
}
}
/**
* Create and display the approval success form
* @param {Object} context - The context object
* @param {string} soNumber - The purchase order number
*/
function displayApprovalSuccessForm(context, soNumber) {
try {
let form = serverWidget.createForm({ title: "Purchase Order " + soNumber + " has been approved successfully" });
let messageObj = message.create({
type: message.Type.INFORMATION,
message: "Approved",
duration: 30000
});
form.addPageInitMessage({ message: messageObj });
context.response.writePage(form);
}
catch (e) {
log.error("error@displayApprovalSuccessForm", e);
}
}
/**
* Create and display the reject form
* @param {Object} context - The context object
* @param {string} soNumber - The purchase order number
*/
function displayRejectForm(context, soNumber) {
try {
let form = serverWidget.createForm({ title: "Purchase Order " + soNumber + " has been Rejected" });
let messageObj = message.create({
type: message.Type.INFORMATION,
message: "Rejected",
duration: 30000
});
form.addPageInitMessage({ message: messageObj });
context.response.writePage(form);
}
catch (e) {
log.error("error@displayApprovalSuccessForm", e);
}
}
/**
* Create and display the Invalid response form
* @param {Object} context - The context object
* @param {string} soNumber - The purchase order number
*/
function displayInvalidResponse(context) {
try {
let form = serverWidget.createForm({ title: "Invalid Response" });
let messageObj = message.create({
type: message.Type.INFORMATION,
message: "This response is no longer valid.",
duration: 30000
});
form.addPageInitMessage({ message: messageObj });
context.response.writePage(form);
}
catch (e) {
log.error("error@displayInvalidResponse", e)
}
}
/**
* Handle the approval or rejection of a purchase order
* @param {Object} context - The context object
* @param {number} purchaseOrderId - The ID of the purchase order
* @param {string} action - The action to be performed ('approve' or 'reject')
* @returns {Object} The result object indicating success or failure
*/
function handleApproval(context, purchaseOrderId, action, levelValue) {
try {
let purchaseOrder = record.load({
type: record.Type.PURCHASE_ORDER,
id: purchaseOrderId,
isDynamic: true
});
let soNumber = purchaseOrder.getText('tranid');
let createdBy = purchaseOrder.getValue({ fieldId: 'custbody_3rp_pocreator' });
let amountAfterDiscount = purchaseOrder.getValue({ fieldId: 'custbody_stc_amount_after_discount' });
let orderDept = purchaseOrder.getValue({ fieldId: 'department' });
let poNextApprover = purchaseOrder.getValue({ fieldId: 'nextapprover' });
let poLAmount = search.lookupFields({
type: "department",
id: orderDept,
columns: ['custrecord2', 'custrecord5', 'custrecord4', 'custrecord7', 'custrecord6', 'custrecord9', 'custrecord8', 'custrecord10', 'custrecord11']
});
let polAmountNew = poLAmount.custrecord2;
let pol2Amount = poLAmount.custrecord4;
let pol3Amount = poLAmount.custrecord6;
let pol4Amount = poLAmount.custrecord8;
let pol5Amount = poLAmount.custrecord10;
let nextApprovaer = poLAmount.custrecord5[0]?.value || '';
let nextApproverL2 = poLAmount.custrecord7[0]?.value || '';
let nextApproverL3 = poLAmount.custrecord9?.[0]?.value || '';
let nextApproverL4 = poLAmount.custrecord11?.[0]?.value || '';
let statusField = purchaseOrder.getValue({ fieldId: 'approvalstatus' });
let statusLevel = purchaseOrder.getValue({ fieldId: 'custbody_jj_approval_level_list' });
let poRaiser = purchaseOrder.getValue({ fieldId: 'custbody_3rp_pocontact' });
let mainStatus = purchaseOrder.getValue({ fieldId: 'status' });
let rejectValue = purchaseOrder.getValue('custbody_jj_rejected')
if (soNumber) {
if (levelValue == 0) {
if (action == 'approve' && !rejectValue && statusField == '1' && amountAfterDiscount > polAmountNew && statusLevel == '' && poRaiser != nextApprovaer && mainStatus != "Closed") {
log.debug("test")
submitApproval(purchaseOrderId, nextApprovaer, 1, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 19);
sendApprovalEmail(purchaseOrderId, createdBy, nextApprovaer, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if (action == 'approve' && !rejectValue && statusField == '1' && amountAfterDiscount > pol2Amount && statusLevel == '' && poRaiser == nextApprovaer && mainStatus != "Closed") {
submitApproval(purchaseOrderId, nextApproverL2, 2, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 19);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL2, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if ((action == 'approve' && !rejectValue && (nextApprovaer == '' || amountAfterDiscount <= polAmountNew || (amountAfterDiscount > polAmountNew && amountAfterDiscount <= pol2Amount && poRaiser == nextApprovaer && statusLevel == '') || (statusLevel == "2" && amountAfterDiscount <= pol3Amount) || (statusLevel == "1" && amountAfterDiscount <= pol2Amount)))) {
if (statusLevel == '' && statusField == '1') {
sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context);
}
else {
displayInvalidResponse(context);
return;
}
}
else if (action == 'reject') {
if (!rejectValue && statusLevel == '' && statusField == '1') {
sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context)
}
else {
displayInvalidResponse(context);
return;
}
}
else {
displayInvalidResponse(context);
return;
}
}
if (levelValue == 1) {
if (action == 'approve' && !rejectValue && statusField == '1' && amountAfterDiscount > pol2Amount && statusLevel == '1' && poRaiser != nextApproverL2 && mainStatus != "Closed") {
submitApproval(purchaseOrderId, nextApproverL2, 2, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 20);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL2, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if (action == 'approve' && !rejectValue && statusField == '1' && amountAfterDiscount > pol3Amount && statusLevel == "1" && poRaiser == nextApproverL2 && mainStatus != "Closed") {
submitApproval(purchaseOrderId, nextApproverL3, 3, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 20);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL3, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if ((action == 'approve' && !rejectValue && (nextApproverL2 == '' || amountAfterDiscount <= pol2Amount || (amountAfterDiscount > pol2Amount && amountAfterDiscount <= pol3Amount && poRaiser == nextApproverL2 && statusLevel == "1") || (statusLevel == "2" && amountAfterDiscount <= pol3Amount)))) {
if (statusLevel == '1' && statusField == '1') {
sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context);
}
else {
displayInvalidResponse(context);
return;
}
}
else if (action == 'reject') {
if (!rejectValue && statusLevel == '1' && statusField == '1') {
sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context)
}
else {
displayInvalidResponse(context);
return;
}
}
else {
displayInvalidResponse(context);
return;
}
}
if (levelValue == "2") {
if (action == 'approve' && !rejectValue && amountAfterDiscount > pol3Amount && poRaiser != nextApproverL3 && statusLevel == '2') {
submitApproval(purchaseOrderId, nextApproverL3, 3, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 21);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL3, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if (action == 'approve' && !rejectValue && amountAfterDiscount > pol4Amount && poRaiser == nextApproverL3 && statusLevel == '2') {
submitApproval(purchaseOrderId, nextApproverL4, 4, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 21);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL4, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if ((action == 'approve' && !rejectValue && (nextApproverL3 == '' || amountAfterDiscount <= pol3Amount || (amountAfterDiscount > pol3Amount && amountAfterDiscount <= pol4Amount && poRaiser == nextApproverL3 && statusLevel == "2")))) {
if (statusLevel == '2' && statusField == '1') {
sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context);
}
else {
displayInvalidResponse(context);
return;
}
}
else if (action == 'reject') {
if (!rejectValue && statusLevel == '2' && statusField == '1') {
sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context)
}
else {
displayInvalidResponse(context);
return;
}
}
else {
displayInvalidResponse(context);
return;
}
}
if (levelValue == '3') {
if (action == 'approve' && !rejectValue && amountAfterDiscount > pol4Amount && poRaiser != nextApproverL4 && statusLevel == '3') {
submitApproval(purchaseOrderId, nextApproverL4, 4, '1', 'A', "Pending Approval");
let pdfFile = renderSalesOrderPDF(purchaseOrderId);
let emailContent = mergeEmailTemplate(purchaseOrderId, 22);
sendApprovalEmail(purchaseOrderId, createdBy, nextApproverL4, emailContent.emailBody, emailContent.emailSubject, pdfFile);
displayApprovalSuccessForm(context, soNumber);
}
else if (action == 'approve' && !rejectValue && (nextApproverL4 == '' || amountAfterDiscount <= pol4Amount || (amountAfterDiscount > pol4Amount && poRaiser == nextApproverL4 && statusLevel == '3'))) {
if (statusLevel == '3' && statusField == '1') {
sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context);
}
else {
displayInvalidResponse(context);
return;
}
}
else if (action == 'reject') {
if (!rejectValue && statusLevel == '3' && statusField == '1') {
sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context)
}
else {
displayInvalidResponse(context);
return;
}
}
else {
displayInvalidResponse(context);
return;
}
}
if (levelValue == '4') {
if (action == 'approve' && !rejectValue && statusLevel == '4' && statusField == '1') {
sendEmailandApproval(purchaseOrderId, poNextApprover, createdBy, soNumber, context);
}
else if (action == 'reject') {
if (!rejectValue && statusLevel == '4' && statusField == '1') {
sendEmailandReject(purchaseOrderId, poNextApprover, createdBy, soNumber, context)
}
else {
displayInvalidResponse(context);
return;
}
}
else {
displayInvalidResponse(context);
return;
}
}
}
} catch (e) {
log.error('Error in handleApproval', e);
return { success: false, message: e.message };
}
}
/**
* The main function that handles the Suitelet request
* @param {Object} context - The context object
*/
function onRequest(context) {
try {
if (context.request.method === 'GET') {
log.debug("context", runtime.executionContext)
log.debug("type", context.type)
let request = context.request;
let soid = parseInt(request.parameters.soid);
let action = request.parameters.action;
let levelValue = request.parameters.level;
log.debug("action", action)
if (action === 'approve' || action === 'reject') {
handleApproval(context, soid, action, levelValue);
}
}
} catch (e) {
log.error('Error in onRequest', e);
}
}
return {
onRequest: onRequest
};
}
);
Email Template
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
/* General styles */
body {
font-family: Arial, sans-serif;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
padding: 20px;
text-align: left;
}
.content p {
font-size: 16px;
line-height: 1.5;
margin: 0 0 20px;
}
.highlight {
font-weight: bold;
}
/* Footer */
.footer {
padding: 10px 20px;
background-color: #f9f9f9;
text-align: left;
font-size: 14px;
border-top: 1px solid #ddd;
}
.footer a {
color: #00539C;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.footer ul {
padding-left: 20px;
margin: 0;
list-style: disc;
}
.footer ul li {
margin-bottom: 5px;
}
/* Responsive styles */
@media screen and (max-width: 450px) and (min-width: 330px) {
#itemid, #totalid {
table-layout: fixed;
}
}
</style>
</head>
<body style="font-family: Arial, sans-serif;color: #333;margin: 0;padding: 0; background-color: #f5f5f5;">
<div><!-- Content --> <img style="width: 100%; max-width: 1000px; height: auto;" src="https://617671-sb1.app.netsuite.com/core/media/media.nl?id=16661&c=617671_SB1&h=Fj-ThuEG0xpirVrtvGU7ed_aJQZo3HC6psa64SEWdfQSq0Hx" width="1000" border="0" />
<div><#assign date="#date">
<p style="font-size: 16px; line-height: 1.5; margin: 0 0 20px;">Hi ${transaction.nextapprover.firstName},</p>
<p style="font-size: 16px; line-height: 1.5; margin: 0 0 20px;">Purchase Order ${transaction.tranId} is waiting for your approval. Please review the attached document file.</p>
<p style="font-size: 16px; line-height: 1.5; margin: 0 0 20px;">To approve or reject this document through this email, please click:</p>
<!-- Buttons --> <br /><strong>
<a style="font-family: Arial, sans-serif; font-size: 16px; color: #333; font-weight: bold; text-decoration: none; background-color: #f8a429; text-align: center; padding: 10px 80px; border-radius: 5px; border: 1px solid #ddd; margin: 0 10px;"
href="https://8183733-sb1.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=1544&deploy=1&compid=8183733_SB1&ns-at=AAEJ7tMQKHJvr_gjq-PzrtbjrqkLKKskWcimK7L3GV8TxBDipMk&soid=${transaction.id}&action=approve&level=0">Approve</a>
<a style="font-family: Arial, sans-serif; font-size: 16px; color: #333; font-weight: bold; text-decoration: none; background-color: #f8a429; text-align: center; padding: 10px 80px; border-radius: 5px; border: 1px solid #ddd; margin: 0 10px;"
href="https://8183733-sb1.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=1544&deploy=1&compid=8183733_SB1&ns-at=AAEJ7tMQKHJvr_gjq-PzrtbjrqkLKKskWcimK7L3GV8TxBDipMk&soid=${transaction.id}&action=reject&level=0">Reject</a>
<p>Notes:</p>
<ul>
<li>To open the record in NetSuite, click the View Document link below.</li>
</ul>
<br /><a style="color: black;" href="https://8183733-sb1.app.netsuite.com/app/accounting/transactions/purchord.nl?id=${transaction.id}">View Document</a></div>
<br /><br />Kind Regards,<br /><strong>${transaction.subsidiary}</strong><br /><img src="https://8183733.secure.netsuite.com/core/media/media.nl?id=11&c=8183733&h=vfyPBdNlbfXmhl37svv_mGpEVvBD7RH4zav93zhZo810ZbKA" alt="" /></div>
</body>
</html>
In this template, use the confirmation suitelet link