I‘ve written before about some customizations that can be made to the catalog checkout screen in ServiceNow. The catalog checkout screen gives users one last opportunity to review their order and provide some additional details about the overall request before submitting an order. One common customization request I’ve heard before is to add additional fields to this checkout screen.
This article is an extension of an idea presented on the ServiceNow wiki that shows one way to approach this problem. The solution described here overcomes some of the problems with the wiki solution and gives a little bit more detail about how the solution works so that it’s easier for you to customize on your own. I’ll also include some formatting examples and show how you can add more than one additional field to the checkout screen.
Add your new field(s) to the checkout screen
1Modify the ‘servicecatalog_cart_template’ UI Macro as follows…
Look for the section of code that has hidden input elements (designated by ‘input type=”HIDDEN”‘) and insert the following line directly after the other hidden elements. This line of code is used to store the unique cart ID for the user session. This ID will be referenced later on in a UI Page client script.
Next, find the section of code that looks like this…
<td width="30%" >
${gs.getMessage('Requested for')}:
</td>
<td width="70%">
<label for="requestor_location">${gs.getMessage('Deliver to')}:</label>
</td>
</tr>
<tr><td>$[SP]</td></tr>
<tr>
<td valign="top">
<j2:if test="$[jvar_can_delta_rf == false]">
$[sc_cart.requested_for.getDisplayValue()]
</j2:if>
<j2:if test="$[jvar_can_delta_rf != false]">
<g2:catalog_requested_for />
</j2:if>
</td>
<td>
<textarea id="requestor_location" style="width: 100%" rows="4"
name="requestor_location" wrap="soft" onChange="catDeliveryAddress('$[sc_cart.sys_id]', 'requestor_location');" >
$[sc_cart.delivery_address]
</textarea>
</td>
</tr>
<tr>
<td>$[SP]</td>
</tr>
And insert the following code directly below it. This sample code adds a ‘Company’ reference field to your checkout form. You’ll need to customize this code to add your own field(s).
<td colspan="2">Company:</td>
</tr>
<tr>
<td>$[SP]</td>
</tr>
<tr>
<td colspan="2">
<g2:ui_reference name="core_company" table="core_company" onchange="setCartValue()"/>
</td>
</tr>
<tr>
<td>$[SP]</td>
</tr>
If you need to add multiple fields you can do so by adding the appropriate html. It’s also pretty simple to change the layout of the fields and their columns if you understand a little bit about html tables. Here’s an example that shows how to add two fields (Company and Requested Date) to the checkout form in a 2-column layout.
<td width="30%">Company:</td>
<td width="70%">Requested Date:</td>
</tr>
<tr>
<td>$[SP]</td>
</tr>
<tr>
<td width="30%">
<g2:ui_reference name="core_company" table="core_company" onchange="setCartValue()"/>
</td>
<td colspan="2">
<g2:ui_date name="requested_date" onchange="setCartValue()"/>
</td>
</tr>
<tr>
<td>$[SP]</td>
</tr>
Passing the field values to the catalog cart and request
2Add the following script to the ‘Client Script’ field on the ‘servicecatalog_checkout_one’ UI Page (note that this sample is for the 2-field checkout addition and you’ll need to modify this script on the ‘var newField1’ and ‘var popVal’ lines if you’re only using one field). Its purpose is to get the values from the new checkout fields as they change and send them to the request ticket when the request is generated. You’ll want to pay attention to the separators used here to designate when a new field/value pair starts. The underlying code reserves some characters in this situation so I made a judgment call based on what I thought would work most of the time. The bottom line is that these need to match what you look for in the business rule in the next step.
If you need to add additional fields to your checkout page you’ll need to customize this function to include those additional field/value combinations.
var fieldSeparator = '^'; //Cannot be ':' or '='
var nameValSeparator = 'IS'; //Cannot be ':' or '='
//Get the new field elements we've added
var newField = gel('core_company');
var newField1 = gel('requested_date');
//Query for the user cart
var cart_item = new GlideRecord('sc_cart_item');
cart_item.addQuery('cart.user', g_user.userID);
cart_item.addQuery('active', 'true');
cart_item.query();
//If found, add the field/value pairs to the 'hints' parameter for the cart
if(cart_item.next()) {
//Aggregate all of the field-value pairs
var popVal = 'company' + nameValSeparator + newField.value + fieldSeparator + 'requested_date' + nameValSeparator + newField1.value;
//Send the values to the request record
cart_item.hints = "<hints><entry key='sysparm_processing_hint' value='setfield:request.u_cartpop=" + popVal + "'/></hints>";
cart_item.update();
}
}
If you’re just adding a single field to the checkout page (and have no plans to potentially add more fields) then you could simply modify the script above to eliminate the ‘u_cartpop’ and ‘popVal’ pieces all together. These pieces exist for the sole purpose of passing multiple field/value pairs on to the request. If you just need a single field, comment out the first ‘popVal’ line, change ‘u_cartpop’ to the name of the field on the request form you need to populate, and use the single field example included in step 1 above.
If you are looking for a more extensible solution that you could add to in the future, or if you already know that you will need more than one field added to the checkout form, read on!
Populating the field values
—This final portion is the major piece that I’ve changed from the solution described on the ServiceNow wiki. The whole purpose of these last steps is to allow you to populate more than one field on the request ticket. The ‘hints’ parameter in the UI Page client script above only accepts a single name/value pair to set. To work around this limitation, the script above concatenates all of the name/value pairs and passes them in as a single string to a custom field you need to create called ‘CartPop [u_cartpop]’.
3Create a new string field on the Request [sc_request] table named ‘CartPop’.
The field will be used to capture the field/value pairs string being passed from the checkout page. It should have a length that is long enough to accommodate all of the field/value pairs being passed in from your checkout page. A 1000 character limit should be more than enough to pass in values from several additional fields. This field can be removed from the request form once it is created.
4Create a new business rule on the Request [sc_request] table with the settings as shown below. The purpose of the business rule is to get the value of the ‘u_cartpop’ field, parse the field/value pairs out of it, and populate those values into the correct field(s) on the request record.
Name: Populate Cart Values
Table: Request [sc_request]
When: before
Insert: true
Condition: !current.u_cartpop.nil()
Script:
var popVals = current.u_cartpop.split("^");
for(x in popVals){
//Split each pair and populate the correct field
var popVal = popVals[x].split("IS");
if(popVal.length == 2){
eval('current.' + popVal[0] + '="' + popVal[1] + '";');
}
}
If you’ve done everything correctly you should be able to order a catalog item, view and populate your new fields on the checkout form, and see those fields populated on the Request record as shown here…
Validating checkout fields / Making checkout fields mandatory
One last step that you may need to take is to make sure that certain checkout fields are filled out or validated before submission. I’ve written about how to do this in a separate article found here.
I can’t make this work for the Watch List. I tried it a few different ways and finally asked SN because I kept getting “undefined”. They told that it wasn’t possible with the watch list at this time, and it would be a fix in a future release. I wanted to mention this because I didn’t want anyone to fight with it as long as I have and not realize this….
I wanted to say thank you for this post. I demoed this today to our ESS team and they loved it. I only had one question come up – is there any way to make the submit not happen if the reference value in the field is not valid? We are using this functionality to get around the watch list issue I noted in my earlier comment, and our field is referencing the sys_user table. I can type Joe Blow if I want and still submit successfully. Is there any kind of “check” I can add to this to force the value to be valid before submit? Thanks again – this is a great post.
You would have to validate the contents of that field before submission. In order to do that, you would have to override the submit function with a custom check of your own on that field. This is the same thing you would do if you wanted to make checkout fields mandatory. This article shows you how that could be done.
https://servicenowguru.wpengine.com/system-ui/ui-macros…
Well – I’m not really interested in making it mandatory – I just want to validate that the value is legitimate. Since it is pulling from the user record, I want to validate that the value is an actual user name and it will be valid when it is copied to the watch list. I realize I’m stretching it, I just thought it may be possible.
I understand that, but the process you would go through would be the same. The simplest way to do what you’re wanting to do is to check the value of the field on submit to see if anything is there. If the field has a valid value, it will store the sys_id of the referenced record.
I need to hide a field from catalog check out screen but, I do not want to modify OOB UI macro (If I touch it, Service-now will not upgrade this piece this from the next upgrade). Where should I run a ‘onLoad’ script, so when it runs, on the fly we hide unwanted fields etc.
The only way to do this without editing the UI macro is to create a global UI script and hide the fields using client-side scripting. While this is possible, I don’t know that I would really recommend it as a better solution since the global UI script would have to run on every form load in your entire system. You can make it pretty lightweight so that it doesn’t really impact anything, but it’s not something you’ll want to make a habit out of. Your global UI script would need to look something like this…
function hideCheckoutFields(){
//Check to make sure we are on the checkout page
if($('sc_cart.do')){
try{
//Remove cart stuff here...
}
catch(e){
//alert('Error');
};
}
}
I have used this technique to add an approver field to the check out page. I use the value entered to populate a “Department Approver” field on the sc_request record. (I am using the ‘simple’ method of setting a single value on the request rather than the more complex method you describe here for populating multiple fields.
It works fine if I create the request as an admin user. If I do it as an itil or end user, the value does not propagate through to the sc_request record. I know that the setCartValue() client script function is working, as I can go and see the cart_item.hints field is set with the correct text.
Do you have any idea why this value would not get propagated through to my target field? I have checked ACLS, and the user can write the target field on the request record. The instance has the high security plugin enabled, and I went and set the “Security manager default behavior in the absence of any ACLs on a table” to “Allow” just in case, but that made no difference.
Do you have any idea what would be stopping this value propagating for any but admin users?
thanks,
margie
It sounds like it’s definitely security-related then. I would set up a test against an instance that isn’t running the high security plugin if you can just to see if it works there. If it’s a high security issue then I’m not sure what the issue might be for sure. You might have to contact someone to take a closer look at your instance to see what’s going on.
Margie,
I just ran into the same issue with the High Security plugin enabled. I was using mutiple added fields on the checkout screen and this technique would not work for non-admin users. I also tried different ACL options, and could not find the right combination to make it work. I eventually modified the code to use an GlideAjax script. Here is how I did it.
UI Page: servicecatalog_checkout_one
Info: I did a couple things different. I am not using the “onchange” event on the fields. My fields are actually pre-polpulated. If someone didn’t change them I wanted to still get the pre-populated info. So the “observe” watches for you to click the “Submit Order” button.
Client Script:
sub.observe('click', sendHintsToCart);
function sendHintsToCart(){
var fieldSeparator = '^'; //Cannot be ':' or '='
var nameValSeparator = 'IS'; //Cannot be ':' or '='
var newField = gel('sc_cart.requested_for.location');
var newField1 = gel('sc_cart.requested_for.phone');
var newField2 = gel('sc_cart.requested_for.pod');
var checkoutValues = 'u_requester_location' + nameValSeparator + newField.value + fieldSeparator + 'u_requester_phone' + nameValSeparator + newField1.value + fieldSeparator + 'u_requester_pod' + nameValSeparator + newField2.value;
var ajax = new GlideAjax('CartUtilityAjax');
ajax.addParam('sysparm_name','setCartItemHints');
ajax.addParam('sysparm_uid', g_user.userID);
ajax.addParam('sysparm_popval', checkoutValues);
ajax.getXML();
}
Script Include: CartUtilityAjax
Info: This just takes some of the SNCGuru example code and runs it on the backend. This made this hints field get populated.
CartUtilityAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
setCartItemHints: function(){
var uid = this.getParameter('sysparm_uid'); //Logged in User ID
var popVal = this.getParameter('sysparm_popval'); // Checkout Values
var fieldSeparator = '^'; //Cannot be ':' or '='
var nameValSeparator = 'IS'; //Cannot be ':' or '='
//Get the new field elements we've added
//Query for the user cart
var cart_item = new GlideRecord('sc_cart_item');
cart_item.addQuery('cart.user', uid);
cart_item.query();
//If found, add the field/value pairs to the 'hints' parameter for the cart
if(cart_item.next()) {
cart_item.hints = "<hints><entry key='sysparm_processing_hint' value='setfield:request.u_cartpop=" + popVal + "'/></hints>";
cart_item.update();
}
}
});
The rest of the process is the same. The business rule etc. This worked for me, hopefully it can help you.
Nice solution! Thanks for sharing. I wish we had better visibility into why high security breaks some things like this.
Thank you guys so much! You rock!
BUT I have a little addition when using Michael Browns Script Include: You need to check the checkbox “Client callable”, otherwise it won’t work.
P.S.: If found a simple way to check required fields on the checkout before submitting, also by using the Client Script field and slightly modifying the submit button. And now with the “observe(‘click’, );” method I have even a better idea! I will create a thread once I completed it.
Thanks for the info Michel! I look forward to seeing your solution once you’ve completed it.
Thanks a lot Michael, I was dealing with this the whole weekend, works great!
For some reason, we occasionally have a request get created without the information from the checkout screen. It’s as if the hints field never gets updated, even though we force setting the value on any changes to any field, as well as upon submission. The biggest headache is we can’t reproduce it at-will to find the root cause. I’m thinking it may be a timing issue or race-condition. Have you seen this behavior before?
Hey Josh,
I haven’t heard of that happening before. If you can manage to reproduce it I can investigate further but until then I’m not sure there’s much I can do.
What happens to the cart item record after the Request is opened? Does it get deleted? If I could find the one linked to the Request, then I could verify if the hints field is getting updated.
It does get deleted. You can see cart information in the ‘sc_cart’ table.
It’s been awhile, but I FINALLY figured out why custom fields added to the checkout screen may not get passed to the request. The root cause is order guides that aren’t fully submitted (user didn’t click Check Out).
When you click Choose Options in an order guide, inactive items are added to the cart (sc_cart_item), one for each item that passes the rule base conditions. They will become active, but only have clicking Check Out. However, if you navigate away form the order guide to a different catalog item without clicking Check Out, the inactive items remain in the cart. If other items are later added to the cart and then the user goes to checkout, there will still be a mix of both active and inactive items in the cart. So when this runs:
var cart_item = new GlideRecord('sc_cart_item');
cart_item.addQuery('cart.user', g_user.userID);
cart_item.query();
You might get an inactive item, which will not be processed by back-end code that creates the Request Items.
In the end this line needs added to the query:
cart_item.addQuery(‘active’, ‘true’);
Problem solved!
Josh, this is awesome! Nice work figuring this out. I just modified the code above to include this change.
I’m having an intermittent issue with this code. I’ve created the AJAX solution since we are using the high security and I’ve added the cart_item.addQuery(‘active’, ‘true); to the ajax. We are also on Eureka, if that makes a difference.
I added gs.logs to the ajax as part of my debugging and found that the uid and popVal are being filled in. I’m not always getting a log statement from in the query before setting the cart_item.hints. So it seems like it’s not always getting the cart_item.
Have you seen this happen before? Is there anything else I should check out in the system?
In a sampling of 20 requests, 8 times the variables did not copy from the checkout screen to the request.
Ok, I think I’ve found a pattern. One of the added fields is a choice list. It seems if I visit the field on the checkout screen, then this works, but if I don’t visit the field (tab to it or click on it), then this breaks. I don’t have the “none” value turned on for the choice list because I don’t want the order to be submitted with a choice being made.
I know this is an old post but it’s still relevant and we’re on Eureka.
Tim,
I have the same problem as you and here’s what I did to fix it.
On the Client Script section of the UI Page: servicecatalog_checkout_one, add the following line at the end:
addLoadEvent(sendHintsToCart);
Essentially I’m updating the cart as soon as the checkout page loads. I’m also adding onChange event calling the same function for each of the variables.
This is a proactive approach rather than reactive i.e. waiting for click. The cart is being deleted after submission so it’s probably a timing issue. We’re trying to update the cart while it’s being deleted.
I notice that the fields are all reference fields. Is there anyway to make a select box referencing a table for its list of options? I’ve been trying to create this and can’t figure it out. The second part would be triggering a second box with based on the selection of the first.
Basically.
Select Box Account type with options Capital or Expense
Then based on that bring up the appropriate accounts
The example above uses a reference field and a date field. You can use a choice field using one of the ‘ui_choice’ macros. You may have to try a few things to get it working just right but other field types are possible.
Hi Mark,
Is there a way to send these variables to the “Requested Item” form instead of the “Request” form? Thanks.
Jesse
They’ll be added to the Request record, but you can personalize the request item form and get to the information by drilling into the ‘Request’ field. You could also use a business rule to copy the information from the parent request to the child request items if you want.
Hey Mark,
I see the line inthe script, “value=’setfield:request.u_cartpop=”, and I assume that I would just sub in “sc_req_item” where it says “request”, but that does not work. The main issue we are having is that we will have separate work order numbers for each requested item, and I need to be able to copy those over to the requested item level.
I have it working as in your example, only issue is with the multiple variables.
Thanks for the quick reply!
Jesse
You can’t substitute ‘sc_req_item’ like that. The cart only populates the request. See my previous post for potential alternatives.
Hi guys,
I created a free text field with ‘ui_input_field’.
We are using the field so the requester can enter the name of a new user (its free text as the new user would not be on the system yet so we cant look it up)
However, I find that when a name contains an apostrophe the screen just hangs. for example. John O’Brien.
Has anyone else seen this?
Does the new Fuji/Geneva cart layout process break this?
I am not changing anything with the new cart layout process but adding this solution into it. I get as far as executing the setCartValue client script. However, no data is being passed to Request. I have triple checked everything, so I am wondering if the new cart process is conflicting or something is still not right in my setup.
How would I do this with the new cart layout process in Fuji and Geneva?
Hi Michael,
I tried your solution that the cart value newly added additional fields are not working for non admin users.
Can you please explain me, the use of first two lines in the below script, since you have mentioned that it will work while clicking on order now button, for my case, it has to work, when I click on “Check out” Button.
Can you help me on this
var sub = $(‘sysverb_insert’);
sub.observe(‘click’, sendHintsToCart);
function sendHintsToCart(){
var fieldSeparator = ‘^’; //Cannot be ‘:’ or ‘=’
var nameValSeparator = ‘IS’; //Cannot be ‘:’ or ‘=’
var newField = gel(‘sc_cart.requested_for.location’);
var newField1 = gel(‘sc_cart.requested_for.phone’);
var newField2 = gel(‘sc_cart.requested_for.pod’);
var checkoutValues = ‘u_requester_location’ + nameValSeparator + newField.value + fieldSeparator + ‘u_requester_phone’ + nameValSeparator + newField1.value + fieldSeparator + ‘u_requester_pod’ + nameValSeparator + newField2.value;
var ajax = new GlideAjax(‘CartUtilityAjax’);
ajax.addParam(‘sysparm_name’,’setCartItemHints’);
ajax.addParam(‘sysparm_uid’, g_user.userID);
ajax.addParam(‘sysparm_popval’, checkoutValues);
ajax.getXML();
}
I am still having an intermittent issue, has any one faced this issue even after adding addLoadEvent(sendHintsToCart); to your client script?