We've updated the ServiceNow Community Code of Conduct, adding guidelines around AI usage, professionalism, and content violations. Read more

Implemented a solution for Inserting multiple attachments through a order guide.

riteshkoranga
Tera Contributor

Hello,

I implemented this solution regarding uploading multiple attachments to an order guide through which all of these attachments should be copied to all of the RITMs that were created by the submission of order guide.


I am posting this because I didn't find any solution related to this, specifically all of the multiple attachment upload solutions were only for catalog items and record producer.
The solutions they provided was not working for order guides. And some also specified that their solution will not work for order guides.

Like this solution: serviceportal.io

I hope this helps for order guides.

So, the customer requirement was, they wanted an order guide related to HR and in it a attachment functionality that can upload multiple attachements.
And when the order guide is submitted, all of these attachments should be copied to each of the RITMs created in the process.

Solution: This my engineered solution, according to my thought process. It may not be the best or most optimized, but it works.

 

My solution architecture:
1. An order guide with multiple variables, and one variable with custom type (in will load our widget in this)

2. A custom widget for this order guide, it will handle our attachments. And this widget will also copy the attachments to the request created by order guide.
3. logic: 

  • Attachments attached -> attachments staged -> order guide submitted -> fetch the request sys id upon submission -> chang the attachements table id to the request sysid that we have fetched and copy the attachments to request.
  • After order guide submission -> one request and mutiple RITMs created -> we will put a after Insert business rule on RITM (sc_req_item) table that will have a condition if the current record is created from a specific order guide (ritm.order_guide) -> call an event - script action from BR (why? will explain below) -> copy all of the attachments to all of the RITMs.

 
Custom Widget:
HTML code

<div class="sp-req-attachments">
  

  
  <label ng-if="data.attachment_sys_id"
         class="padding-top-s"
         style="font-weight:normal;cursor:pointer;">
    <!-- Use modal="true" for the standard SP experience -->
    <sp-attachment-button modal="true"></sp-attachment-button>
 
    <span class="m-l-xs">${Add attachments}</span>
  </label>
  
  <!-- Attachment list -->
  <div class="attachment-list" ng-if="data.attachment_sys_id">
    <now-attachments-list template="sp_attachment_single_line" class="padder-b"></now-attachments-list>
  </div>

  <!-- Fallback when no request is available -->
  <div ng-if="!data.attachment_sys_id" class="text-muted">
    ${No request selected.}
  </div>
</div>

Client script
I have put a listener on rootscope that listens to the submission of the current order guide, i have placed checks to make sure it only runs for order guide with the current cart sysID,
And below the listener we have attachment handler and messages, it uploads the attachments to sys_attachment table with the current cart sysId as tableSysId temporarily, we will change the tableSysId after order guide submission.
I took this code of attachment handler from OOB widget Catalog Checkout, removed the unnecessary code and only kept this attachment uploader/handler.

function ($scope,$rootScope, nowAttachmentHandler, spUtil, spAriaUtil, spModal, $log) {
  
	
	$scope.result={};
	var off=$rootScope.$on('$sp.sc_order_guide.submitted', function (e, r) {
		
  	//$scope.result=r;
  	//console.log("Result:",$scope.result);
		
		if(r&r.cart_id !== $scope.data.attachment_sys_id){
			return;
		}
		
		
		var reqId = r&&r.sys_id;
		var cartId = $scope.data.attachment_sys_id;
		
		//console.log("resID: ",reqId);
		//console.log('cartId: ',cartId);
		
		if(reqId&&cartId){
			$scope.server.get({
				action:"relink_cart_attachments_to_req",
				cart_sys_id:cartId,
				request_sys_id:reqId,
				
			}).then(function (resp) {
					//console.log("[OG] copy status:", resp && resp.data && resp.data.copy_status);
				}, function (err) {
					console.warn("[OG] copy failed:", err);
				});

		}
		
		
	off();
	});
	
	
  $scope.m = $scope.data.msgs;

  
  $scope.setFocusToAttachment = angular.noop;
  $scope.setFocusToAttachmentButton = angular.noop;

  // Show an error when a file is larger than the instance limit
  $scope.$on('dialog.upload_too_large.show', function () {
    $log.error($scope.m.largeAttachmentMsg);
    spUtil.addErrorMessage($scope.m.largeAttachmentMsg);
  });

  // Initialize attachment handler for sc_request
  var ah = $scope.attachmentHandler = new nowAttachmentHandler(function (attachments, action) {
    $scope.attachments = attachments;

    if (action === "added")
      $scope.setFocusToAttachment();
    if (action === "renamed")
      spAriaUtil.sendLiveMessage($scope.m.renameSuccessMsg);
    if (action === "deleted")
      spAriaUtil.sendLiveMessage($scope.m.deleteSuccessMsg);

    
    spUtil.get($scope, { action: "from_attachment" });
  }, function (error) {
    spUtil.addErrorMessage(error.msg + error.fileName);
  });

  // Configure the target record & size limit
  // NOTE: maxAttachmentSize is in MB; handler expects BYTES
  var table = 'sc_request';
  var sysId = $scope.data.attachment_sys_id; 
  var maxBytes = 1024 * 1024 * $scope.data.maxAttachmentSize;

  ah.setParams(table, sysId, maxBytes);

  //  load the attachment list
  $scope.attachmentHandler.getAttachmentList();
	

  // Confirm delete 
  $scope.confirmDeleteAttachment = function (attachment) {
    spModal.confirm($scope.data.msgs.delete_attachment).then(function () {
      $scope.attachmentHandler.deleteAttachment(attachment);
      $scope.setFocusToAttachmentButton();
    });
  };

		

}


