The invoice emails including the credit memo and invoices will be sent on a scheduled basis. The requirement can be achieved by creating a custom record for scheduling purposes. The process will work as follows:
- A custom record will be created containing the following fields –
- Author
- Email Template
- Email Subject
- Email Body
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/currentRecord', 'N/email', 'N/record', 'N/redirect', 'N/render', 'N/runtime', 'N/search', 'N/ui/serverWidget', 'N/url'],
/**
* @param{currentRecord} currentRecord
* @param{email} email
* @param{record} record
* @param{redirect} redirect
* @param{render} render
* @param{runtime} runtime
* @param{search} search
* @param{serverWidget} serverWidget
* * @param{url} url
*/
(currentRecord, email, record, redirect, render, runtime, search, serverWidget, url) => {
/**
* Defines the function that is executed at the beginning of the map/reduce process and generates the input data.
* @param {Object} inputContext
* @param {boolean} inputContext.isRestarted - Indicates whether the current invocation of this function is the first
* invocation (if true, the current invocation is not the first invocation and this function has been restarted)
* @param {Object} inputContext.ObjectRef - Object that references the input data
* @typedef {Object} ObjectRef
* @property {string|number} ObjectRef.id - Internal ID of the record instance that contains the input data
* @property {string} ObjectRef.type - Type of the record instance that contains the input data
* @returns {Array|Object|Search|ObjectRef|File|Query} The input data to use in the map/reduce process
* @since 2015.2
*/
//Search created to retrieve contacts of a customer with email type is receive invoices and credits.
//custentity_emial_type is a custom multi select field created in the contact record
function contactSearch(custId) {
var customerSearchObj = search.create({
type: "customer",
filters:
[
["internalid", "anyof", custId],
"AND",
["contact.custentity_emial_type", "anyof", "1"]
],
columns:
[
search.createColumn({name: "internalid", label: "Internal ID"}),
search.createColumn({
name: "email",
join: "contact",
label: "Email"
})
]
});
var searchResultCount = customerSearchObj.runPaged().count;
var contactSearchResult = customerSearchObj.run().getRange({
start: 0,
end: 1000
});
log.debug('SearchCount in contact search', searchResultCount)
return contactSearchResult;
}
const getInputData = (inputContext) => {
try {
log.debug('In Getinput')
//Search created to get invoices and credit memo with open status that are created today
var transactionSearchObj = search.create({
type: "transaction",
filters:
[
// ["datecreated", "on", "today"],
// "AND",
["internalid", "anyof", "232", "2567"],
"AND",
["type", "anyof", "CustInvc", "CustCred"],
"AND",
["status", "anyof", "CustCred:A", "CustInvc:A"],
"AND",
["mainline", "is", "T"],
"AND",
["customer.custentity_emial_type", "anyof", "1"]
],
columns:
[
search.createColumn({name: "internalid", label: "Internal ID"}),
search.createColumn({name: "tranid", label: "Document Number"}),
search.createColumn({name: "trandate", label: "Date"}),
search.createColumn({name: "datecreated", label: "Date Created"}),
search.createColumn({name: "asofdate", label: "As-Of Date"}),
search.createColumn({name: "type", label: "Type"}),
search.createColumn({name: "entity", label: "Name"}),
search.createColumn({name: "memo", label: "Memo"}),
search.createColumn({name: "amount", label: "Amount"}),
search.createColumn({
name: "internalid",
join: "customer",
label: "Internal ID"
}),
search.createColumn({
name: "email",
join: "customer",
label: "Email"
}),
search.createColumn({
name: "altname",
join: "customer",
label: "Name"
})
]
});
var searchResultCount = transactionSearchObj.runPaged().count;
var searchResult = transactionSearchObj.run().getRange({
start: 0,
end: 1000
});
log.debug('SearchCount', searchResultCount)
log.debug('SearchResuLT Data', searchResult)
return searchResult;
} catch (e) {
log.debug('Error@GetInputData', e)
}
}
/**
* Defines the function that is executed when the reduce entry point is triggered. This entry point is triggered
* automatically when the associated map stage is complete. This function is applied to each group in the provided context.
* @param {Object} reduceContext - Data collection containing the groups to process in the reduce stage. This parameter is
* provided automatically based on the results of the map stage.
* @param {Iterator} reduceContext.errors - Serialized errors that were thrown during previous attempts to execute the
* reduce function on the current group
* @param {number} reduceContext.executionNo - Number of times the reduce function has been executed on the current group
* @param {boolean} reduceContext.isRestarted - Indicates whether the current invocation of this function is the first
* invocation (if true, the current invocation is not the first invocation and this function has been restarted)
* @param {string} reduceContext.key - Key to be processed during the reduce stage
* @param {List<String>} reduceContext.values - All values associated with a unique key that was passed to the reduce stage
* for processing
* @since 2015.2
*/
const reduce = (reduceContext) => {
try {
log.debug('In Reduce')
let dataObj = JSON.parse(reduceContext.values)
log.debug('Dataobj', dataObj)
//Load custom record created for getting the author , email subject and email body.
let bulkEmailerCustRecord = record.load({
type: 'customrecord_invoice_rcrd_schdle_hgpl84',
id: 1,
isDynamic: true
})
log.debug('Bulk emailer record', bulkEmailerCustRecord);
//Get author field value from custom record.
var author = bulkEmailerCustRecord.getValue({
fieldId: 'custrecord_author'
});
log.debug('AUTHOR', author)
//Get internal Id of invoice or credit memo
var intId = dataObj.values.internalid[0].value
log.debug('InternalId', intId)
//Get customer email
var rec = dataObj.values["customer.email"]
//Get author from custom record
var sub = bulkEmailerCustRecord.getValue({
fieldId: 'custrecord_author'
});
//Get email template from the custom record
var temp = bulkEmailerCustRecord.getValue({
fieldId: 'custrecord_email_template'
});
log.debug('Templete value',temp)
log.debug('Receipt', rec)
//Get customer id from the search result
var custId = dataObj.values["customer.internalid"][0].value
log.debug('Cust id', custId)
//Calling the contact search to get the contact emails of the customer id passing to it.
var ccContact = contactSearch(custId)
log.debug('Contact search res', ccContact)
var contacts = [];
//Storing the cc contacts to an array
for (var i = 0; i < ccContact.length; i++) {
contacts.push(ccContact[i].getValue({
name: "email",
join: "contact",
label: "Email"
}))
}
log.debug('Contact array', contacts)
//Create pdf of invoice
var invoice = render.transaction({
entityId: parseInt(dataObj.id),
printMode: render.PrintMode.PDF
})
log.debug('Invoice', invoice)
//function to call the suitlet script with the given id and deployment id
function callSuiltelet(intId) {
var suiteletURL = url.resolveScript({
scriptId: 767,
deploymentId: 'customdeploy_jj_sl_invcrdtmemo_view_hgpl',
returnExternalUrl: false,
params: {
'custscriptcust_id': custId,
}
});
return suiteletURL;
}
//Invoking the function to call the suitelet script
var sUrl = callSuiltelet(intId);
log.debug('called suitelet', sUrl)
//Creating a invoice view link for to add in the email body
var emailUrl = "<a href='" + sUrl + "'>Open Invoice Details</a>"
log.debug('Email Url', emailUrl)
//If there is a template in the custom record created, executes the following code
if(temp){
var emailTemplLoad = render.mergeEmail({
templateId: bulkEmailerCustRecord.getValue({
fieldId: 'custrecord_email_template'
})
});
log.debug('EmailTEMPLTE', emailTemplLoad)
var emailSubject = emailTemplLoad.subject;
var emailBody = emailTemplLoad.body;
// email.send({
// author: bulkEmailerCustRecord.getValue({
// fieldId: 'custrecord_author'
// }),
// recipients: dataObj.values["customer.email"],
// cc: contacts,
// subject: emailSubject,
// body: emailBody + '<br><br>' + 'Please find below link to open your Invoice Details' + '<br><br>' + emailUrl,
// attachments: [invoice]
// });
}
//If there is no email template added in the custom record created, excutes the following code with the given email subject and the body
else{
//Code to send email with author from custom record, receipt as customer email, email body from custom record, and with PDF attachment
// email.send({
// author: bulkEmailerCustRecord.getValue({
// fieldId: 'custrecord_author'
// }),
// recipients: dataObj.values["customer.email"],
// cc: contacts,
// subject: bulkEmailerCustRecord.getValue({
// fieldId: 'custrecord_email_subject'
// }),
// body: (bulkEmailerCustRecord.getValue({
// fieldId: 'custrecord_email_body'
// })) + '<br><br>' + 'Please find below link to open your Invoice Details' + '<br><br>' + emailUrl,
// attachments: [invoice],
//
// });
}
} catch (e) {
log.debug('Error@Reduce', e)
}
}
/**
* Defines the function that is executed when the summarize entry point is triggered. This entry point is triggered
* automatically when the associated reduce stage is complete. This function is applied to the entire result set.
* @param {Object} summaryContext - Statistics about the execution of a map/reduce script
* @param {number} summaryContext.concurrency - Maximum concurrency number when executing parallel tasks for the map/reduce
* script
* @param {Date} summaryContext.dateCreated - The date and time when the map/reduce script began running
* @param {boolean} summaryContext.isRestarted - Indicates whether the current invocation of this function is the first
* invocation (if true, the current invocation is not the first invocation and this function has been restarted)
* @param {Iterator} summaryContext.output - Serialized keys and values that were saved as output during the reduce stage
* @param {number} summaryContext.seconds - Total seconds elapsed when running the map/reduce script
* @param {number} summaryContext.usage - Total number of governance usage units consumed when running the map/reduce
* script
* @param {number} summaryContext.yields - Total number of yields when running the map/reduce script
* @param {Object} summaryContext.inputSummary - Statistics about the input stage
* @param {Object} summaryContext.mapSummary - Statistics about the map stage
* @param {Object} summaryContext.reduceSummary - Statistics about the reduce stage
* @since 2015.2
*/
const summarize = (summaryContext) => {
}
return {getInputData, reduce, summarize}
});
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/currentRecord', 'N/record', 'N/redirect', 'N/render', 'N/runtime', 'N/search', 'N/task', 'N/url', 'N/ui/serverWidget', 'N/file'],
/**
* @param{currentRecord} currentRecord
* @param{record} record
* @param{redirect} redirect
* @param{render} render
* @param{runtime} runtime
* @param{search} search
* @param{task} task
* @param{url} url
* @param{serverWidget} serverWidget,
* @param{file} file
*/
(currentRecord, record, redirect, render, runtime, search, task, url, serverWidget, file) => {
/**
* Defines the Suitelet script trigger point.
* @param {Object} scriptContext
* @param {ServerRequest} scriptContext.request - Incoming request
* @param {ServerResponse} scriptContext.response - Suitelet response
* @since 2015.2
*/
//Customer search created with criteria considering top level parent also to get details of transactions
function invoiceCreditMemoSearch(parms) {
var transactionSearchObj = search.create({
type: "customer",
filters:
[
[["toplevelparent.internalid", "anyof", parms], "OR", ["internalid", "anyof", parms]],
"AND",
["transaction.type", "anyof", "CustInvc", "CustCred"],
"AND",
["transaction.status", "anyof", "CustCred:A", "CustInvc:A"],
"AND",
["transaction.mainline", "is", "T"]
],
columns:
[
search.createColumn({name: "internalid", label: "Internal ID"}),
search.createColumn({name: "altname", label: "Name"}),
search.createColumn({
name: "tranid",
join: "transaction",
label: "Document Number"
}),
search.createColumn({
name: "trandate",
join: "transaction",
label: "Date"
}),
search.createColumn({
name: "subsidiary",
join: "transaction",
label: "Subsidiary"
}),
search.createColumn({
name: "duedate",
join: "transaction",
label: "Due Date/Receive By"
}),
search.createColumn({
name: "daysoverdue",
join: "transaction",
label: "Days Overdue"
}),
search.createColumn({
name: "amount",
join: "transaction",
label: "Amount"
}),
search.createColumn({
name: "amountremaining",
join: "transaction",
label: "Amount Remaining"
}),
search.createColumn({
name: "internalid",
join: "transaction",
label: "Internal ID"
}),
search.createColumn({
name: "type",
join: "transaction",
label: "Type"
})
]
});
var searchResultCount = transactionSearchObj.runPaged().count;
var searchResult = transactionSearchObj.run().getRange({
start: 0,
end: 1000
});
log.debug('SearchCount', searchResultCount)
log.debug('SearchResuLT Data', searchResult)
return searchResult;
}
const onRequest = (scriptContext) => {
try {
//Get Method
if (scriptContext.request.method === 'GET') {
var form = serverWidget.createForm({title: 'INVOICE DETAILS'});
// var scriptObj = runtime.getCurrentScript();
// log.debug('SCRIPT: ', scriptObj);
// var invoiceId = scriptObj.getParameter({name: 'custscriptinvoice_id'});
// log.debug('Invoice Id s',invoiceId)
var parms = scriptContext.request.parameters.custscriptcust_id;
log.debug('Invoice id from url', parms)
//Sublist is added to the form
var sublistDet = form.addSublist({
id: 'custpage_det',
type: serverWidget.SublistType.LIST,
label: 'Invoice Details'
});
//Adding the body fields to the suitelet
var date = sublistDet.addField({
id: 'custpage_date',
label: 'DATE',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var docnum = sublistDet.addField({
id: 'custpage_docnumber',
label: 'DOCUMENT NUMBER',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var sub = sublistDet.addField({
id: 'custpage_subsidiary',
label: 'SUBSIDIARY',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var name = sublistDet.addField({
id: 'custpage_name',
label: 'CUSTOMER NAME',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var duedate = sublistDet.addField({
id: 'custpage_duedate',
label: 'DUE DATE',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var amount = sublistDet.addField({
id: 'custpage_amount',
label: 'AMOUNT',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var amtrem = sublistDet.addField({
id: 'custpage_amountf',
label: 'AMOUNT REMAINING',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
var dueday = sublistDet.addField({
id: 'custpage_duedays',
label: 'DUE DAYS',
type: serverWidget.FieldType.TEXT,
align: serverWidget.LayoutJustification.RIGHT
});
//Adding the download option for each invoices
var downlink = sublistDet.addField({
id: 'custpage_down',
type: serverWidget.FieldType.URL,
label: ' '
}).linkText = 'Download'
//Invoking the customer search created inside a function and passing the customer id as a parameter to it.
var searchResult = invoiceCreditMemoSearch(parms);
log.debug('Searchresuslt count', searchResult.length)
log.debug('SearchResult', searchResult)
//Clling the client script
form.clientScriptFileId = 5913;
//Iterating through the search result.
for (var i = 0; i < searchResult.length; i++) {
//If the type of transaction is Invoice then following URl is taken
if ((searchResult[i].getValue({
name: "type",
join: "transaction",
label: "Type"
})) === 'CustInvc') {
var urlVal = "https://6687177-sb1.app.netsuite.com/app/accounting/print/hotprint.nl?regular=T&sethotprinter=T&formnumber=148&trantype=custinvc&&id=" + searchResult[i].getValue({
name: "internalid",
join: "transaction",
label: "Internal ID"
}) + "&label=Invoice&printtype=transaction"
}
//If the transaction type is a credit memo, then the following URL is taken
else {
var urlVal = "https://6687177-sb1.app.netsuite.com/app/accounting/print/hotprint.nl?regular=T&sethotprinter=T&formnumber=139&trantype=custcred&&id=" + searchResult[i].getValue({
name: "internalid",
join: "transaction",
label: "Internal ID"
}) + "&label=Credit+Memo&printtype=transaction&whence="
}
//Setting the URL value to the download link
sublistDet.setSublistValue({
id: 'custpage_down',
line: i,
value: urlVal
});
//Setting the Name
sublistDet.setSublistValue({
id: 'custpage_name',
line: i,
value: (searchResult[i].getValue({name: "altname", label: "Name"}
)) || " "
});
//Setting the date
sublistDet.setSublistValue({
id: 'custpage_date',
line: i,
value: (searchResult[i].getValue({
name: "trandate",
join: "transaction",
label: "Date"
}
)) || " "
});
//Setting the document number
sublistDet.setSublistValue({
id: 'custpage_docnumber',
line: i,
value: (searchResult[i].getValue({
name: "tranid",
join: "transaction",
label: "Document Number"
}
)) || " "
});
//Setting the Amount
sublistDet.setSublistValue({
id: 'custpage_amount',
line: i,
value: (searchResult[i].getValue({
name: "amount",
join: "transaction",
label: "Amount"
}
)) || " "
});
//Setting the due date
sublistDet.setSublistValue({
id: 'custpage_duedate',
line: i,
value: (searchResult[i].getValue({
name: "duedate",
join: "transaction",
label: "Due Date/Receive By"
}
)) || " "
});
//Setting the amount remaining value
sublistDet.setSublistValue({
id: 'custpage_amountf',
line: i,
value: (searchResult[i].getValue({
name: "amountremaining",
join: "transaction",
label: "Amount Remaining"
}
)) || " "
});
//Setting the subsidiary
sublistDet.setSublistValue({
id: 'custpage_subsidiary',
line: i,
value: (searchResult[i].getText({
name: "subsidiary",
join: "transaction",
label: "Subsidiary"
}
)) || " "
});
//Setting the due days
sublistDet.setSublistValue({
id: 'custpage_duedays',
line: i,
value: (searchResult[i].getValue({
name: "daysoverdue",
join: "transaction",
label: "Days Overdue"
}
)) || " "
});
}
//Adding a button to download the CSV file, the function here invoked is defined in the client script
form.addButton({
id: 'downcsv',
label: 'Download CSV',
functionName: 'csvSave'
});
scriptContext.response.writePage(form);
}
} catch (e) {
log.debug('Error@On Request', e)
}
}
return {onRequest}
});