Auto invoicing Inventory Items on Schedule basis

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;
    });

Leave a comment

Your email address will not be published. Required fields are marked *