Map/Reduce scripts in NetSuite is a scheduled script type that can work on large set of data. It is intended for parallel bulk data operations. They offer more power and benefits over the standard Scheduled scripts.
This is a sample code of a map-reduce script for sending emails to all customers with pending invoices that are due before the start of current month. A csv file attachment is added with the email showing a list of their overdue invoices with details such as due date, amount etc.
Note: The id of admin and folder to keep files created are hard-coded and need to be changed.
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/email', 'N/file', 'N/search'],
/**
* @param{email} email
* @param{file} file
* @param{search} search
*/
(email, file, search) => {
/**
* 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
*/
const getInputData = (inputContext) => {
try {
log.debug({title:'getInputData',details:inputContext});
return search.create({
type: "invoice",
filters:
[
["type","anyof","CustInvc"],
"AND",
["formuladate: {duedate}","before","startofthismonth"],
"AND",
["mainline","is","T"],
"AND",
["email","isnotempty",""],
"AND",
["amountremaining","greaterthan","0.00"]
],
columns:
[
search.createColumn({
name: "altname",
join: "customer",
label: "Name"
}),
search.createColumn({
name: "email",
join: "customer",
label: "Email"
}),
search.createColumn({name: "tranid", label: "Document Number"}),
search.createColumn({name: "duedate", label: "Due Date/Receive By"}),
search.createColumn({name: "amountremaining", label: "Amount Remaining"}),
search.createColumn({name: "salesrep", label: "Sales Rep"}),
search.createColumn({
name: "email",
join: "salesRep",
label: "Sales Rep Email"
}),
search.createColumn({
name: "internalid",
join: "salesRep",
label: "Sales Rep Internal Id"
})
]
});
} catch (err) {
log.debug("error@getInputData", err)
}
}
/**
* Defines the function that is executed when the map entry point is triggered. This entry point is triggered automatically
* when the associated getInputData stage is complete. This function is applied to each key-value pair in the provided
* context.
* @param {Object} mapContext - Data collection containing the key-value pairs to process in the map stage. This parameter
* is provided automatically based on the results of the getInputData stage.
* @param {Iterator} mapContext.errors - Serialized errors that were thrown during previous attempts to execute the map
* function on the current key-value pair
* @param {number} mapContext.executionNo - Number of times the map function has been executed on the current key-value
* pair
* @param {boolean} mapContext.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} mapContext.key - Key to be processed during the map stage
* @param {string} mapContext.value - Value to be processed during the map stage
* @since 2015.2
*/
const map = (mapContext) => {
try {
var data = JSON.parse(mapContext.value);
//log.debug({title: 'data', details: data});
var customer_email = data.values['email.customer'];
mapContext.write({
key: customer_email,
value: data.values
});
} catch (err) {
log.debug("error@map", err)
}
}
/**
* 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
*/
function ConvertToCSV(objArray) {
var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
var str = '';
for (var i = 0; i < array.length; i++) {//traversing each object element in object array
var line = '';
log.debug("array element in csv conversion", array[i]);
for (var index in array[i]) {
log.debug("index",index);
/*if (index=="salesrep" || index=="email.salesRep" || index=="internalid.salesRep")
continue;*/
if (line != '') line += ','
line += array[i][index];
}
str += line + '\r\n';
}
return str;
}
const reduce = (reduceContext) => {
try {
log.debug({title:'ReduceContext',details:reduceContext.values});
var emailId = reduceContext.key;
var values = reduceContext.values.map(JSON.parse);
var id_of_sender, csvContent, temp, temp1;//initialized here to get block scope till email.send function
values.map(function(invoice) {
if ((invoice['internalid.salesRep']) === '' || (invoice['internalid.salesRep'])==null || (invoice['internalid.salesRep'])===undefined){//checking if customer has a sales rep
id_of_sender = 736;//Admins id is hardcoded
}
else{
temp = invoice['internalid.salesRep'];//returns object with value like {value:"734",text:"734"}
temp1 = Object.values(temp)[1];//takes value at index 1
id_of_sender = Number(temp1);//string to number
}
});
//JSON to CSV Converter to create attchment csv file
var dataObj = JSON.stringify(values,['altname.customer', 'email.customer', 'tranid', 'duedate', 'amountremaining']);//only the required fields are selected
log.debug("Data obj", dataObj);
csvContent = ConvertToCSV(dataObj);
log.debug("CSV file data", csvContent);
//log.debug({title: 'invoice data for csv',details: invoiceData});
var csvFile = file.create({
name: 'pending_invoice.csv',
contents: 'Customer_name,Customer_email,Invoice_no,Due_date,Remaining_amount\r\n'+ csvContent,
folder: 419,//id of demo_files folder
fileType: 'CSV'
});//creates a csv file in the folder specified
var fileId = csvFile.save();
var fileObj = file.load({
id: fileId
});
var emailBody = "Greetings Customer,\nPlease pay your pending invoices. List of pending invoices are attached as a csv document\n";
email.send({
author: id_of_sender,//id of admin
recipients: emailId,
subject: 'Invoice Due Reminder New',
body: emailBody,//testing with email body instead of attachment
attachments: [fileObj]
//fileObj is the object to file is loaded
});
reduceContext.write({key: emailId});
log.debug("one reduce stage iteration completed")
} catch (err) {
log.debug("Error in reduce", err)
}
}
/**
* 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) => {
log.debug({title:'summarize',details:summaryContext})
}
return {getInputData, map, reduce, summarize}
});