Requirement:
When an item fulfillment status becomes ‘shipped’, the item fulfillment should get automatically invoiced, but only the inventory item should be invoiced.
Solution:
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
/**
* Script Description
* Auto invoicing will be performed for the shipped IFs that were shipped in last 24 hours
*
*/
/*******************************************************************************
* Matterport | MATT-1 | Auto Invoicing For Inventory Items
* **************************************************************************
*
* Date: 05/11/2023
* Production movement:
*
* Author: Jobin & Jismi IT Services LLP
*
*
* REVISION HISTORY
*
* Revision
*
******************************************************************************/
define(['N/email', 'N/error', 'N/file', 'N/render', 'N/record', 'N/runtime', 'N/search', 'N/url', 'N/config'],
/**
* @param{email} email
* @param{error} error
* @param{file} file
* @param{render} render
* @param{record} record
* @param{runtime} runtime
* @param{search} search
*/
(email, error, file, render, record, runtime, search, url, config) => {
"use strict";
/**
* DATASETS is a constant object that has different functions used in the script as values
*/
const DATASETS = {
/**
* Function to fetch the IFs that were closed in last 24 hours
* @returns {Object} itemfulfillmentSearchObj
*/
fetchItemFulfillmentsShippedLastDay: function () {
try {
let itemfulfillmentSearchObj = search.create({
type: "itemfulfillment",
filters:
[
["type", "anyof", "ItemShip"],
"AND",
["mainline", "is", "T"],
"AND",
["shipping", "is", "F"],
"AND",
["cogs", "is", "F"],
"AND",
["taxline", "is", "F"],
"AND",
["systemnotes.field", "anyof", "TRANDOC.KSTATUS"],
"AND",
["systemnotes.newvalue", "is", "Shipped"],
"AND",
["systemnotes.date", "within", "hoursago24", "secondsago0"],
"AND",
["createdfrom.type", "anyof", "SalesOrd"]
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" }),
search.createColumn({ name: "createdfrom", label: "Created From" })
]
});
let searchResultCount = itemfulfillmentSearchObj.runPaged().count;
let returnVal = searchResultCount > 0 ? itemfulfillmentSearchObj : [];
return returnVal;
} catch (e) {
log.error("Error @ fetchItemFulfillmentsShippedLastDay", e.message);
this.createFile("" + ',' + "" + ',' + e.message + ',' + '\r\n');
return [];
}
},
/**
* Function to check if there exist any invoice for this sales order that was manually invoices, or by any other third party integration
* @param {number} soId
* @returns {Boolean}
*/
checkIfAnyManualInvoiceExist: function (soId) {
try {
let salesorderSearchObj = search.create({
type: "invoice",
filters:
[
["type", "anyof", "CustInvc"],
"AND",
["mainline", "is", "T"],
"AND",
["custbody_jj_related_fulfillment_matt1", "anyof", "@NONE@"],
"AND",
["createdfrom.internalid", "anyof", soId]
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" })
]
});
let searchResultCount = salesorderSearchObj.runPaged().count;
if (searchResultCount > 0) {
return true;
}
else {
return false;
}
} catch (e) {
log.error("Error @ checkIfAnyManualInvoiceExist", e.message);
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return false;
}
},
/**
* The function that processes each IF for creating auto invoice
* @returns {object}
*/
processItemFulFillment: function () {
try {
//check if already an invoice is created for this IF
let invoiceExists = DATASETS.invoiceExistsForItemFulfillment(MAIN.ifRecord.getValue({ fieldId: 'id' }));
if (!DATASETS.checkForParameter(invoiceExists)) {
return DATASETS.createInvoice(
MAIN.ifRecord.getValue({
fieldId: 'createdfrom'
}),
MAIN.ifRecord.getValue({
fieldId: 'id'
}),
DATASETS.createMap()
);
} else {
return { status: 'failure', reason: "An invoice for this itemfulfillment already exists" }
}
} catch (e) {
log.error("Error @ processItemFulFillment", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return { status: 'failure', reason: e.message };
}
},
/**
* Function to create invoice for shipped item fulfillments
* @param {number} salesOrdId
* @param {number} ItemFulfillmentId
* @param {number} mapArray
* @returns {object}
*/
createInvoice: function (salesOrdId, ItemFulfillmentId, mapArray) {
try {
if (mapArray.length < 1) { //If there is no Mapped Items, return false
return false;
}
const SKIP_ITEM_TYPE = {
'Group': 'Group',
'EndGroup': 'EndGroup'
};
let invoiceRecord = record.transform({
fromType: record.Type.SALES_ORDER,
fromId: salesOrdId,
toType: record.Type.INVOICE,
isDynamic: true,
});
//set the body fields
invoiceRecord.setValue({
fieldId: 'custbody_jj_related_fulfillment_matt1',
value: ItemFulfillmentId,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'approvalstatus',
value: "2",
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'shipcarrier',
value: MAIN.ifShippingCarrier,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'shipmethod',
value: MAIN.ifShippingMethod,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'shippingcost',
value: MAIN.ifShippingCost,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'trandate',
value: MAIN.ifDate,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'postingperiod',
value: MAIN.postingPeriod,
ignoreFieldChange: true
});
invoiceRecord.setValue({
fieldId: 'terms',
value: MAIN.terms,
ignoreFieldChange: true
});
let dueDate = invoiceRecord.getValue({
fieldId: 'duedate',
ignoreFieldChange: true
});
if (!dueDate) {
invoiceRecord.setValue({
fieldId: 'duedate',
value: MAIN.ifDate,
ignoreFieldChange: true
});
}
invoiceRecord.setValue({
fieldId: 'tobeemailed',
value: false,
ignoreFieldChange: true
});
let lineCount = invoiceRecord.getLineCount({
sublistId: 'item'
});
let mappedLine, previousItemType;
for (let index = lineCount - 1; index >= 0; index--) {
invoiceRecord.selectLine({
sublistId: 'item',
line: index
});
let lineItemType = invoiceRecord.getCurrentSublistValue
({
sublistId: 'item',
fieldId: 'itemtype',
});
if ((lineItemType == 'Group') && (previousItemType == 'EndGroup')) {
invoiceRecord.removeLine({
sublistId: 'item',
line: index,
ignoreRecalc: false
});
continue;
}
else if (SKIP_ITEM_TYPE[lineItemType]) {
previousItemType = lineItemType;
continue;
}
//Filtering Out the Items using mapArray
//The below code is to filter out the inventory item lines in IF from the item lines in SO
mappedLine = mapArray.find(function (eachValue) {
return parseInt(eachValue.line.value) == parseInt(invoiceRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'orderline',
}));
}) || false;
if (this.checkForParameter(mappedLine && mappedLine?.quantity?.value)) {
//invoice only the qty fulfilled.
invoiceRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: mappedLine.quantity.value
});
if (this.checkForParameter(mappedLine?.itemfxamount?.value)) {
let amount = ((Number(mappedLine.itemfxamount.value) / Number(mappedLine.soQty.value)) * mappedLine.quantity.value).toFixed(2);
invoiceRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'amount',
value: amount
});
}
let lineRate = invoiceRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate'
});
let lineAmount = invoiceRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'amount'
});
if (!DATASETS.checkForParameter(lineRate) && DATASETS.checkForParameter(lineAmount)) {
let lineQuantity = invoiceRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});
let unitRate = Number(lineAmount) / Number(lineQuantity);
invoiceRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate',
value: Number(unitRate).toFixed(2)
});
}
invoiceRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'location',
value: mappedLine.location.value
});
if (this.checkForParameter(mappedLine?.istaxable?.value)) {
invoiceRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'istaxable',
value: mappedLine.istaxable.value
});
}
invoiceRecord.commitLine({
sublistId: 'item'
});
previousItemType = lineItemType;
}
else {
invoiceRecord.removeLine({
sublistId: 'item',
line: index,
ignoreRecalc: false
});
}
}
//check if the invoice has atleast one line item
lineCount = invoiceRecord.getLineCount({
sublistId: 'item'
});
if (lineCount > 0) {
let invoiceTotal = invoiceRecord.getValue({
fieldId: 'total',
});
let recordId = invoiceRecord.save({
enableSourcing: false,
ignoreMandatoryFields: true
});
if (recordId) {
record.submitFields({
type: record.Type.ITEM_FULFILLMENT,
id: ItemFulfillmentId,
values: {
custbody_jj_relatedinvoice_matt1: recordId,
custbody_jj_autoinvoice_created_matt1: true
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
let customerFields = search.lookupFields({
type: search.Type.CUSTOMER,
id: MAIN.customer,
columns: ['custentity_cus_submit_inv_to_portal', 'email']
})
let sendInvoiceToCustomer = customerFields.custentity_cus_submit_inv_to_portal;
let customerEmail = customerFields.email;
//send the invoce email to customer
DATASETS.sendInvoiceToCustomer(invoiceTotal, sendInvoiceToCustomer, customerEmail, recordId)
return { status: 'success', reason: recordId };
} else {
return { status: 'failure', reason: 'Invoice is not created' }
}
}
} catch (e) {
log.error("Error @ Create invoice", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return { status: 'failure', reason: e.message }
}
},
/**
* Get Sublist fields and their values
*
* @param {Object} recordObj Initialised Object reference to record
* @param {String} sublistId sublistId on the Object
* @return {Array[Object]}
*/
getSublist: function (recordObj, sublistId) {
try {
let result = [];
let sublistFields = recordObj.getSublistFields(sublistId);
let lineCount = recordObj.getLineCount({
sublistId: sublistId
});
for (let line = 0; line < lineCount; line++) {
result.push(
sublistFields.reduce(function (a, c) {
try {
a[c] = {
value: recordObj.getSublistValue({
sublistId: sublistId,
fieldId: c,
line: line
}),
text: recordObj.getSublistText({
sublistId: sublistId,
fieldId: c,
line: line
})
};
return a;
} catch (er) {
}
return a;
}, {})
);
}
return result;
} catch (e) {
log.error("Error @ getSublist", e.message);
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return [];
}
},
/**
* Function to check if an invoice already exists or not for an item fulfillment
* @param {int} id
* @returns {boolean}
*/
invoiceExistsForItemFulfillment: function (id) {
try {
let invoiceSearchObj = search.create({
type: "invoice",
filters:
[
["type", "anyof", "CustInvc"],
"AND",
["mainline", "is", "T"],
"AND",
["shipping", "is", "F"],
"AND",
["taxline", "is", "F"],
"AND",
["cogs", "is", "F"],
"AND",
["custbody_jj_related_fulfillment_matt1", "anyof", id]
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" })
]
});
if (invoiceSearchObj.runPaged().count > 0) {
return true;
} else {
return false;
}
} catch (e) {
log.error("Error @ invoiceExistsForItemFulfillment", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return false;
}
},
/**
* Function to create a mapping between the item lines of IF and SO
* @returns {Array}
*/
createMap: function () {
try {
let mapObj = DATASETS.lineMap({
data: DATASETS.getSublist(MAIN.ifRecord, 'item').reduce(function (a, v) {
if ((Number(v.kitmemberquantityfactor.value) < 1) && (v.itemtype.value == 'InvtPart' || (v.itemtype.value == 'Assembly'))) {
a.push({
item: v.item,
quantity: v.quantity,
orderline: v.orderline,
location: v.location,
itemfxamount: v.itemfxamount
});
}
return a;
}, []),
mapKey: 'orderline'
}, {
data: DATASETS.getSublist(MAIN.soRecord, 'item').reduce(function (a, v) {
a.push({
item: v.item,
itemtype: v.itemtype,
groupsetup: v.groupsetup,
ingroup: v.ingroup,
rate: v.rate,
soQty: v.quantity,
description: v.description,
line: v.line,
qtyInvoiced: v.quantitybilled,
qtyFulfilled: v.quantityfulfilled,
istaxable: v.istaxable
});
return a;
}, []),
mapKey: 'line'
});
return mapObj;
} catch (e) {
log.error("Error @ createMap", e.message);
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return [];
}
},
/**
* Function to map Item FulFillment Item Lines To Sales Order Item Lines
* @param {object} IfLines
* @param {object} SoLines
* @returns {Array}
*/
lineMap: function (IfLines, SoLines) {
try {
//Map items between Item Fulfillment and Sales Order, contains only Item FulFillment items
let mappedItems = IfLines.data.map(function (currentValue) {
return Object.assign({}, currentValue,
SoLines.data.find(function (eachValue) {
return currentValue[IfLines.mapKey].value == eachValue[SoLines.mapKey].value;
}) || {}
);
});
return mappedItems;
} catch (e) {
log.error("Error @ lineMap", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
return [];
}
},
/**
* Function to send email to client listing the errors occurred in execution
* @param {Array} errors
* @param {Array} invoiceExistsArray
*/
sendErrorEmail: function (errors, invoiceExistsArray) {
try {
let emailBody = '<div>Hello there,<br/><br/>';
let itemFulFillmentPath, salesOrderPath, ifUrl, soUrl;
if (errors.length > 0) {
//create the table listing IFs failed to create invoice
emailBody += 'Attached is a list of fulfillments for which the script was unable to generate automated invoices. Please review the list and take appropriate action to ensure that invoices are created for these fulfillments.<br/> <br /></div> <table style=" border-collapse: collapse;"><tr style="height:50px;"> <th style="border:solid black; background-color:#45c0e5; font-weight: bold; width:170px; text-align:center;">ITEM FULFILLMENT</th><th style="border:solid black; background-color:#45c0e5; font-weight: bold; width:170px; text-align:center;">SALES ORDER</th><th style="border:solid black; background-color:#45c0e5; font-weight: bold; width:170px; text-align:center;">REASON</th></tr>';
for (let i = 0; i < errors.length; i++) {
itemFulFillmentPath = url.resolveRecord({
recordType: record.Type.ITEM_FULFILLMENT,
recordId: errors[i].ifId,
isEditMode: false
});
salesOrderPath = url.resolveRecord({
recordType: record.Type.SALES_ORDER,
recordId: errors[i].soId,
isEditMode: false
});
ifUrl = '<a href="' + itemFulFillmentPath + '">' + errors[i].ifNumber + '</a>';
soUrl = '<a href="' + salesOrderPath + '">' + errors[i].soNumber + '</a>';
emailBody += '<tr style="height:40px;"><td style="border:solid black; width:170px; text-align:center;">' + ifUrl + '</td><td style="border:solid black; width:170px; text-align:center;">' + soUrl + '</td><td style="border:solid black; width:170px; text-align:center;">' + errors[i].reason + '</td></tr>';
}
emailBody += '</table><br/><br/>';
}
if (invoiceExistsArray.length > 0) {
emailBody += 'Below attached is a list of fulfillments for which the auto invoicing was not performed. This is because the item fulfillment or sales order contains a manually processed invoice.<br/> <br /></div> <table style=" border-collapse: collapse;"><tr style="height:50px;"> <th style="border:solid black; background-color:#45c0e5; font-weight: bold; width:170px; text-align:center;">ITEM FULFILLMENT</th><th style="border:solid black; background-color:#45c0e5; font-weight: bold; width:170px; text-align:center;">SALES ORDER</th></tr>';
for (let j = 0; j < invoiceExistsArray.length; j++) {
itemFulFillmentPath = url.resolveRecord({
recordType: record.Type.ITEM_FULFILLMENT,
recordId: invoiceExistsArray[j].ifId,
isEditMode: false
});
salesOrderPath = url.resolveRecord({
recordType: record.Type.SALES_ORDER,
recordId: invoiceExistsArray[j].soId,
isEditMode: false
});
ifUrl = '<a href="' + itemFulFillmentPath + '">' + invoiceExistsArray[j].ifNumber + '</a>';
soUrl = '<a href="' + salesOrderPath + '">' + invoiceExistsArray[j].soNumber + '</a>';
emailBody += '<tr style="height:40px;"><td style="border:solid black; width:170px; text-align:center;">' + ifUrl + '</td><td style="border:solid black; width:170px; text-align:center;">' + soUrl + '</td></tr>';
}
emailBody += '</table><br/><br/>';
}
emailBody += '<div>Thank You.';
email.send({
author: 1214178,
recipients: ['AR@matterport.com'], // Matterport AR
subject: 'Invoice Cannot be Created',
body: emailBody,
relatedRecords: {
entityId: 1214178
}
});
} catch (e) {
log.error("Error @ sendErrorEmail", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
}
},
/**
* Function to send created invoices
* @param {int} total
* @param {Array} sendEmail
* @param {string} customerEmail
* @param {int} invoiceId
*/
sendInvoiceToCustomer: function (total, sendEmail, customerEmail, invoiceId) {
try {
if ((total > 0) && (!DATASETS.checkForParameter(sendEmail[0]) || (sendEmail[0] && sendEmail[0].text == 'No'))) {
//recipient should the email in SO. If not available, then it should be the current user.
let recipient = MAIN.soRecord.getValue({ fieldId: 'email' }) ? MAIN.soRecord.getValue({ fieldId: 'email' }) : customerEmail;
let transactionFile = render.transaction({
entityId: invoiceId,
printMode: render.PrintMode.PDF
});
let companyDetails = config.load({
type: config.Type.COMPANY_INFORMATION
});
let companyName = companyDetails.getValue({ fieldId: 'companyname' });
let invoiceDocNumber = search.lookupFields({
type: search.Type.INVOICE,
id: invoiceId,
columns: ['tranid']
}).tranid;
//send invoice
if (recipient) {
email.send({
author: 1214178,
recipients: recipient,
subject: companyName + ': Invoice #' + invoiceDocNumber,
body: 'Please find the attached invoice.',
attachments: [transactionFile],
relatedRecords: {
entityId: 1214178,
transactionId: invoiceId
}
});
}
}
} catch (e) {
log.error("Error @ sendInvoiceToCustomer", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
}
},
/**
* Function to create error text file in filecabinet to store all the errors occured in script execution to create a CSV file for errors
* Since there are nested functions in this script, it's difficult to write any occurred errors directly to summarize, and this is why a text file is created to store the occurred errors.
* @param {string} error
*/
createFile(error) {
try {
//check if already text file exists or not
let folderSearchObj = search.create({
type: "folder",
filters:
[
["name","is","JJ-MATT-1-Errors"]
],
columns:
[
search.createColumn({ name: "numfiles", label: "# of Files" }),
search.createColumn({ name: "internalid", label: "Internal ID" }),
search.createColumn({
name: "internalid",
join: "file",
label: "Internal ID"
})
]
});
let searchResultCount = folderSearchObj.runPaged().count;
let createdFile;
if (searchResultCount > 0) {
let textFile, folderInternalId, numOfFiles, fileObj, fileContent;
folderSearchObj.run().each(function (result) {
textFile = result.getValue({
name: "internalid",
join: "file",
label: "Internal ID"
});
folderInternalId = result.getValue({ name: "internalid", label: "Internal ID" })
numOfFiles = result.getValue({ name: "numfiles", label: "# of Files" })
return false;
})
if (numOfFiles > 0) {
fileObj = file.load({
id: textFile
});
fileContent = fileObj.getContents();
fileContent += error;
createdFile = file.create({
name: 'Error.txt',
fileType: file.Type.PLAINTEXT,
contents: fileContent,
folder: folderInternalId
});
createdFile.save();
}
else {
createdFile = file.create({
name: 'Error.txt',
fileType: file.Type.PLAINTEXT,
contents: error,
folder: folderInternalId
});
createdFile.save();
}
}
} catch (e) {
log.error("Error @ createFile", e.message);
}
},
/**
* Function to create a CSV file for the errors
* @param {string} errors
*/
createFileForErrors: function (errors) {
try {
//get the text file that containing all the errors.
let textFileFolder = '/SuiteScripts/Jobin and Jismi IT Services LLP/MATT-1/JJ-MATT-1-Errors';
let folderSearchObj = search.create({
type: "file",
filters:
[
["formulatext: {folder}", "is", "JJ-MATT-1-Errors"] //prod
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" })
]
});
let searchResultCount = folderSearchObj.runPaged().count;
if (searchResultCount > 0) {
let fileId;
folderSearchObj.run().each(function (result) {
fileId = result.getValue({ name: "internalid", label: "Internal ID" })
return true;
})
//load the file and get contents
let fileObj = file.load({
id: fileId
});
let fileContents = fileObj.getContents();
let titleArray = ["Item Fulfillment Number", "Sales Order Number", "Error Reason"];
let csvFileData = titleArray.toString() + '\r\n';
//append errors to csv file
csvFileData += fileContents;
csvFileData += '\r\n';
//filename
let today = new Date(), dd = String(today.getDate()).padStart(2, '0'), mm = String(today.getMonth() + 1).padStart(2, '0'), yyyy = today.getFullYear();
let todayDate = mm + '-' + dd + '-' + yyyy;
//search to get the internal id of folder for storing the csv file
let folderSearch = search.create({
type: "folder",
filters:
[
["name","is","Errors in Auto Invoicing"]
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" })
]
});
let resultCount = folderSearch.runPaged().count;
if (resultCount > 0) {
let folderId;
folderSearch.run().each(function (result) {
folderId = result.getValue({ name: "internalid", label: "Internal ID" })
return false;
})
let createdFile = file.create({
name: 'error_' + todayDate + '.csv',
fileType: file.Type.CSV,
contents: csvFileData,
description: 'Error information',
folder: folderId,
encoding: file.Encoding.UTF8
});
createdFile.save();
}
file.delete({
id: fileId
});
}
} catch (e) {
log.error("Error @ createFileForErrors", e.message)
}
},
/**
* @description the function to check whether a value exists in parameter
* @param parameter -passing parameter
* @param parameterName - passing parameter name
* @returns{Boolean}
*/
checkForParameter: function (parameter) {
try {
if (parameter !== "" && parameter !== null && parameter !== undefined && parameter !== false && parameter !== "null" && parameter !== "undefined" && parameter !== " " && parameter !== 'false' && parameter != 0) {
return true;
}
else {
return false;
}
} catch (e) {
log.error("Error @ checkForParameter", e.message)
return false;
}
}
}
const MAIN = {
/**
* Function to load the IF and its created from sales order
* @param {Object} itemFulfillmentObj
* @param {Object} reduceContext
*/
initialize: function (itemFulfillmentObj, reduceContext) {
try {
this.reduceContext = reduceContext;
this.ifRecord = record.load({
type: record.Type.ITEM_FULFILLMENT,
id: itemFulfillmentObj.id,
isDynamic: true
});
this.soRecord = record.load({
type: record.Type.SALES_ORDER,
id: itemFulfillmentObj.values.createdfrom.value,
isDynamic: true
});
this.customer = this.soRecord.getValue({ fieldId: 'entity' });
this.ifShippingCarrier = this.ifRecord.getValue({ fieldId: 'shipcarrier' });
this.ifShippingMethod = this.ifRecord.getValue({ fieldId: 'shipmethod' });
this.ifShippingCost = this.ifRecord.getValue({ fieldId: 'shippingcost' });
this.ifDate = this.ifRecord.getValue({ fieldId: 'trandate' });
this.postingPeriod = this.ifRecord.getValue({ fieldId: 'postingperiod' });
this.terms = this.soRecord.getValue({ fieldId: 'terms' });
} catch (e) {
log.error("Error @ initialize", e.message)
this.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
}
},
/**
* 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
*/
getInputData: function (inputContext) {
try {
//fetch the IFS closed in last 24 hours to process the auto invoicing
return DATASETS.fetchItemFulfillmentsShippedLastDay()
} catch (e) {
log.error("Error @ getInputData", e.message);
return [];
}
},
/**
* 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
*/
reduce: function (reduceContext) {
try {
let itemFulfillmentObj = JSON.parse(reduceContext.values)
let getRecords = MAIN.initialize(itemFulfillmentObj, reduceContext);
//if any invoice is created manually for this sales order, then we do not need to create auto invoices for any of the IF of this SO
let manualInvoices = DATASETS.checkIfAnyManualInvoiceExist(MAIN.soRecord.id)
if (DATASETS.checkForParameter(manualInvoices)) {
if (!MAIN.ifRecord.getValue({ fieldId: 'custbody_jj_relatedinvoice_matt1' })) {
reduceContext.write({
key: "invoiceExists",
value: { soNumber: MAIN.soRecord.getValue({ fieldId: 'tranid' }), ifNumber: MAIN.ifRecord.getValue({ fieldId: 'tranid' }), soId: MAIN.soRecord.id, ifId: MAIN.ifRecord.id }
})
return false;
}
}
if (MAIN.soRecord.getValue({ fieldId: 'status' }) == 'Billed') {
return false;
}
//Process ItemFulFilment
let createInvoice = DATASETS.processItemFulFillment();
if ((createInvoice.status == 'failure') && (createInvoice.reason)) {
reduceContext.write({
key: "Errors",
value: { reason: createInvoice.reason, soNumber: MAIN.soRecord.getValue({ fieldId: 'tranid' }), ifNumber: MAIN.ifRecord.getValue({ fieldId: 'tranid' }), soId: MAIN.soRecord.id, ifId: MAIN.ifRecord.id }
})
}
} catch (e) {
log.error("Error @ reduce", e.message);
DATASETS.createFile(MAIN.ifRecord.getValue({ fieldId: 'tranid' }) + ',' + MAIN.soRecord.getValue({ fieldId: 'tranid' }) + ',' + e.message + ',' + '\r\n');
}
},
/**
* 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
*/
summarize: function (summaryContext) {
try {
let errorArray = [], invoiceExistsArray = [], scriptErrors = [];
summaryContext.output.iterator().each(function (key, value) {
if ((key == "Errors") && JSON.parse(value)) { //need to send email to client listing the occurred errors.
errorArray.push(JSON.parse(value));
}
else if ((key == "invoiceExists") && JSON.parse(value)) { //need to send email to client listing the occurred errors.
invoiceExistsArray.push(JSON.parse(value));
}
else if ((key == "errorInScript") && JSON.parse(value)) { //need to send email to client listing the occurred errors.
scriptErrors.push(JSON.parse(value));
}
return true;
})
//send error email
if (errorArray.length > 0 || invoiceExistsArray.length > 0) {
let sendErrorEmails = DATASETS.sendErrorEmail(errorArray, invoiceExistsArray);
}
//if there are errors, then store those errors in a CSV file and save in filecabinet
DATASETS.createFileForErrors()
} catch (e) {
log.error("Error @ summarize", e.message)
}
}
}
return MAIN;
});