This article is relevant if you are a NetSuite SuiteScripts 2.0 developer and you need to understand the pattern to work with item receipts and lot or serial numbers.
Background
Our NetSuite Systems Integration Practice is leading a new NetSuite Advanced Manufacturing implementation for a client that seeks to get off the Infor Visual ERP system. When I am asked to develop a strategy to lead a complex implementation, it’s important to break the effort into shorter and simpler efforts. Thus the interim strategy is to integrate purchase orders and item receipts from Infor Visual while we get all the inventory and manufacturing planned, set up and ready to go. Thus, we leveraged a number of our existing tools (available free of license charge to our clients):
- Apache Camel Integration: an open source middleware framework designed to use queued services to tie systems together. We previously used this framework to integrate scalable FTP services.
- Prolecto Record Import Export Manager (RIEM): a tool that supercharges hands-free CSV (and other) data imports into NetSuite in a scalable fashion.
The application is targeting the purchase order for lot-controlled raw material goods. NetSuite Lot Control and Serial Numbers (as well as Bins) demand Subrecord management for all item receipt and item fulfillment operations. The architecture of the integration delivers purchase order and item receipt data to a NetSuite Platform using SuiteScript 2.0 to produce the business logic and database work.
SuiteScript 2.0 Code Pattern to Create Lot Based Item Receipts
When I am helping new software developers learn patterns and syntax, I believe it can be very helpful to see an existing set of scripts perform a complete logic operation. We found that NetSuite’s documented SuiteScript 2.0 code examples to work with lot numbers left a few open questions. Thus, I present the following SuiteScript program in my hopes to give you a head-start on your development challenge.
/* * NetSuite SuiteScript 2.0 Pattern to illustrate * Purchase Order Transform to Item Receipt (assumes an existing purchase in the database) * Access Item Receipt Lines and update values * Assign Item Receipt Lot Numbers (pattern is very close for serial numbers) * * Thanks to Boris M. and other term members for their years of efforts at Prolecto Resources, Inc. */ define(["N/record", "N/format", "N/search", "../utils/Const"], function(record, format, search, Const) { // -- RecordHandler as part of Prolecto's Record Import / Export Manager (scalable import processing; available to those that ask) function RecordHandler() { } //find Purchase Order via lookup function findPOid(tranid) { var searchObj = search.create({ type : "purchaseorder", filters : [ "tranid", "is", tranid ], columns : [ "internalid"] }); var searchResult = {}; searchObj.run().each(function(result) { searchResult = {poid : result.getValue(result.columns[0])}; return true; }); if (searchResult.poid) { return searchResult.poid; } else { return; } } //find Item receipt function findIRid(tranid) { var searchObj = search.create({ type : "itemreceipt", filters : [ "tranid", "is", tranid ], columns : [ "internalid"] }); var searchResult = {}; searchObj.run().each(function(result) { searchResult = {irid : result.getValue(result.columns[0])}; return true; }); if (searchResult.irid) { return searchResult.irid; } else { return; } } //source data needs to have string data formatted function getDateValue(dateAsString) { try { var trandate = format.parse({ value: dateAsString, type: format.Type.DATE }); } catch (e) { log.error('IR RecordHandler', 'Parse date error: ' + e.message); } return trandate; } //function to lookup item to assess if it is lot managed function lookupItemId(itemid) { var searchObj = search.create({ type : "item", filters : [ "internalid", "is", itemid ], columns : [ "internalid", "islotitem"] }); var searchResult = {}; searchObj.run().each(function(result) { searchResult = {internalid : result.getValue(result.columns[0]), islotitem: result.getValue(result.columns[1])}; return true; }); if (searchResult.internalid) { log.debug('lookupItemId', 'Found['+ itemid +']:=' + searchResult.internalid + ':lot:' + searchResult.islotitem); return searchResult; } else { log.debug('lookupItemId', 'Not Found['+ itemid +']'); return; } } //here is where the RecordHandle does the real work //payloadData expects a hot object -- not string. RecordHandler.prototype.handle = function(payloadData, isTest) { log.debug('IR RecordHandler', 'Start:' + JSON.stringify(payloadData)); var result = {}; //in this example, our payloadData assumes a shape not show here; follow along via variable naming try { var header = payloadData[0]; var poId = header.purcorderid; var poInternalId = findPOid(poId); var irId = header.id; //item receipt //only move forward if we found an existing purchase order in NetSuite if (poInternalId) { //see if we already gave this item receipt versus creating one var irRecId = findIRid(irId); if (irRecId) { // Reconcile / Update log.debug('IR RecordHandler', 'Update'); result.type = 'UPDATE'; var irRec = record.load({type: record.Type.ITEM_RECEIPT, id: irRecId}); result.tranId = irId; result.recordId = irRecId; // update logic - enter start logic here // update logic - end } else { // here we need to create an item receipt log.debug('IR RecordHandler', 'Insert'); //primary logic to transform the purchase order to an itme receipt var irRec = record.transform({ fromType : record.Type.PURCHASE_ORDER, fromId : poInternalId, toType : record.Type.ITEM_RECEIPT }); //set the item receipt ID as we want it the same as our external system irRec.setValue("tranid", header.id); //item receipt data will default today; use the date provided with the payload if (header.receiveddate) { var recDate = getDateValue(header.receiveddate); if (recDate) { irRec.setValue("trandate", recDate); } } //we expect lines on the item receipt for everything yet to receive var irLineCount = irRec.getLineCount({ sublistId : "item" }); log.debug('IR RecordHandler', 'Line count: ' + irLineCount); // First un-select all as we will manually perform the operation for (var i = 0; i < irLineCount; i++) { irRec.setSublistValue({sublistId: "item", fieldId: "itemreceive", value: false, line : i}); } // Second select only matching by payload line; in our case, we have our own custom line number to help // us synchronize between the external system for (var j = 0; j < payloadData.length; j++) { //hunt for the respective line var lineNumber = irRec.findSublistLineWithValue({ sublistId: 'item', fieldId: 'custcol_cd_vis_line_number', value: payloadData[j].purcorderlineno + "" }); //only act if we found the line if (lineNumber != -1) { log.debug('IR RecordHandler', 'LineIndex: ' + lineNumber + ' match payload line: ' + (j+1)); // act on the item receipt line irRec.setSublistValue({sublistId: 'item', fieldId: 'quantity', value: payloadData[j].receivedqty, line : lineNumber}); irRec.setSublistValue({sublistId: 'item', fieldId: 'custcol_cd_vis_line_number', value: payloadData[j].lineno, line : lineNumber}); irRec.setSublistValue({sublistId: "item", fieldId: "itemreceive", value: true, line : lineNumber}); // get itemid and check if we need to create inventory detail [lot number action] var itemid = irRec.getSublistValue({sublistId: 'item', fieldId: 'item', line: lineNumber}); log.debug('IR RecordHandler', 'LineIndex: ' + lineNumber + ' item: ' + itemid); var itemIdRef = lookupItemId(itemid); // our custom function to see if this is lot based log.debug('IR RecordHandler', 'LineIndex: ' + lineNumber + ' item: ' + itemid + ' : ' + itemIdRef.islotitem); // if this is a lot based item, we need to do the Subrecord work if (itemIdRef && itemIdRef.islotitem == true) { //in our application, we are defaulting the location based on a constant [may have defaulted from PO] irRec.setSublistValue({sublistId: 'item', fieldId: 'location', value: Const.DEFAULT_LOCATION, line : lineNumber}); //since this is an insert, the operation is straight forward; we operate on the inherit line zero; otherwise we need to loop var objSubRecord = irRec.getSublistSubrecord({ sublistId: 'item', fieldId: 'inventorydetail', line: lineNumber }); //in this operation, we are going to default the LOT number as 'VISUAL'; any kind of algorithm can be used; objSubRecord.setSublistValue({sublistId: 'inventoryassignment', fieldId: 'quantity', line: 0, value: payloadData[j].receivedqty}); objSubRecord.setSublistValue({sublistId: 'inventoryassignment', fieldId: 'receiptinventorynumber', line: 0, value: 'VISUAL'}); } log.debug('IR RecordHandler','End line'); } } //finally, save the item receipt and get the resulting ID var irRecId = irRec.save(); result.recordId = irRecId; result.tranId = irId; log.debug('IR RecordHandler', 'Success: ' + irId + ' : ' + irRecId); } } else { // We have an external item receipt against a PO that is not yet in NetSuite (your business logic will vary) // appropriate return for Record Import Export Manager to try again log.debug('IR RecordHandler', 'Not PO: ' + poId + ' found for IR: ' + irId); result.tranId = irId; result.skip = true; result.errorMessage = 'No PO:' + poId + 'found.' } } catch (e) { log.error('IR RecordHandler', e.message); result.isError = true; result.errorMessage = e.message; } log.debug('IR RecordHandler', 'handle end.'); return result; } return { RecordHandler : RecordHandler }; });
Belong to a Professional Team of NetSuite Innovators
The goal of this article is to help you become more powerful in the work you do. If you are passionate about the NetSuite business platform and hold high standards for care and seek to be recognized for your unique ability to listen to concerns so you can turn that challenge into innovative offers for help, then perhaps we should start a conversation about becoming a contributing member of our team.
Marty,
Was you client satisfied with the transition from Infor to NetSuite? I have found that NetSuite fails in so many ways when it comes to lot item inventory management, especially in the context of lot-numbered inventory costing. But arguably, Infor fail miserably on the reporting and financial side of things.
Did you client end up using both systems with this integration in place or was he able to finally get 100% on board with NS?
One challenge I see with lots in NetSuite is lot-specific inventory/cost adjustments. We have 100k+ lots in our system. The NS subrecord architecture for lots is a PAIN when volume is high.
Did you end up coming up with a similar solution for this client when it comes to cost and qty adjustments (especially since the inventory worksheet does not handle lots in Netsuite)?
Thanks!
Arthur,
We find that for any Lot-based operations of volume, we need to do some level of automation. We further believe that it is important to link a custom lot record on NetSuite’s native lot record so we can enhance it with more information and create better information views. We will be making a major announcement about lot costing and manufacturing soon.
Marty