Copy RITM Variables to New Catalog Form
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-08-2025 01:55 AM
I'm working on a Service Portal widget that displays "My Requests" and need to implement a Copy functionality for specific catalog items (iPhone 13 Pro). The requirement is:
- Show a "Copy" button beside RITMs for iPhone 13 Pro catalog items ✅ (Working)
- When clicked, redirect to the catalog item form ✅ (Working)
- Auto-populate the new form with variable values from the original RITM ❌ (Not working)
Current Implementation
Widget Client Script (Copy Button)
$scope.copyRequest = function(ritmSysId, catalogItemSysId) { var copyUrl = '/sp?id=sc_cat_item&sys_id=' + catalogItemSysId + '©_from=' + ritmSysId; window.open(copyUrl, '_blank'); };
Widget HTML Template
<div ng-if="item.cat_item_name == 'iPhone 13 Pro'"> <button class="btn btn-primary btn-sm" ng-click="copyRequest(item.sys_id, item.cat_item_sys_id)" title="Copy this request"> <i class="fa fa-copy"></i> Copy </button> </div>
Catalog Item Client Script (onLoad)
function onLoad() { function getParameterByName(name) { var url = window.location.href; name = name.replace(/[\[\]]/g, '\\$&'); var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); var results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, ' ')); } var ritmId = getParameterByName('copy_from'); if (ritmId) { var ga = new GlideAjax('CatalogPrefillUtil'); ga.addParam('sysparm_name', 'getPrefillData'); ga.addParam('sysparm_ritm_id', ritmId); ga.getXMLAnswer(function(response) { try { console.log('📦 Raw response from GlideAjax:', response); var values = JSON.parse(response); // Set variables here - use exact *Name* of variables (not label!) if (values.monthly_data_allowance) g_form.setValue('monthly_data_allowance', values.monthly_data_allowance); if (values.storage) g_form.setValue('storage', values.storage); } catch (e) { console.error('❌ Error parsing or applying variable values:', e); } }); } }
Script Include (Client Callable)
var CatalogPrefillUtil = Class.create(); CatalogPrefillUtil.prototype = { initialize: function() {}, getPrefillData: function() { var ritmId = gs.getParameter('sysparm_ritm_id'); var result = {}; var ritm = new GlideRecord('sc_req_item'); if (ritm.get(ritmId)) { var vars = ritm.variables; result.monthly_data_allowance = vars.monthly_data_allowance + ''; result.storage = vars.storage + ''; } return JSON.stringify(result); }, type: 'CatalogPrefillUtil' };
Issue Encountered
When I test the Script Include in Scripts - Background:
var util = new CatalogPrefillUtil(); gs.setParameter('sysparm_ritm_id', 'ACTUAL_RITM_SYS_ID'); var result = util.getPrefillData(); gs.log('Test result: ' + result);
Result:
*** Script: 🔍 DEBUG: Starting getPrefillData for RITM: undefined*** Script: ❌ RITM not found: undefined*** Script: 📤 Final result: {"error":"RITM not found"}
The RITM ID is coming through as undefined, suggesting the parameter isn't being passed correctly.
Questions
- What's the correct way to access catalog variables from a RITM record? Is ritm.variables.variable_name the right approach?
- How should I properly retrieve variable values from sc_req_item records? Should I be querying sc_item_option_mtom instead?
- Is there a better way to pass parameters from Service Portal to catalog client scripts?
- Are there any known issues with gs.getParameter() in Script Includes when called from Service Portal?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-08-2025 03:33 AM
so onLoad of other catalog item you are not able to get RITM sysId which you included in URL?
did you check what came in URL?
use this and see if you get RITM sysId
function onLoad() {
var url = top.location.href;
alert(url);
var ritmSysId = new URLSearchParams(url).get("copy_from");
alert(ritmSysId);
}
If my response helped please mark it correct and close the thread so that it benefits future readers.
Ankur
✨ Certified Technical Architect || ✨ 9x ServiceNow MVP || ✨ ServiceNow Community Leader
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-08-2025 05:16 AM
Hi ankur, I made changes as per your suggestion but still nothing happens when I click on copy. Below are the scripts, can you please identify the issue so we can fix this. I would greatly appreciate your help on this. Widget Client script -
(function() {
// ─── HANDLE “ADD_TO_CART” ACTION ────────────────────────
if (input && input.action === 'add_to_cart') {
try {
var util = new CatalogPrefillUtil();
var pre = util.getPrefillData(input.orig_sys_id);
var cart = new sn_sc.CatalogCart();
cart.addItem(pre.cat_item, 1, pre.vars);
cart.save();
data.newCartItemId = cart.request_item.sys_id;
} catch (e) {
data.error = '' + e;
}
return; // skip standard widget logic
}
data.id = $sp.getParameter('id');
var localInput = input;
var itemsObj;
// ─── Drafts: remove one draft ────────────────────────────
if (localInput && localInput.action === "remove_item") {
var cartRecord = new sn_sc.CartJS('draft_items');
cartRecord.remove(localInput.removeItemID);
itemsObj = getDraftItems(localInput.prevLimit);
data.draftItems = itemsObj.items;
data.hasMoreDrafts = itemsObj.hasMore;
return;
}
// ─── Drafts: load more ───────────────────────────────────
if (localInput && localInput.action == "fetch_more_draft_items") {
itemsObj = getDraftItems(localInput.prevLimit + 100);
data.draftItems = itemsObj.items;
data.hasMoreDrafts = itemsObj.hasMore;
return;
}
var alsoRequest = false;
var msg = data.messages = {};
msg.myRequestsTitle = options.title ? gs.getMessage(options.title) : gs.getMessage('My Requests');
msg.openRequests = gs.getMessage('Open requests');
msg.closedRequests = gs.getMessage('Closed requests');
msg.showMoreRequests = gs.getMessage('Show More Requests');
data.filterMsg = gs.getMessage("Search open requests");
data.draftFilterMsg = gs.getMessage("Search draft items");
data.draftItemsMsg = gs.getMessage("Draft Items");
data.deleteDraftItemMsg = gs.getMessage("Are you sure you want to delete the draft item?");
data.dialogCancel = gs.getMessage('Cancel');
data.dialogDelete = gs.getMessage('Delete draft');
data.draftSearchText = $sp.getParameter("draftSearchText");
var selectDraftTab = $sp.getParameter('selectDraftTab');
if (gs.nil(localInput)) {
data.isRequestsTabActive = !data.draftSearchText && !selectDraftTab;
data.isDraftsTabActive = !!data.draftSearchText || selectDraftTab;
} else {
data.isRequestsTabActive = true;
data.isDraftsTabActive = false;
}
if (gs.nil(data.draftSearchText))
data.draftSearchText = "";
else
data.draftSearchText = decodeURIComponent(data.draftSearchText);
data.hide_draft_tab = (gs.getProperty('glide.sc.disable.save_as_draft') == 'true') ||
(gs.getProperty('glide.sc.enable.save_as_draft.portal.' + $sp.getPortalRecord().getValue("url_suffix")) != 'true');
var recordTable = options.record_table || $sp.getParameter("table");
var recordId = options.record_id || $sp.getParameter("sys_id");
data.is_associated_ticket_tab = options.is_associated_ticket_tab;
if (localInput && localInput.view === 'open')
data.filterMsg = gs.getMessage("Search open requests");
else if (localInput && localInput.view === 'close')
data.filterMsg = gs.getMessage("Search closed requests");
data.is_new_order = (($sp.getParameter("is_new_order") + '') === "true");
data.requestSubmitMsg = gs.getMessage('Thank You. Your request has been submitted');
var draftItemsObj = getDraftItems(100);
data.draftItems = draftItemsObj.items;
data.hasMoreDrafts = draftItemsObj.hasMore;
function getDraftItems(limit) {
var userID = gs.getUser().getID();
var cart = new SPCart("draft_items", userID);
if (!gs.nil(cart) && typeof cart.getItemsWithPagination === "function")
itemsObj = cart.getItemsWithPagination('sys_updated_on', limit);
else {
gs.info("Drafts tab hidden: unable to fetch draft cart.");
data.hide_draft_tab = true;
itemsObj = {};
}
return itemsObj;
}
function getField(gr, name) {
var f = {};
var id = gr.getUniqueValue();
gr = new GlideRecord(gr.getRecordClassName());
gr.get(id);
f.display_value = gr.getDisplayValue(name);
f.value = gr.getValue(name);
var ge = gr.getElement(name);
if (ge) {
var ed = ge.getED();
if (ed) f.type = ed.getInternalType();
f.label = ge.getLabel();
}
return f;
}
function getMyRequestSysIds() {
var ids = {};
var rq = new GlideRecord('request_filter');
rq.addActiveQuery();
if (rq.isValidField('applies_to'))
rq.addQuery('applies_to', 1).addOrCondition('applies_to', 10);
rq.query();
while (rq.next()) {
var tableName = rq.isValidField('table') ? rq.getValue('table') : rq.table_name;
var gr2 = new GlideRecord(tableName);
if (!gr2.isValid()) continue;
gr2.addQuery(rq.filter);
gr2.query();
if (tableName == 'sc_request') alsoRequest = true;
while (gr2.next()) {
ids[gr2.sys_id + ''] = {
page: rq.portal_page.nil() ? '' : rq.portal_page.getDisplayValue() + '',
primary_display: rq.primary_display.nil() ? '' : rq.primary_display + '',
secondary_displays: rq.secondary_display.nil() ? '' : rq.secondary_display + ''
};
}
}
return ids;
}
// ─── Build My Requests List ─────────────────────────────────
var myRequestMap = getMyRequestSysIds();
var taskIDs = Object.keys(myRequestMap);
var gr = new GlideRecordSecure('task');
// apply open/close or associated ticket filters
if (!data.is_associated_ticket_tab) {
if (localInput && localInput.view === 'open') gr.addActiveQuery();
else if (localInput && localInput.view === 'close') gr.addQuery('active', 0);
else gr.addActiveQuery();
} else {
// your existing associated ticket logic...
// (unchanged)
}
gr.orderByDesc('sys_updated_on');
// text search across task & RITM if needed
if (localInput && localInput.search_text) {
var req = [];
var task = new GlideRecordSecure('task');
task.addQuery('123TEXTQUERY321', localInput.search_text);
if (localInput && localInput.view === 'open') task.addQuery('active', 1);
else if (localInput && localInput.view === 'close') task.addQuery('active', 0);
else task.addQuery('active', 1);
task.addQuery('sys_id', taskIDs);
task.query();
while (task.next()) req.push(task.getUniqueValue());
var ritmGR = new GlideRecord('sc_req_item');
if (alsoRequest && ritmGR.isValid()) {
if (localInput && localInput.view === 'open') ritmGR.addQuery('request.active', 1);
else if (localInput && localInput.view === 'close') ritmGR.addQuery('request.active', 0);
else ritmGR.addQuery('request.active', 1);
ritmGR.addQuery('123TEXTQUERY321', localInput.search_text);
ritmGR.addQuery('request.sys_id', taskIDs);
ritmGR.query();
while (ritmGR.next()) req.push(ritmGR.getValue('request'));
}
gr.addQuery('sys_id', req);
} else
gr.addQuery('sys_id', taskIDs);
gr.query();
data.request = {};
data.request.req_list = [];
var recordIdx = 0;
var limit = options.items_per_page ? options.items_per_page : 15;
data.lastLimit = (localInput && localInput.action == 'fetch_more')
? localInput.lastLimit + limit
: limit;
data.hasMore = false;
// loop through results, up to lastLimit
while (recordIdx != data.lastLimit && gr.next()) {
var portalSettings = myRequestMap[gr.getUniqueValue()] || {};
var record = {};
record.sys_id = gr.getValue('sys_id');
// ─── sc_request branch ─────────────────────────────
if (gr.getRecordClassName() == 'sc_request') {
var ritm = new GlideRecord("sc_req_item");
ritm.addQuery("request", gr.getUniqueValue());
ritm.query();
if (ritm.getRowCount() == 0) continue;
if (ritm.getRowCount() > 1) {
record.display_field = gs.getMessage("{0} requested items", ritm.getRowCount());
} else {
ritm.next();
record.display_field = ritm.getDisplayValue("short_description") || ritm.cat_item.getDisplayValue();
}
record.url = {
id: portalSettings.page || 'sc_request',
table: 'sc_request',
sys_id: record.sys_id
};
record.request_due_date = ritm.getValue('u_request_due_date');
record.cat_item = ritm.getDisplayValue('cat_item');
// ─── NEW: copy button flags ───────────────────
var copyCheck = new GlideRecord('sc_req_item');
copyCheck.addQuery('request', gr.getUniqueValue());
copyCheck.addQuery('cat_item', '73b1bafa9752cd1021983d1e6253afb5');
copyCheck.setLimit(1);
copyCheck.query();
if (copyCheck.next()) {
record.show_copy = true;
record.iphone_ritm_id = copyCheck.getUniqueValue();
} else {
record.show_copy = false;
record.iphone_ritm_id = '';
}
// ────────────────────────────────────────────────
} else {
// non-sc_request tasks (unchanged)
record.display_field = portalSettings.primary_display
? getField(gr, portalSettings.primary_display).display_value
: getField(gr, 'number').display_value;
record.url = {
id: portalSettings.page || 'ticket',
table: gr.getRecordClassName(),
sys_id: record.sys_id
};
}
// common fields
record.display_number = getField(gr, 'number').display_value || '';
if (portalSettings.secondary_displays) {
record.secondary_displays = [];
var secs = portalSettings.secondary_displays.split(",");
for (var i=0; i<secs.length; i++)
record.secondary_displays.push(getField(gr, secs[i]));
} else {
record.secondary_displays = getField(gr, 'short_description');
}
record.updated_on = gr.getValue('sys_updated_on');
record.state = gr.getDisplayValue('state');
if ((recordIdx !== 0) && (data.lastLimit - limit === recordIdx))
record.highlight = true;
data.request.req_list.push(record);
recordIdx++;
}
if (gr.next())
data.hasMore = true;
console.log("has more :", data.hasMore);
})();
HTML of Widget-
<div class="panel panel-default b" ng-init="c.trackPage()">
<div class="panel-heading" ng-show="::!data.is_associated_ticket_tab">
<h2 class="panel-title">{{::data.messages.myRequestsTitle}}</h2>
</div>
<uib-tabset role="tablist" ng-hide="::c.data.hide_draft_tab">
<uib-tab role="tab"
active="c.data.isRequestsTabActive"
select="c.changeSelectedTab('requests')"
index="requests"
class="uib-vis-tab"
heading="${Submitted requests}">
<div ng-include="'myRequests'"></div>
</uib-tab>
<uib-tab role="tab"
active="c.data.isDraftsTabActive"
select="c.changeSelectedTab('drafts')"
index="drafts"
class="uib-vis-tab"
heading="${Drafts}">
<div ng-include="'draftItemsTemplate'"></div>
</uib-tab>
</uib-tabset>
<div ng-include="'myRequests'" ng-show="::c.data.hide_draft_tab"></div>
</div>
<script type="text/ng-template" id="draftItemsTemplate">
<div class="panels-container list-group">
<!-- existing draftItemsTemplate content -->
</div>
</script>
<script type="text/ng-template" id="myRequests">
<div class="panels-container list-group">
<!-- Search / Filter Header -->
<div ng-show="::!data.is_associated_ticket_tab" class="list-group-item row requests-header-container">
<div class="col-md-3 col-xs-12 m-b-sm fit-content">
<div class="form-inline control-view" ng-if="c.options.show_view == 'true'">
<label class="control-label hidden-xs wrapper-xs" id="label_view" for="view">${View}</label>
<select ng-model="c.viewFilter"
id="view"
class="form-control adjust-width"
ng-change="c.changeView()"
style="width:80%"
ng-options="item.key as item.value for item in c.filterOptions">
</select>
</div>
</div>
<div class="col-md-4 col-xs-12 padding-left-large fit-content">
<div class="input-group" style="width:100%">
<input ng-model="c.filterText"
ng-keypress="c.checkEnter($event)"
class="form-control"
style="width:100%"
placeholder="{{data.filterMsg}}"
aria-label="{{data.filterMsg}}">
<span class="input-group-btn">
<button class="btn btn-default align-icon"
type="button"
ng-click="c.search()"
data-original-title="{{data.filterMsg}}"
aria-label="{{data.filterMsg}}"
data-toggle="tooltip"
data-placement="bottom">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
</div>
<!-- No Results / Search Feedback -->
<div ng-if="c.data.request.req_list.length == 0 && !c.filterText" class="panel-body panels-container">
${You do not have any requests}
</div>
<div ng-if="c.data.request.req_list.length == 0 && c.filterText" class="panel-body panels-container">
${Search didn't match any requests}
</div>
<!-- Requests Table -->
<div role="table"
ng-if="c.data.request.req_list.length > 0"
class="table"
aria-label="{{::data.messages.myRequestsTitle}}">
<div ng-show="::!data.is_associated_ticket_tab"
role="rowgroup"
class="column-headers">
<div role="row" class="list-group-item table-responsive">
<span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${Request}</span>
<span role="columnheader" class="col-xs-3 padder-r-none padder-l-none">${Request Delivery Date}</span>
<span role="columnheader" class="col-xs-2 padder-r-none padder-l-none">${State}</span>
<span role="columnheader" class="col-xs-2 padder-r-none padder-l-none">${updated_capital}</span>
<span role="columnheader" class="col-xs-2 padder-r-none padder-l-none"></span>
</div>
</div>
<ul role="rowgroup" class="padder-l-none padder-r-none">
<li role="row"
class="list-group-item table-responsive"
ng-repeat="item in c.data.request.req_list | limitTo: c.data.lastLimit track by item.sys_id"
style="margin:0px">
<!-- 1) Main Column: Request Link -->
<div role="cell" class="col-xs-3 padder-l-none padder-r-none main-column">
<div class="primary-display">
<a href="?id={{::item.url.id}}&table={{::item.url.table}}&sys_id={{::item.url.sys_id}}"
sn-focus="{{::item.highlight}}"
aria-label="{{::item.display_field}} , {{::item.display_number}}">
{{::item.display_field}}
</a>
</div>
<small class="text-muted">
<div ng-repeat="f in item.secondary_displays" class="secondary-display">
<span>{{::f.display_value}}</span>
</div>
</small>
</div>
<!-- 2) Delivery Date -->
<div role="cell" class="col-xs-3 padder-l-none padder-r-none state-column">
<div class="request_due_date">
<span>{{::item.request_due_date}}</span>
</div>
</div>
<!-- 3) State -->
<div role="cell" class="col-xs-2 padder-l-none padder-r-none state-column">
<div class="state">
<span>{{::item.state}}</span>
</div>
</div>
<!-- 4) Updated Timestamp -->
<div role="cell" class="col-xs-2 padder-l-none padder-r-none updated-column">
<div class="updated">
<i class="fa fa-clock-o" aria-hidden="true" title="${Updated}"></i>
<sn-time-ago timestamp="::item.updated_on"/>
</div>
</div>
<!-- 5) Copy Button (iPhone 13 Pro only) -->
<div role="cell" class="col-xs-2 padder-l-none padder-r-none">
<div class="copy-but" ng-if="item.show_copy">
<button ng-click="copyRequest(item.sys_id, item.cat_item_sys_id)">Copy</button>
</div>
</div>
</li>
</ul>
</div>
<!-- Load More Requests -->
<div class="col-sm-12 pull-none" ng-if="c.data.hasMore" style="padding-bottom:15px">
<div class="text-a-c" ng-if="c.fetching">
<i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
<span class="sr-only">${Loading more requests}</span>
</div>
<button class="btn btn-default btn-show-more"
ng-click="c.loadMore()">
{{::data.messages.showMoreRequests}}
</button>
</div>
</div>
</script> I have written below onload client script on catalog item -
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-08-2025 05:18 AM
Thank you for marking my response as helpful.
Sorry but it's a big code and I don't have that much time to investigate.
I already shared the working solution if you are passing the correct value in URL as parameter then script I shared should help you get the value.
I hope you can enhance it further.
If my response helped please mark it correct and close the thread so that it benefits future readers.
Ankur
✨ Certified Technical Architect || ✨ 9x ServiceNow MVP || ✨ ServiceNow Community Leader
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-08-2025 05:29 AM
I understand and thank you for your response, can you at least correct this client script? This will help me.