Scheduled integration of Item Receipt (IR) Sync from multiple TrackTraceRX to NetSuite
define(['N/record', 'N/search', 'N/format', 'N/config', '../../Common Library/jj_tracktracerx_ns_utility.js', '../../Config Module/jj_cm_tracktracerx_api_config.js'],
/**
* @param{record} record
* @param{search} search
* @param{format} format
* @param{config} config
* @param{jjUtil} jjUtil
* @param{TrackTrace} TrackTrace
*/
(record, search, format, config, jjUtil, trackTraceAPI) => {
const TRACKTRACELIB = trackTraceAPI.Library;
// UUID Fields for subsidiaries
const ITEM_FIELD_UUID_DRUG = 'custrecord_jj_itemuuid_trcktrcerx_drug_';
const ITEM_FIELD_UUID_NAKORN = 'custrecord_jj_itemuuid_trcktrcerx_nakorn';
const ITEM_FIELD_UUID_GALAXY = 'custrecord_jj_itemuuid_trcktrcerx_galaxy';
const ITEM_FIELD_UUID_BRIGHT = 'custrecord_jj_itemuuid_trcktrcerx_bright';
// Subsidiary to UUID field mapping
const subsidiaryUUIDMap = {
29: ITEM_FIELD_UUID_BRIGHT, // Brightline
17: ITEM_FIELD_UUID_NAKORN, // Nakorn
26: ITEM_FIELD_UUID_GALAXY, // Galaxy
12: ITEM_FIELD_UUID_DRUG // Drugplace
};
/**
* @description Function to search UOM Details custom record with product UUID
* @param {string} productUUID UUID of the product from the shipment
* @returns {object|null} Matching UOM details record if found, null otherwise
*/
function findUOMDetails(productUUID, subsidiaryValue) {
try {
let itemUuidField = subsidiaryUUIDMap[subsidiaryValue];
let uomSearch = search.create({
type: "customrecord_jj_item_uomdetails", // Replace with the correct internal ID of your UOM details custom record
filters: [
[itemUuidField, "is", productUUID] // Assuming 'custrecord_product_uuid' is the field for product UUID
],
columns: [
search.createColumn({ name: "internalid", label: "Internal ID" }),
search.createColumn({ name: "custrecord_jj_item_qty_lowestunit", label: "Lowest Unit of Sale" }),
search.createColumn({ name: "custrecord_jj_item_saleable_unit", label: "Unit" })
]
});
let result = uomSearch.run().getRange({ start: 0, end: 1 });
if (result.length > 0) {
let uomDetails = {
internalId: result[0].getValue({ name: "internalid" }),
lowestUnitOfSale: result[0].getValue({ name: "custrecord_jj_lowest_unit_of_sale" }),
unit: result[0].getValue({ name: "custrecord_jj_unit" })// Fetch the lowest unit field value
};
log.debug('Found UOM details', uomDetails);
return uomDetails;
} else {
log.debug('No matching UOM found for product UUID:', productUUID);
return null;
}
} catch (e) {
log.error('error@findUOMDetails', e);
return null;
}
}
/**
* @description Function to fetch shipments from TrackTraceRX
* @returns {Array} Array of shipment objects
*/
function fetchShipmentsFromTrackTrace() {
try {
let shipmentResponse;
let shipArray = [];
let shipmentMainArray = [];
let subsidiaries = ["29", "17", "12", "26",];
for (let i = 0; i < 4; i++) {
let configSearch = TRACKTRACELIB.apiConfigfileSearch(subsidiaries[i]);
log.debug("configSearch search...........", configSearch)
let api = TRACKTRACELIB.TRACKTRACERX_API_REQUESTS.recevingshipmnets;
shipmentResponse = TRACKTRACELIB.requestToTrackTrace("GET", configSearch?.domain + '/' + configSearch?.version + api, "", configSearch);
shipArray.push(shipmentResponse);
}
for (let j = 0; j < 4; j++) {
if (shipArray[j]?.code == 200) {
for (let k = 0; k < 4; k++) {
log.debug('Fetched Shipments', shipArray[k].body);
shipmentMainArray.push(shipArray[k].body.data)
}
return shipmentMainArray;
} else {
log.error('Error fetching shipments from TrackTraceRX', shipArray[j].message);
return [];
}
}
} catch (e) {
log.error('error@fetchShipmentsFromTrackTrace', e);
return [];
}
}
function findSubsidiary(poNumber) {
let purchaseorderSearchObj = search.create({
type: "purchaseorder",
filters:
[
["type", "anyof", "PurchOrd"],
"AND",
["numbertext", "is", poNumber]
],
columns:
[
search.createColumn({ name: "subsidiary", label: "Subsidiary" })
]
});
let searchResultCount = purchaseorderSearchObj.runPaged().count;
let poSubsidiary;
purchaseorderSearchObj.run().each(function (result) {
poSubsidiary = result.getValue({
name: "subsidiary"
})
});
return poSubsidiary;
}
/**
* @description Function to search for open POs in NetSuite matching the shipment data
* @param {string} poNumber PO Number from TrackTraceRX
* @param {string} vendorUUID Vendor UUID from TrackTraceRX
* @returns {object} Matching PO details if found
*/
function findMatchingPO(poNumber, vendorUUID, subsidiaryPo) {
let vendorField = vendorSubsidiaryMap[subsidiaryPo];
try {
let poSearch = search.create({
type: "purchaseorder",
filters: [
["numbertext", "is", poNumber],
"AND",
["status", "anyof", "PurchOrd:D", "PurchOrd:E", "PurchOrd:B"],
],
columns: [
search.createColumn({ name: "internalid", label: "internalid" }),
search.createColumn({ name: "location", label: "location" }),
search.createColumn({ name: "subsidiary", label: "subsidiary" })
]
});
let result = poSearch.run().getRange({ start: 0, end: 10 });
if (result.length > 0) {
log.debug('Found matching PO', result[0]);
return result[0];
} else {
return null;
}
} catch (e) {
log.error('error@findMatchingPO', e);
return null;
}
}
/**
* @description Function to get detailed shipment information for a given shipment UUID
* @param {string} shipmentUUID UUID of the shipment to retrieve details
* @returns {object} Shipment details from TrackTraceRX
*/
function getShipmentDetails(shipmentUUID, subsidiaryValue) {
try {
for (let i = 0; i < 4; i++) {
let configSearch = TRACKTRACELIB.apiConfigfileSearch(subsidiaryValue);
let api = TRACKTRACELIB.TRACKTRACERX_API_REQUESTS.getSpecificShipment.replace('{shipment_type}', 'Inbound').replace('{shipment_uuid}', shipmentUUID);
let apiWithParams = api + "?is_convert_lowest_sealable_unit=true";
let shipmentDetailsResponse = TRACKTRACELIB.requestToTrackTrace("GET", configSearch?.domain + '/' + configSearch?.version + apiWithParams, "", configSearch);
if (shipmentDetailsResponse?.code == 200) {
log.debug('Fetched Shipment Details', shipmentDetailsResponse.body);
return shipmentDetailsResponse.body;
} else {
log.error('Error fetching shipment details', shipmentDetailsResponse.message);
return null;
}
}
} catch (e) {
log.error('error@getShipmentDetails', e);
return null;
}
}
/**
* @description Function to create or update the shipment integration record
* @param {object} shipRespBody Shipment response body from TrackTraceRX
* @param {string} nsShipId Netsuite Shipment Internal ID
* @param {string} nsIntegrInfoId Transaction Integration Info Record Internal ID
* @param {string} shipIntegrInfoId Shipment Integration Info Record Internal ID
* @param {string} errorMsg Error occurred during creation
*/
function createOrUpdateShipIntegrationRecord(shipRespBody, nsShipId, shipIntegrInfoId, errorMsg) {
try {
let recObj = {};
if (jjUtil.checkForParameter(nsShipId)) {
recObj.custrecord_jj_shipment = nsShipId;
recObj.custrecord_jj_shipment_uuid = shipRespBody?.uuid;
recObj.custrecord_jj_shipment_sync_status = "Created";
recObj.custrecord_jj_shipment_sync_error = "";
recObj.custrecord_jj_tracktrace_shipmen_payload = JSON.stringify(shipRespBody);
} else {
recObj.custrecord_jj_tracktrace_transaction = "";
recObj.custrecord_jj_shipment_uuid = shipRespBody?.uuid || "";
recObj.custrecord_jj_shipment_sync_status = "Failed";
recObj.custrecord_jj_shipment_sync_error = errorMsg;
recObj.custrecord_jj_tracktrace_shipmen_payload = JSON.stringify(shipRespBody);
}
let integrationRecId;
if (jjUtil.checkForParameter(shipIntegrInfoId)) {
integrationRecId = record.submitFields({
type: 'customrecord_jj_tracktrace_shipments',
id: shipIntegrInfoId,
values: recObj,
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
} else {
let integrationRecObj = record.create({
type: 'customrecord_jj_tracktrace_shipments'
});
for (let key in recObj) {
integrationRecObj.setValue({ fieldId: key, value: recObj[key] });
}
integrationRecId = integrationRecObj.save({
enableSourcing: false,
ignoreMandatoryFields: true
});
}
log.debug('Shipment Integration Record Updated', integrationRecId);
} catch (e) {
log.error('error@createOrUpdateShipIntegrationRecord', e);
}
}
/**
* Function is defined to search the order integration ifo record and the shipment integration records created under this
* @param {String} orderId SO/PO internal id
* @returns {object}
*/
function integrationInfoSearch(orderId) {
try {
let integrInfo = {};
let customrecord_jj_tracktrace_integr_infoSearchObj = search.create({
type: "customrecord_jj_tracktrace_integr_info",
filters:
[
["custrecord_jj_tracktrace_transaction", "anyof", orderId]
],
columns:
[
search.createColumn({ name: "internalid", label: "Internal ID" }),
search.createColumn({ name: "custrecord_jj_tracktrace_transaction", label: "Transaction" }),
search.createColumn({ name: "custrecord_jj_transaction_uuid", label: "Transaction UUID" }),
search.createColumn({
name: "internalid",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Internal ID"
}),
search.createColumn({
name: "custrecord_jj_shipment",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Shipment"
}),
search.createColumn({
name: "custrecord_jj_shipment_uuid",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Shipment UUID"
})
]
});
customrecord_jj_tracktrace_integr_infoSearchObj.run().each(function (result) {
// .run().each has a limit of 4,000 results
integrInfo.id = result.getValue({ name: "internalid", label: "Internal ID" });
integrInfo.orderid = result.getValue({ name: "internalid", label: "Internal ID" });
integrInfo.orderuuid = result.getValue({ name: "internalid", label: "Internal ID" });
let shipmentInfo = result.getValue({
name: "internalid",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Internal ID"
});
if (!integrInfo.shipments && shipmentInfo)
integrInfo.shipments = [];
if (shipmentInfo) {
let shipment = {}
shipment.id = shipmentInfo
shipment.shipmentid = result.getValue({
name: "custrecord_jj_shipment",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Shipment"
})
shipment.shipmentuuid = result.getValue({
name: "custrecord_jj_shipment_uuid",
join: "CUSTRECORD_JJ_TRANSACTION_INTEGRATION",
label: "Shipment UUID"
});
integrInfo.shipments.push(shipment);
}
return true;
});
return integrInfo;
} catch (e) {
log.debug('error@integrationInfoSearch', e);
return false;
}
}
/**
* Function defined to create the shipment in Netsuite
* @param {string} orderId Order internal id
* @param {object} shipRespBody response body got from the shipment get API call
* @returns {object}
*/
function createShipmentInNs(orderId, shipRespBody, subsidiaryValue) {
try {
// Get the shipment item lines
let shipmentItems = shipRespBody?.ShipmentLineItem;
log.debug("shipmentItems", shipmentItems)
// Transform the order to Item Receipt
let objRecord = record.transform({
fromType: 'purchaseorder',
fromId: orderId,
toType: 'itemreceipt',
isDynamic: true, // Ensure the record is dynamic
});
// Set the received date from the shipment response
let receivedDate = shipRespBody?.received_on_event?.date;
if (receivedDate) {
receivedDate = receivedDate.replace(" ", "T");
receivedDate = new Date(receivedDate + "Z");
objRecord.setValue({
fieldId: 'trandate', value: receivedDate
});
}
// Set memo field if notes exist in the shipment response
objRecord.setValue({ fieldId: 'memo', value: shipRespBody?.notes || "" });
let itemLineCount = objRecord.getLineCount({ sublistId: 'item' });
for (let j = 0; j < itemLineCount; j++) {//Iterate through the IR item line
objRecord.selectLine({ sublistId: 'item', line: j });
let orderLine = objRecord.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'orderline'
});
let shipItemLine = shipmentItems
log.debug("shipItemLine", shipItemLine)
//Proceed if the IR item line exists in the TrackTraceRx shipment
if (shipItemLine) {
let shipmentItemObj = shipItemLine[0];
log.debug("shipmentItemObj", shipmentItemObj)
let uomDetails = findUOMDetails(shipmentItemObj.product.uuid, subsidiaryValue);
log.debug("uomDetails", uomDetails)
if (!uomDetails) {
log.debug('Skipping item with product UUID as no matching UOM was found', shipmentItemObj.product.uuid);
continue; // Skip this item if no UOM details found
}
let receivedQty = shipmentItemObj?.quantity;
log.debug("receivedQty", receivedQty)
if (uomDetails.unit === '2') {
// Perform special calculation for unit '2'
let lowestUOMQty = uomDetails.lowestUnitOfSale; // Assume the custom field for the lowest UOM quantity
if (lowestUOMQty && lowestUOMQty > 0) {
receivedQty = receivedQty / lowestUOMQty; // Convert quantity based on the lowest unit
} // Example: multiply by 2 (you can replace this with any calculation logic needed)
}
//set item line
objRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'itemreceive',
value: true
});
objRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: receivedQty
});
let objSubrecord = objRecord.getCurrentSublistSubrecord({
sublistId: 'item',
fieldId: 'inventorydetail'
});
let itemLots = shipmentItemObj?.product.lots;
log.debug("itemLots", itemLots)
//set inventory details subrecord
for (let l = 0; l < itemLots.length; l++) {
objSubrecord.selectLine({ sublistId: 'inventoryassignment', line: l });
objSubrecord.setCurrentSublistText({
sublistId: 'inventoryassignment',
fieldId: 'receiptinventorynumber',
text: itemLots[l]?.number ?? itemLots[l]?.lot_number
});
objSubrecord.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'binnumber',//bin
value: 936//Receiving//itemLots[l]?.ti_notes
});
let expDate = itemLots[l]?.expiration_date;
let expDateObj = new Date(expDate);
let expDateStr = format.format({
value: expDateObj,
type: format.Type.DATETIMETZ,
timezone: 'GMT',
});
let expDateFormatedObj = format.parse({
value: expDateStr,
type: format.Type.DATE,
timezone: 'GMT'
});
objSubrecord.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'expirationdate',
value: expDateFormatedObj
});
objSubrecord.setCurrentSublistValue({
sublistId: 'inventoryassignment',
fieldId: 'quantity',
value: itemLots[l]?.quantity
});
objSubrecord.commitLine({ sublistId: 'inventoryassignment' });
}
objRecord.commitLine({ sublistId: 'item' });
} else {
//remove the item line from IR
objRecord.selectLine({ sublistId: 'item', line: j });
objRecord.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'itemreceive',
value: false
});
objRecord.commitLine({ sublistId: 'item' });
}
}
let shipId = objRecord.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
return { id: shipId, error: '' };
} catch (e) {
log.error('error@createShipmentInNs', e);
let error = jjUtil.checkForParameter(e?.message) ? e.message : e;
return { id: '', error: error };
}
}
/**
* Defines the function that is executed at the beginning of the map/reduce process and generates the input data.
* @returns {Array} The input data to use in the map/reduce process
*/
const getInputData = () => {
try {
return fetchShipmentsFromTrackTrace();
} catch (e) {
log.error('error@getInputData', e);
return [];
}
};
/**
* Defines the function that is executed when the reduce entry point is triggered.
* @param {Object} reduceContext
*/
const reduce = (reduceContext) => {
try {
let shipmentObj = JSON.parse(reduceContext.values[0]);
for (let i = 0; i < shipmentObj.length; i++) {
let poNumber = shipmentObj[i].transactions[0].po_number
let pouuid = shipmentObj[i].transactions[0].uuid
let vendorUUID = shipmentObj[i].trading_partner_uuid;
let subsidiaryPo = findSubsidiary(poNumber)
let poRecord = findMatchingPO(poNumber, vendorUUID, subsidiaryPo);
if (poRecord) {
let orderId = poRecord.getValue({ name: 'internalid' });
let subsidiaryValue = poRecord.getValue({ name: "subsidiary" });
let shipmentUUID = shipmentObj[i].uuid;
log.debug("shipmentUUID",shipmentUUID)
let shipmentDetails = getShipmentDetails(shipmentUUID, subsidiaryValue);
if (shipmentDetails) {
// Create Item Receipt in NetSuite and handle mapping
let shipNs = createShipmentInNs(orderId, shipmentDetails, subsidiaryValue);
record.submitFields({
type: 'purchaseorder',
id: orderId,
values: {
custbody_jj_tracktrace_uuid: pouuid
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
createOrUpdateShipIntegrationRecord(shipmentDetails, shipNs?.id, '', shipNs?.error);
}
} else {
log.debug('No matching PO found for PO Number:', poNumber);
}
}
} catch (e) {
log.error('error@reduce', e);
}
};
return { getInputData, reduce };
});