For the client’s convenience, we were tasked to add a button on the Transfer Order form that creates a new Invoice record. The client also requested that pertinent fields from the Transfer Order be automatically copied onto the newly-opened Invoice record, including the inventory detail. This button must only be available for Transfer Orders with the “Received” status.
To accomplish this task, we first created the following custom fields in Customization > Lists, Records, & Fields > Transaction Body Fields > New (custbody is automatically prepended on the field IDs):
- custbody_customer_name
- custbody_note
- custbody_source_transfer_order
- custbody_created_invoice
We created a client script and a user event script for both the Transfer Order and the Invoice, as follows:
Client Script (Transfer Order)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 |
/** *@NApiVersion 2.1 *@NScriptType ClientScript */ define([ "N/record", "N/search", "N/ui/dialog", "N/error", "N/url", "N/ui/message", ], function (record, search, dialog, error, url, message) { function pageInit(context) {} function invoiceFromTo(toId) { if (document.getElementById("custscript_invoice_from_to_modal") == null) { var link = document.createElement("link"); link.rel = "stylesheet"; link.type = "text/css"; link.href = "https://www.w3schools.com/w3css/4/w3.css"; document.head.appendChild(link); var modalElement = `<div id="custscript_invoice_from_to_modal" class="w3-modal w3-display-container"> <div class="w3-modal-content w3-display-middle w3-display-container" style="height: 100px; width: 60%;"> <div class="w3-container w3-display-middle"> <p>Please wait. We're creating the invoice for you...</p> </div> </div> </div>`; var tempDiv = document.createElement("div"); tempDiv.innerHTML = modalElement.trim(); document.body.appendChild(tempDiv.firstChild); } function createInvoice(waitMessage) { try { if (toId == undefined) { waitMessage.hide(); return; } //Load the transfer order record var toRec = record.load({ type: record.Type.TRANSFER_ORDER, id: toId, }); var relatedRecID; var relatedRecType; var relatedRecord; //Fetch the related records from the transfer order var itemfulfillmentSearchObj = search.create({ type: "itemfulfillment", filters: [ ["createdfrom", "anyof", toRec.id], "AND", ["mainline", "is", "T"], ], }); var searchResultCount = itemfulfillmentSearchObj.runPaged().count; //Create a warning if the transfer order does not have any related records and cancel the operation if (parseInt(searchResultCount) < 1) { waitMessage.hide(); message .create({ type: message.Type.WARNING, title: `No Related Records`, message: `This transfer order does not have any related records yet. Please fulfill or receive the order first.`, duration: 18000, }) .show(); return; } itemfulfillmentSearchObj.run().each(function (result) { relatedRecID = result.id; relatedRecType = result.recordType; return false; }); if (!isNaN(parseInt(relatedRecID))) { relatedRecord = record.load({ type: relatedRecType, id: relatedRecID, }); } //Start of Invoice Creation var invRec = record.create({ type: record.Type.INVOICE, isDynamic: true, }); //Object containing the mapping; key = invoice; value = transfer order var fieldMap = { entity: "custbody_customer_name", subsidiary: "subsidiary", location: "transferlocation", memo: "memo", custbody_note: "custbody_note", department: "department", }; //Go through all key value pairs in fieldMap; Get value from TO and set to Invoice Object.keys(fieldMap).forEach(function (invField) { var toField = fieldMap[invField]; var toVal = toRec.getValue({ fieldId: toField, }); if (toVal != undefined && toVal != "" && toVal != null) { invRec.setValue({ fieldId: invField, value: toVal, }); } }); //Sets the source transfer order of the created invoice; This helps determine if an invoice has already been created from the TO invRec.setValue({ fieldId: "custbody_source_transfer_order", value: toRec.id, }); var toLineCount = toRec.getLineCount({ sublistId: "item", }); if (relatedRecord != undefined) { var relatedRecordLineCount = relatedRecord.getLineCount({ sublistId: "item", }); //Go through each item sublist line of the related record for (var line = 0; line < relatedRecordLineCount; line++) { invRec.selectNewLine({ sublistId: "item", }); //Set sublist field values with the values from the related records ["item", "quantity", "rate", "amount"].forEach(function (field) { var relatedRecordVal = relatedRecord.getSublistValue({ sublistId: "item", fieldId: field, line: line, }); if ( relatedRecordVal != undefined && relatedRecordVal != "" && relatedRecordVal != null ) { invRec.setCurrentSublistValue({ sublistId: "item", fieldId: field, value: relatedRecordVal, ignoreFieldChange: false, }); } }); //Set rate and amount to 0; these are required fields that the related records do not have invRec.setCurrentSublistValue({ sublistId: "item", fieldId: "rate", value: 0, ignoreFieldChange: false, }); invRec.setCurrentSublistValue({ sublistId: "item", fieldId: "amount", value: 0, ignoreFieldChange: false, }); //Gets and sets the invoice inventory detail from the related record var relLine = relatedRecord.findSublistLineWithValue({ sublistId: "item", fieldId: "item", value: invRec.getCurrentSublistValue({ sublistId: "item", fieldId: "item", }), }); var relInvDet = relatedRecord.getSublistSubrecord({ sublistId: "item", fieldId: "inventorydetail", line: relLine, }); var invInvDet = invRec.getCurrentSublistSubrecord({ sublistId: "item", fieldId: "inventorydetail", }); //List of inventory detail body fields that will be retrieved from the related records and set to the invoice var invDetBodyFields = [ "item", "itemdescription", "location", "quantity", "tolocation", "unit", ]; //List of sublist fields in the inventory detail from the related records to the invoice; //The prefix separated by the colon can be v or t indicating the use of setValue or setText respectively var invDetSublistFields = [ "v:binnumber", "v:expirationdate", "v:inventorystatus", "v:issueinventorynumber", "v:packcarton", "v:pickcarton", "v:quantity", "v:quantitystaged", "v:tobinnumber", "v:toinventorystatus", ]; //Copies the inventory detail from the related record to the invoice using the array of body and sublist fields as reference var relInvDetLineCount = relInvDet.getLineCount({ sublistId: "inventoryassignment", }); for ( var relInvDetLine = 0; relInvDetLine < relInvDetLineCount; relInvDetLine++ ) { invInvDet.selectNewLine({ sublistId: "inventoryassignment", }); invDetSublistFields.forEach(function (sublistField) { var isValue = sublistField.startsWith("v:"); sublistField = sublistField.substring(2, sublistField.length); if (isValue) { var relSublistVal = relInvDet.getSublistValue({ sublistId: "inventoryassignment", fieldId: sublistField, line: relInvDetLine, }); if (relSublistVal != undefined) { try { invInvDet.setCurrentSublistValue({ sublistId: "inventoryassignment", fieldId: sublistField, value: relSublistVal, ignoreFieldChange: false, }); console.log({ title: "Successfully set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } catch (error) { console.log({ title: "Unable to set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } } else { console.log({ title: "InvDet Sublist Field Has No Value", details: `${line} : ${sublistField}`, }); } } else { var relSublistVal = relInvDet.getSublistText({ sublistId: "inventoryassignment", fieldId: sublistField, line: relInvDetLine, }); if (relSublistVal != undefined) { try { invInvDet.setCurrentSublistText({ sublistId: "inventoryassignment", fieldId: sublistField, text: relSublistVal, ignoreFieldChange: false, }); console.log({ title: "Successfully set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } catch (error) { console.log({ title: "Unable to set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } } else { console.log({ title: "InvDet Sublist Field Has No Value", details: `${line} : ${sublistField}`, }); } } }); invInvDet.commitLine({ sublistId: "inventoryassignment", ignoreRecalc: false, }); } invRec.commitLine({ sublistId: "item", ignoreRecalc: false, }); } } console.log( `The record is about to be saved. Any errors below, if any are related to saving the record` ); //Save invoice var invId = invRec.save({ enableSourcing: true, ignoreMandatoryFields: true, }); return { invoice: invId, transferOrder: toRec, }; } catch (error) { waitMessage.hide(); message .create({ type: message.Type.ERROR, title: `Error in Invoice Creation`, message: `The script encountered the following errors: ${error}`, duration: 18000, }) .show(); } } function postCreation(params) { var invId = params.creationResult.invoice; var waitMessage = params.waitMessage; var toRec = params.creationResult.transferOrder; if (isNaN(parseInt(invId))) throw `The creation of invoice failed. Sorry.`; //Get the record URL var recUrl = url.resolveRecord({ isEditMode: true, recordId: invId, recordType: record.Type.INVOICE, }); document.getElementById( "custscript_invoice_from_to_modal" ).style.display = "none"; waitMessage.hide(); var domain = url.resolveDomain({ hostType: url.HostType.APPLICATION, }); //Inform the user that the operation has been completed //In some cases, the browser blocks the opening of the invoice in a new tab. As a backup, the edit url for the invoice is also displayed message .create({ type: message.Type.INFORMATION, title: "Invoice Created", message: `The invoice has been created and should have opened in a new tab. If it didn't, your browser may be blocking popups. Please go to <a href="https://${domain}${recUrl}" target="_blank">this link</a> to edit the record instead`, duration: 180000, }) .show(); //Opens the invoice in edit mode in a new tab window.open(recUrl, "_blank"); return { invoice: invId, transferOrder: toRec, }; } search.create .promise({ type: "invoice", filters: [ ["type", "anyof", "CustInvc"], "AND", ["custbody_source_transfer_order", "anyof", toId], ], }) .then((searchObj) => { return new Promise((resolve) => { searchObj.runPaged .promise() .then((pagedDataObj) => { return pagedDataObj.count; }) .then((resultCount) => { console.log( `Done counting existing invoices from this TO: ${resultCount}` ); resolve({ resultCount: resultCount, searchObj: searchObj, }); }); }); }) .then((args) => { var resultCount = args.resultCount; var searchObj = args.searchObj; if (resultCount > 0) { return new Promise((resolve) => { searchObj.run().each.promise((result) => { var existingInvoiceURL = url.resolveRecord({ isEditMode: false, recordId: result.id, recordType: record.Type.INVOICE, }); var domain = url.resolveDomain({ hostType: url.HostType.APPLICATION, }); message .create({ type: message.Type.WARNING, title: `Invoice Already Exists`, message: `The invoice created from this transfer order already exists. To view the invoice, go to <a href="https://${domain}${existingInvoiceURL}" target="_blank">this link</a>. To create an invoice again, please delete the existing invoice first.`, duration: 30000, }) .show(); setTimeout(() => { resolve(true); }, 1000); return false; }); }); } else { return false; } }) .then((exists) => { if (!exists) { //Create a message to show that the operation is ongoing var waitMessage = message.create({ type: message.Type.INFORMATION, title: "Creating Invoice...", message: "The invoice is being created. Please wait... The created invoice will open in edit mode once done.", }); waitMessage.show(); document.getElementById( "custscript_invoice_from_to_modal" ).style.display = "block"; return new Promise((resolve) => { setTimeout(() => { resolve(waitMessage); }, 1000); }); } else { throw `Invoice already exists.`; } }) .then((waitMessage) => { return new Promise((resolve) => { resolve({ creationResult: createInvoice(waitMessage), waitMessage: waitMessage, }); }); }) .then((params) => { return postCreation(params); }) .then((args) => { var toRec = args.transferOrder; toRec.setValue({ fieldId: "custbody_created_invoice", value: args.invoice, }); var savedToId = toRec.save({ enableSourcing: true, ignoreMandatoryFields: true, }); }) .catch((error) => { document.getElementById( "custscript_invoice_from_to_modal" ).style.display = "none"; }); } return { pageInit: pageInit, invoiceFromTo: invoiceFromTo, }; }); |
User Event Script (Transfer Order)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/** *@NApiVersion 2.1 *@NScriptType UserEventScript */ define([ "N/url", "N/ui/serverWidget", "N/record", "N/search", "N/runtime", "N/config", ], function (url, serverWidget, record, search, runtime, config) { function beforeLoad(context) { try { var form = context.form; var newrecord = context.newRecord; if (newrecord.type != record.Type.TRANSFER_ORDER) return; if (newrecord.getValue({ fieldId: "status" }) != "Received") return; var domain = url.resolveDomain({ hostType: url.HostType.APPLICATION, }); var clientScriptId = config .load({ type: config.Type.COMPANY_PREFERENCES, }) .getValue({ fieldId: "custscript_toinv_cs_id", }); if (clientScriptId != undefined && clientScriptId != "") { context.form.clientScriptFileId = clientScriptId; var customer = newrecord.getValue({ fieldId: "custbody_sererra_customer_name", }); form.addButton({ id: "custpage_create_invoice", label: "Create Invoice", functionName: `invoiceFromTo(${newrecord.id})`, }); } } catch (error) { log.error({ title: "Error", details: error, }); } } return { beforeLoad: beforeLoad, }; }); |
Client Script (Invoice)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
/** *@NApiVersion 2.1 *@NScriptType ClientScript */ define(["N/record", "N/search", "N/ui/message"], function ( record, search, message ) { function pageInit(context) { var importMessage = message.create({ type: message.Type.INFORMATION, title: "Importing...", message: "The import operation has started. Please wait as all values are being supplied. This message will be gone once the operation is done.", }); async function startImport() { try { if (context.mode != "create") return; var urlArray = window.location.href.split("?"); if (urlArray.length <= 1) { log.debug({ title: "URL Params", details: "URL parameters not found", }); return; } //Builds an object literal containing all parameters from url var params = {}; urlArray[1].split("&").forEach(function (item) { var key = item.split("=")[0]; var val = item.split("=")[1]; params[key] = val; }); var toId = params.custparam_to_id; if (toId == undefined) { return; } importMessage.show(); var invRec = context.currentRecord; var toRec = record.load({ type: record.Type.TRANSFER_ORDER, id: toId, }); var fieldMap = { //entity: 'custbody_customer_name', subsidiary: "subsidiary", location: "transferlocation", memo: "memo", custbody_note: "custbody_note", department: "department", }; Object.keys(fieldMap).forEach(function (invField) { var toField = fieldMap[invField]; var toVal = toRec.getValue({ fieldId: toField, }); if (toVal != undefined && toVal != "" && toVal != null) { invRec.setValue({ fieldId: invField, value: toVal, ignoreFieldChange: true, }); } }); invRec.setValue({ fieldId: "custbody_source_transfer_order", value: toRec.id, ignoreFieldChange: true, }); var toLineCount = toRec.getLineCount({ sublistId: "item", }); var invLineCount = invRec.getLineCount({ sublistId: "item", }); var relatedRecID; var relatedRecType; var relatedRecord; var itemfulfillmentSearchObj = search.create({ type: "itemfulfillment", filters: [ ["createdfrom", "anyof", toRec.id], "AND", ["mainline", "is", "T"], ], }); var searchResultCount = itemfulfillmentSearchObj.runPaged().count; itemfulfillmentSearchObj.run().each(function (result) { relatedRecID = result.id; relatedRecType = result.recordType; }); if (!isNaN(parseInt(relatedRecID))) { relatedRecord = record.load({ type: relatedRecType, id: relatedRecID, }); } if (relatedRecord != undefined) { var relatedRecordLineCount = relatedRecord.getLineCount({ sublistId: "item", }); for (var line = 0; line < relatedRecordLineCount; line++) { if (line < invLineCount) invRec.selectLine({ sublistId: "item", line: line, }); else invRec.selectNewLine({ sublistId: "item", }); ["item", "quantity", "rate", "amount"].forEach(function (field) { var relatedRecordVal = relatedRecord.getSublistValue({ sublistId: "item", fieldId: field, line: line, }); if ( relatedRecordVal != undefined && relatedRecordVal != "" && relatedRecordVal != null ) { invRec.setCurrentSublistValue({ sublistId: "item", fieldId: field, value: relatedRecordVal, }); } }); invRec.setCurrentSublistValue({ sublistId: "item", fieldId: "rate", value: 0, }); invRec.setCurrentSublistValue({ sublistId: "item", fieldId: "amount", value: 0, }); invRec.commitLine({ sublistId: "item", ignoreRecalc: true, }); } } } catch (error) { log.error({ title: "Error", details: error, }); } } startImport().then(function () { importMessage.hide(); }); } return { pageInit: pageInit, }; }); |
User Event Script (Invoice)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
/** *@NApiVersion 2.1 *@NScriptType UserEventScript */ define(["N/record", "N/search"], function (record, search) { function beforeSubmit(context) { var rec = context.newRecord; var eventType = context.type; if (eventType != context.UserEventType.CREATE) return; var toId = rec.getValue({ fieldId: "custbody_source_transfer_order", }); if (isNaN(parseInt(toId))) return; var toRec = record.load({ type: record.Type.TRANSFER_ORDER, id: toId, }); var relatedRecID; var relatedRecType; var relatedRecord; var itemfulfillmentSearchObj = search.create({ type: "itemfulfillment", filters: [ ["createdfrom", "anyof", toRec.id], "AND", ["mainline", "is", "T"], ], }); var searchResultCount = itemfulfillmentSearchObj.runPaged().count; itemfulfillmentSearchObj.run().each(function (result) { relatedRecID = result.id; relatedRecType = result.recordType; //return true; }); if (!isNaN(parseInt(relatedRecID))) { relatedRecord = record.load({ type: relatedRecType, id: relatedRecID, }); } var lineCount = rec.getLineCount({ sublistId: "item", }); if (relatedRecord != undefined) { for (var line = 0; line < lineCount; line++) { var relLine = relatedRecord.findSublistLineWithValue({ sublistId: "item", fieldId: "item", value: rec.getSublistValue({ sublistId: "item", fieldId: "item", line: line, }), }); var relInvDet = relatedRecord.getSublistSubrecord({ sublistId: "item", fieldId: "inventorydetail", line: relLine, }); var invInvDet = rec.getSublistSubrecord({ sublistId: "item", fieldId: "inventorydetail", line: line, }); var invDetBodyFields = [ "item", "itemdescription", "location", "quantity", "tolocation", "unit", ]; var invDetSublistFields = [ "binnumber", "expirationdate", "inventorystatus", "issueinventorynumber", "packcarton", "pickcarton", "quantity", "quantitystaged", "tobinnumber", "toinventorystatus", ]; invDetBodyFields.forEach(function (bodyField) { var relBodyVal = relInvDet.getValue({ fieldId: bodyField, }); if (relBodyVal != undefined) { try { invInvDet.setValue({ fieldId: bodyField, value: relBodyVal, }); log.audit({ title: "Successfully Set Field Value", details: `${bodyField}: ${relBodyVal}`, }); } catch (error) { log.error({ title: "Failed to set inventory detail value", details: `${bodyField}: ${relBodyVal}`, }); } } else { log.audit({ title: "InvDet Field Has No Value", details: relBodyField, }); } }); var relInvDetLineCount = relInvDet.getLineCount({ sublistId: "inventoryassignment", }); for ( var relInvDetLine = 0; relInvDetLine < relInvDetLineCount; relInvDetLine++ ) { invDetSublistFields.forEach(function (sublistField) { var relSublistVal = relInvDet.getSublistValue({ sublistId: "inventoryassignment", fieldId: sublistField, line: relInvDetLine, }); if (relSublistVal != undefined) { try { invInvDet.setSublistValue({ sublistId: "inventoryassignment", fieldId: sublistField, line: relInvDetLine, value: relSublistVal, }); log.audit({ title: "Successfully set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } catch (error) { log.error({ title: "Unable to set inventory detail sublist field", details: `${line} : ${sublistField} : ${relSublistVal}`, }); } } else { log.audit({ title: "InvDet Sublist Field Has No Value", details: `${line} : ${sublistField}`, }); } }); } } } } return { beforeSubmit: beforeSubmit, }; }); |
Expected Behaviors
- On the Transfer Order, only TOs marked as “Received” will have the button available. The button will be unavailable for TOs marked otherwise.
- The user will be prompted that the invoice is being created upon clicking the button.
- If the Invoice was successfully created, a notification will appear above the form with a link to the newly-created Invoice.
- The created Invoice will also be accessible from the TO under the Related Records subtab.
- If an Invoice has already been generated for a TO, the user will be alerted and provided a link to the relevant Invoice.
- The Invoice fields are automatically populated upon creation. The Inventory Detail for the items will also be accessible on the Invoice.