Server Script: 

I am fetching cart details in code and passing them to client.

I have created a function for the listener we have in the client script, this function will fetch the attachments from sys_attachment table and change the sysId from cartSysId to ReqSysId that we are getting from listener.
This will attach the attachment to the created request.

(function () {
  var localInput = input;
	

  
  if (localInput && localInput.action === "from_attachment")
    return;

  // Messages used by the client
  var m = data.msgs = {};
  m.renameSuccessMsg   = gs.getMessage("Attachment renamed successfully");
  m.deleteSuccessMsg   = gs.getMessage("Attachment deleted successfully");
  m.delete_attachment  = gs.getMessage("Delete Attachment?");
  m.largeAttachmentMsg = gs.getMessage(
    "Attached files must be smaller than {0} - please try again",
    "" + getMaxSizeMB() + "MB"
  );

  // Provide the instance limit (MB). Client converts to bytes.
  data.maxAttachmentSize = getMaxSizeMB();

  
 
var cartName = options.cart_name || input.cart.name || '';
var cartJS = new sn_sc.CartJS(cartName, gs.getUserID());
data.cart = cartJS.getCartDetails(false);

//data.attachment_table = 'sc_cart';
data.attachment_sys_id = data.cart.sys_id;
	
	

	
if (input && input.action === "relink_cart_attachments_to_req") {
  (function () {
    var out = { ok: false, updated: 0, msg: "" };
    try {
      var cartId = String(input.cart_sys_id || "");
      var reqId  = String(input.request_sys_id || "");

      if (!cartId || !reqId) {
        console.log("Missing cart_sys_id or request_sys_id");
      } else {
				//console.log("inside function");
        var att = new GlideRecord('sys_attachment');
        
        att.addQuery('table_name', 'sc_request');
        att.addQuery('table_sys_id', cartId);
        att.query();

        while (att.next()) {
          att.setValue('table_sys_id', reqId);  
          att.update();
          out.updated++;
        }

        
        //console.log("Re-linked " + out.updated + " attachment(s) to sc_request " + reqId);
      }
    } catch (e) {
     console.log("Relink error: " + e);
      gs.warn(out.msg);
    }
    
  })();
	
  return; 
}


  //data.attachment_table = 'sc_request';
  //data.attachment_sys_id = reqSysId;

  function getMaxSizeMB() {
    var mb = parseInt(gs.getProperty("com.glide.attachment.max_size", 1024), 10);
    if (isNaN(mb)) mb = 24;
    return mb;
  }
})();


This was the widget part, and we will attach this widget to custom type variable in order guide.
pic 1.pngorder guide 2.png

Business rule for this: Applied on RITM table upon creation 
It only runs for this specific order guide due the conditional check and ignores RITMs created from other order guides.

 

When to run: After Insert

(function executeRule(current, previous /*null when async*/) {

	var orderSysId = current.getValue('order_guide');

	// if (orderSysId === "f560362cc35fb65460389f3ed4013129") {
	// 	gs.log("Match found!");
	// }

	if(orderSysId === "f560362cc35fb65460389f3ed4013129"){
		var reqSysId = current.getValue('request');
		var curSysId= current.getUniqueValue();

		//gs.log('inside BR'+"Request id: "+reqSysId +"ritm id: "+curSysId);
		
		gs.eventQueue('copy_request_attachments_to_ritm', current, reqSysId, curSysId, 'copy attachment');
		
		
	}
})(current, previous);




 





There was a race condition due to BR, the BR on RITM was running even before attachments were getting attached to the request. There was slight delay in the attachment process and BR was reading into request with no attachments attached.
To solve this, I created a event script action that runs after a certain interval, or wait by gs.sleep() (Update : This is not good practice , you can use gs.eventQueueScheduled() for this, gs.sleep() can withhold the entire thread for a set time.)

Script action code:

Event Name: 

(function () {
  
  var reqId  = String(event.parm1 || '');
  var ritmId = String(event.parm2 || '');

  if (!reqId || !ritmId) {
    gs.log('[Event] Missing parameters. reqId=' + reqId + ', ritmId=' + ritmId);
    return;
  }

//   gs.info('[Event] Start: check/wait for REQ attachments. req=' + reqId + ', ritm=' + ritmId);

  function hasReqAttachments() {
    var att = new GlideRecord('sys_attachment');
    att.addQuery('table_name', 'sc_request');
    att.addQuery('table_sys_id', reqId);
    att.setLimit(1);
    att.query();
    return att.hasNext();
  }

  // Optional wait: up to ~15s 
  var tries = 0, max = 30;
  while (tries++ < max && !hasReqAttachments()) {
    gs.sleep(500); 
  }

  if (hasReqAttachments()) {
    // gs.info('[Event] Attachments found on REQ; copying to RITM. req=' + reqId + ', ritm=' + ritmId);
    try {
      new GlideSysAttachment().copy('sc_request', reqId, 'sc_req_item', ritmId);
    //   gs.info('[Event] Copied REQ → RITM attachments. req=' + reqId + ', ritm=' + ritmId);
    } catch (e) {
      gs.error('[Event] Attachment copy failed. req=' + reqId + ', ritm=' + ritmId + ', err=' + e);
    }
  } else {
    gs.info('[Event] No REQ attachments after wait; skipping. req=' + reqId + ', ritm=' + ritmId);
  }
})();



Final result:

pic 1.pngpic2.pngpic3.pngpic4.png
pic5.pngpic6.pngpic7.pngpic8.png

 

I made this solution because I could not find solution regarding order guide attachments in internet.

This there any better way to do this?

0 REPLIES 0