Name

global.StdChangeUtilsSNC

Description

This function contains utility functions related to Standard Change Request. Public functions - publishNewStandardChangeProposal modifyExistingStandardChangeProposal retireExistingStandardChangeProposal reviewedFieldsAcl getVersionNumber getVersionName getTemplNCategoryForModify getValue validateTemplateValueCompliance updateVersionStats rejectProposal validateCategorisation rollBackApproval getActionURL Customer overridable private functions - _addQueryAllClosedChanges _addQueryAllUnsuccessfulChanges

Script

var StdChangeUtilsSNC = Class.create();
StdChangeUtilsSNC.prototype = Object.extendsObject(AbstractAjaxProcessor, {
  TABLE_NAME_CHANGE: 'change_request',
  TABLE_NAME_CHANGE_TASK: 'change_task',
  TABLE_NAME_PROPOSAL: 'std_change_proposal',
  TABLE_NAME_VERSION: 'std_change_producer_version',
  TABLE_NAME_TEMPLATE: 'std_change_template',
  TABLE_NAME_PRODUCER: 'std_change_record_producer',
  TABLE_NAME_RELATED_TASK: 'std_change_proposal_task',
  TABLE_NAME_PROPERTIES: 'std_change_properties',
  TABLE_NAME_CATALOG: 'sc_catalog',
  TABLE_NAME_CATEGORY: 'sc_category',
  DEFAULT_CHG_TYPE: 'standard',
  DEFAULT_CHG_MODEL: 'e55d0bfec343101035ae3f52c1d3ae49',
  APP_MODULE_STD_CHG_CAT: '022f2aa293b002002dcef157b67ffb15',
  ATTR_CATALOG: 'catalog',
  ATTR_CATEGORY: 'category',
  ATTR_CLOSE_CODE: 'close_code',
  ATTR_STD_CHG_PRODUCER: 'std_change_producer',
  ATTR_STATE: 'state',
  ATTR_STD_CHG_PRODUCER_VERSION: 'std_change_producer_version',
  ATTR_TYPE: 'type',
  ATTR_CHG_MODEL: 'chg_model',
  ATTR_INTERNAL_NAME: 'internal_name',
  INTERCEPTOR_STD_CHG: '8db74e90c611227400d47c65b1076db8',
  PROP_ROW_KEY: 'main_config',
  PROP_MANDATORY_FIELDS: 'mandatory_fields',
  PROP_UNMODIFIABLE_FIELDS: 'restricted_fields',
  PROP_FIELDS_TO_COPY: 'fields_to_copy',
  PROP_DEFAULT_VALUES: 'default_values',
  PROP_READONLY_FIELDS: 'readonly_fields',
  PROP_TWO_STEP: 'two_step',
  SYS_PROP_PREVENT_INACTIVE_INSERT: 'com.snc.change_request.standard_change.abort_insert_inactive_template',
  PROPOSE_STD_CHANGE_RP_NEW: 'cb2a927f935002003b7a7a75e57ffb4c',
  PROPOSE_STD_CHANGE_RP_MODIFY: '32b19f3b9fb002002920bde8132e7024',
  PROPOSE_STD_CHANGE_RP_RETIRE: '011f117a9f3002002920bde8132e7020',
  ROLE_CHG_MANAGER: 'change_manager',
  STATE_NEW: 1,
  STATE_WIP: 2,
  STATE_CLOSED: 3,
  STATE_CANCELLED: 4,
  PROPOSAL_TYPE_NEW: 1,
  PROPOSAL_TYPE_MODIFY: 2,
  PROPOSAL_TYPE_RETIRE: 3,
  APPROVAL_STATE_NOT_REQUESTED: 'not requested',
  APPROVAL_STATE_APPROVED: 'approved',
  APPROVAL_STATE_REJECTED: 'rejected',
  CLOSE_CODES_UNSUCCESSFUL: ['unsuccessful'],

  initialize: function(request, responseXML, gc) {
  	AbstractAjaxProcessor.prototype.initialize.call(this, request, responseXML, gc);
  	this.log = new GSLog('com.snc.std_change_request.log', 'StdChangeUtilsSNC');
  	this.arrayUtil = new ArrayUtil();
  	this.TABLE_LABEL_CHANGE = new GlideRecord(this.TABLE_NAME_CHANGE).getLabel();
  },

  isNew: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.state - 0 === this.STATE_NEW;
  },

  isWIP: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.state - 0 === this.STATE_WIP;
  },

  isClosed: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.state - 0 === this.STATE_CLOSED;
  },

  isCancelled: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.state - 0 === this.STATE_CANCELLED;
  },

  isNewTemplate: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.proposal_type - 0 === this.PROPOSAL_TYPE_NEW;
  },

  isModifyTemplate: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.proposal_type - 0 === this.PROPOSAL_TYPE_MODIFY;
  },

  isRetireTemplate: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.proposal_type - 0 === this.PROPOSAL_TYPE_RETIRE;
  },

  isApproved: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.approval + "" === this.APPROVAL_STATE_APPROVED;
  },

  isRejected: function(proposalGr) {
  	if (!this._isValidRecord(proposalGr))
  		return false;
  	return proposalGr.approval + "" === this.APPROVAL_STATE_REJECTED;
  },

  ajaxFunction_isProposalApproved: function() {
  	var proposalId = this.getParameter('sysparm_proposalSysId');
  	if (!proposalId)
  		return false;

  	var gr = new GlideRecordSecure(this.TABLE_NAME_PROPOSAL);
  	if (gr.get(proposalId))
  		return this.isApproved(gr);

  	return false;
  },

  ajaxFunction_isProposalRejected: function() {
  	var proposalId = this.getParameter('sysparm_proposalSysId');
  	if (!proposalId)
  		return false;


  	var gr = new GlideRecordSecure(this.TABLE_NAME_PROPOSAL);
  	if (gr.get(proposalId))
  		return this.isRejected(gr);

  	return false;
  },

  _isValidRecord: function(proposalGr) {
  	return proposalGr && proposalGr.isValid();
  },

  _createRecordProducerAndVersion: function(proposalGr, templateId) {
  	var gr = new GlideRecord(this.TABLE_NAME_PRODUCER);
  	gr.initialize();
  	gr.name = proposalGr.template_name;
  	gr.short_description.setDisplayValue(proposalGr.short_description.getDisplayValue());
  	gr.category = proposalGr.category;
  	gr.sc_catalogs = proposalGr.catalog;
  	gr.template = templateId;
  	var producerId = gr.insert();
  	if (producerId) {
  		var proposalId = proposalGr.getUniqueValue();
  		var versionId = this._createVersion(producerId, proposalId);
  		gr.script = this._createScriptContent(versionId, producerId);
  		gr.current_version = versionId;
  		gr.update();
  		this._copyAttachments(proposalGr, producerId);
  		return versionId;
  	} else
  		this.log.logErr('Record Producer and version could not be created for the proposal' + proposalGr.getUniqueValue());
  },

  _copyAttachments: function(srcGr, targetSysId) {
  	var res = [];
  	if (srcGr.hasAttachments())
  		res = j2js(GlideSysAttachment.copy(srcGr.getTableName(), srcGr.getUniqueValue(), this.TABLE_NAME_PRODUCER, targetSysId));
  	return res;
  },

  _deleteAttachments: function(attachmentIds) {
  	var sysAtt = new GlideSysAttachment();
  	for (var idx in attachmentIds)
  		if (attachmentIds.hasOwnProperty(idx))
  			sysAtt.deleteAttachment(attachmentIds[idx]);
  },

  _deleteRPAttachments: function(producerId) {
  	var targetGR = new GlideRecord('sys_attachment');
  	targetGR.addQuery('table_sys_id', producerId);
  	targetGR.query();

  	var attachmentIds = [];
  	while (targetGR.next())
  		attachmentIds.push(targetGR.getUniqueValue());
  	this._deleteAttachments(attachmentIds);
  },

  getOriginalTemplate: function(recordProducerSysId) {
  	var gr = new GlideRecord(this.TABLE_NAME_PRODUCER);
  	var templateVal = '';
  	if (!gr.get(recordProducerSysId))
  		return templateVal;

  	templateVal = gr.template.template;
  	var fieldsAndVals = this._parseEncodedQuery(templateVal);
  	this._addMandatoryFieldsToTemplateVals(fieldsAndVals);
  	return this._formQuery(fieldsAndVals);
  },

  ajaxFunction_copyAttachments: function() {
  	var srcTable = this.getParameter('sysparm_srcTable');
  	var srcSysId = this.getParameter('sysparm_srcSysId');
  	var targetTable = this.getParameter('sysparm_targetTable');
  	var targetSysId = this.getParameter('sysparm_targetSysId');
  	var removeExisting = (this.getParameter('sysparm_removeExisting') + '' === 'true');
  	var attachmentIds = [];
  	var attachmentNames = [];
  	var attachmentImgSrcs = [];
  	var attachmentActions = [];

  	//Validations - can't find or read the source record, get out
  	var srcGr = new GlideRecord(srcTable);
  	if (!srcGr.get(srcSysId))
  		return;

  	if (!srcGr.canRead()) {
  		gs.warn("ajaxFunction_copyAttachments: User is not authorized to perform this action");
  		return;
  	}

  	//Alongside testing canRead on the source record, the user must have write access to the table or existing record
  	//to add or remove attachments.
  	//In the case that we don't have a record, we reinitialise and test the table level ACL.
  	var targetGr = new GlideRecord(targetTable);
  	if (!targetGr.get(targetSysId))
  		targetGr = new GlideRecord(targetTable);

  	if (!targetGr.canWrite()){
  		gs.warn("ajaxFunction_copyAttachments: User is not authorized to perform this action");
  		return;
  	}

  	if (removeExisting) {
  		attachmentIds = [];
  		var targetGR = new GlideRecord('sys_attachment');
  		targetGR.addQuery('table_name', this.TABLE_NAME_PROPOSAL);
  		targetGR.addQuery('table_sys_id', targetSysId);
  		targetGR.query();

  		while (targetGR.next()) {
  			attachmentIds.push(j2js(targetGR.sys_id));
  			attachmentNames.push(j2js(targetGR.file_name));
  			attachmentImgSrcs.push(j2js(GlideSysAttachment.selectIcon(targetGR.sys_id)));
  			attachmentActions.push('delete');
  		}
  		this._deleteAttachments(attachmentIds);
  	}

  	// srcGr declared on line 210
  	if (srcGr.hasAttachments())
  		j2js(GlideSysAttachment.copy(srcTable, srcSysId, targetTable, targetSysId));

  	var gr = new GlideRecord('sys_attachment');
  	gr.addQuery('table_name', this.TABLE_NAME_PROPOSAL);
  	gr.addQuery('table_sys_id', targetSysId);
  	gr.query();

  	while (gr.next()) {
  		attachmentIds.push(j2js(gr.sys_id));
  		attachmentNames.push(j2js(gr.file_name));
  		attachmentImgSrcs.push(j2js(GlideSysAttachment.selectIcon(gr.sys_id)));
  		attachmentActions.push('add');
  	}
  	this._generateAttachmentsResponse({
  		ids: attachmentIds,
  		names: attachmentNames,
  		srcs: attachmentImgSrcs,
  		actions: attachmentActions
  	});
  },

  // Generate XML containing the Attachments info
  _generateAttachmentsResponse: function(attachmentsInfo) {
  	for (var i in attachmentsInfo.ids) {
  		if (attachmentsInfo.ids.hasOwnProperty(i)) {
  			var attachment = this.newItem("attachmentInfo");
  			attachment.setAttribute("id", attachmentsInfo.ids[i]);
  			attachment.setAttribute("name", attachmentsInfo.names[i]);
  			attachment.setAttribute("imgSrc", attachmentsInfo.srcs[i]);
  			attachment.setAttribute("action", attachmentsInfo.actions[i]);
  		}
  	}
  },

  _createStandardChangeTemplate: function(stdChgProposalGR) {
  	var gr = new GlideRecord(this.TABLE_NAME_TEMPLATE);
  	gr.initialize();
  	gr.name = stdChgProposalGR.template_name;
  	gr.template = stdChgProposalGR.template_value;
  	gr.short_description = stdChgProposalGR.short_description;
  	return gr.insert();
  },

  _updateStandardChangeTemplate: function(stdChgProposalGR, templateId) {
  	var templateGr = new GlideRecord(this.TABLE_NAME_TEMPLATE);
  	if (templateGr.get(templateId)) {
  		templateGr.name = stdChgProposalGR.template_name;
  		templateGr.template = stdChgProposalGR.template_value;
  		templateGr.short_description = stdChgProposalGR.short_description;
  		templateGr.update();
  	}
  },

  _updateRecordProducer: function(stdChgProposalGR, templateId, rpGr, versionId) {
  	rpGr.name = stdChgProposalGR.template_name;
  	rpGr.short_description = stdChgProposalGR.short_description;
  	rpGr.short_description.setDisplayValue(stdChgProposalGR.short_description.getDisplayValue());
  	rpGr.category = stdChgProposalGR.category;
  	rpGr.sc_catalogs = stdChgProposalGR.catalog;
  	rpGr.template = templateId;
  	rpGr.script = this._createScriptContent(versionId, rpGr.sys_id);
  	rpGr.current_version = versionId;
  	rpGr.update();

  	// Delete existing attachments if there
  	this._deleteRPAttachments(rpGr.sys_id);

  	this._copyAttachments(stdChgProposalGR, rpGr.sys_id);
  },

  _createVersion: function(producerId, proposalId) {
  	var gr = new GlideRecord(this.TABLE_NAME_VERSION);
  	gr.initialize();
  	gr.std_change_producer = producerId;
  	gr.std_change_proposal = proposalId;
  	return gr.insert();
  },

  rollBackApproval: function(proposalGr) {
  	proposalGr.approval = this.APPROVAL_STATE_NOT_REQUESTED;
  	proposalGr.state = this.STATE_NEW;
  	proposalGr.update();
  },

  _getCatalogParams: function(proposalGr, errParams) {
  	if (gs.nil(proposalGr.catalog))
  		errParams = proposalGr.catalog.getLabel();

  	if (gs.nil(proposalGr.category))
  		if (gs.nil(errParams))
  			errParams = proposalGr.category.getLabel();
  		else
  			errParams = errParams + ", " + proposalGr.category.getLabel();
  	return errParams;
  },

  validateCategorisation: function(proposalGr) {
  	if (!this.isRetireTemplate(proposalGr) && (gs.nil(proposalGr.catalog) || gs.nil(proposalGr.category) || gs.nil(proposalGr.template_name))) {
  		var errParams = '';
  		errParams = this._getCatalogParams(proposalGr, errParams);

  		if (gs.nil(proposalGr.template_name)) {
  			if (gs.nil(errParams))
  				errParams = proposalGr.template_name.getLabel();
  			else
  				errParams = errParams + ", " + proposalGr.template_name.getLabel();
  		}
  		gs.addErrorMessage(gs.getMessage('The following mandatory fields are not filled in: {0}', errParams));
  		return "failure";
  	} else
  		return "success";
  },

  rejectProposal: function(proposalSysId) {
  	var stdChgProposalGR = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (stdChgProposalGR.get(proposalSysId)) {
  		stdChgProposalGR.state = this.STATE_CANCELLED;
  		stdChgProposalGR.active = false;
  		stdChgProposalGR.update();
  	}
  },

  // This method will be executed by the workflow when a proposal is rejected
  rejectProposalWF: function(proposalGr) {
  	if (!proposalGr || !proposalGr.getUniqueValue()) {
  		this.log.logWarning("rejectProposalWF: No std_change_proposal record supplied");
  		return;
  	}

  	/* We disassociate existing approvals from the workflow so new approval records will be created when approval is requested again */
  	var existingApprovalsGr = new GlideMultipleUpdate("sysapproval_approver");
  	existingApprovalsGr.addQuery("sysapproval", proposalGr.getUniqueValue());
  	existingApprovalsGr.addQuery("state", "!=", 'cancelled');
  	existingApprovalsGr.addNotNullQuery("wf_activity");
  	existingApprovalsGr.setValue("wf_activity", "");
  	existingApprovalsGr.execute();
  	existingApprovalsGr = new GlideMultipleUpdate("sysapproval_group");
  	existingApprovalsGr.addQuery("parent", proposalGr.getUniqueValue());
  	existingApprovalsGr.addQuery("approval", "!=", 'cancelled');
  	existingApprovalsGr.addNotNullQuery("wf_activity");
  	existingApprovalsGr.setValue("wf_activity", "");
  	existingApprovalsGr.execute();
  },

  publishNewStandardChangeProposal: function(proposalSysId) {
  	var stdChgProposalGR = new GlideRecord(this.TABLE_NAME_PROPOSAL);

  	if (stdChgProposalGR.get(proposalSysId)) {
  		var templateId = this._createStandardChangeTemplate(stdChgProposalGR);
  		var versionId = this._createRecordProducerAndVersion(stdChgProposalGR, templateId);
  		stdChgProposalGR.std_change_producer_version = versionId;
  		stdChgProposalGR.active = false;
  		stdChgProposalGR.state = this.STATE_CLOSED;
  		stdChgProposalGR.update();
  		return versionId;
  	}
  },

  _deactivateTemplate: function(templateId) {
  	if (templateId) {
  		var templateGr = new GlideRecord(this.TABLE_NAME_TEMPLATE);
  		if (templateGr.get(templateId)) {
  			templateGr.active = false;
  			templateGr.update();
  		}
  	}
  },

  retireExistingStandardChangeProposal: function(proposalSysId, existingProducerSysId) {
  	var stdChgProposalGR = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (stdChgProposalGR.get(proposalSysId)) {
  		//deactivate related producer
  		var producerGr = new GlideRecord(this.TABLE_NAME_PRODUCER);
  		if (producerGr.get(existingProducerSysId)) {
  			if (j2js(producerGr.retired)) {
  				gs.addErrorMessage(gs.getMessage('Cannot retire Standard Template {0} again', producerGr.getDisplayValue()));
  				return;
  			}
  			var versionId = this._createVersion(existingProducerSysId, proposalSysId);
  			stdChgProposalGR.std_change_producer_version = versionId;
  			stdChgProposalGR.active = false;
  			stdChgProposalGR.state = this.STATE_CLOSED;
  			stdChgProposalGR.update();

  			producerGr.current_version = versionId;
  			producerGr.retired = true;
  			producerGr.update();
  			this._deactivateTemplate(producerGr.template);
  			return versionId;
  		}
  	}
  },

  modifyExistingStandardChangeProposal: function(proposalSysId, existingProducerSysId) {
  	var stdChgProposalGR = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (stdChgProposalGR.get(proposalSysId)) {
  		//fetch producer
  		var producerGr = new GlideRecord(this.TABLE_NAME_PRODUCER);
  		if (producerGr.get(existingProducerSysId)) {
  			if (j2js(producerGr.retired)) {
  				gs.addErrorMessage(gs.getMessage('Cannot modify a retired Standard Template {0}', producerGr.getDisplayValue()));
  				return;
  			}
  			var versionId = this._createVersion(existingProducerSysId, proposalSysId);
  			var templateId = producerGr.template;
  			if (templateId)
  				this._updateStandardChangeTemplate(stdChgProposalGR, templateId);
  			this._updateRecordProducer(stdChgProposalGR, templateId, producerGr, versionId);
  			stdChgProposalGR.std_change_producer_version = versionId;
  			stdChgProposalGR.active = false;
  			stdChgProposalGR.state = this.STATE_CLOSED;
  			stdChgProposalGR.update();
  			return versionId;
  		}
  	}
  },

  cancelStandardChangeProposal: function(proposalId) {
  	if (!proposalId)
  		return;

  	var stdChgProposalGR = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (!stdChgProposalGR.get(proposalId) || !stdChgProposalGR.canWrite())
  		return;

  	stdChgProposalGR.state = this.STATE_CANCELLED;
  	stdChgProposalGR.active = false;
  	stdChgProposalGR.update();
  },

  copyChangeTaskTemplates: function(proposalGr) {
  	if (!proposalGr || proposalGr.std_change_producer.nil())
  		return;

  	var versionGr = new GlideRecord(this.TABLE_NAME_VERSION);
  	versionGr.addQuery('std_change_producer', proposalGr.getValue('std_change_producer'));
  	versionGr.orderByDesc('version');
  	versionGr.query();

  	if (!versionGr.next())
  		return;

  	var currentProposalGr = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (!currentProposalGr.get(versionGr.getValue('std_change_proposal')))
  		return;

  	var relatedTaskTemplateGr = new GlideRecord(this.TABLE_NAME_RELATED_TASK);
  	relatedTaskTemplateGr.addQuery('std_change_proposal', currentProposalGr.getUniqueValue());
  	relatedTaskTemplateGr.query();

  	var newRelatedTaskTemplateGr = new GlideRecord(this.TABLE_NAME_RELATED_TASK);
  	while (relatedTaskTemplateGr.next()) {
  		newRelatedTaskTemplateGr.initialize();
  		newRelatedTaskTemplateGr.setValue('std_change_proposal', proposalGr.getUniqueValue());
  		newRelatedTaskTemplateGr.setValue('name', relatedTaskTemplateGr.name);
  		newRelatedTaskTemplateGr.setValue('short_description', relatedTaskTemplateGr.short_description);
  		newRelatedTaskTemplateGr.setValue('template', relatedTaskTemplateGr.template);
  		newRelatedTaskTemplateGr.setValue('order', relatedTaskTemplateGr.order);
  		newRelatedTaskTemplateGr.insert();
  	}
  },

  createChangeTasks: function(changeGr) {
  	if (!changeGr || changeGr.std_change_producer_version.nil())
  		return;

  	var versionGr = new GlideRecord(this.TABLE_NAME_VERSION);
  	if (!versionGr.get(changeGr.getValue('std_change_producer_version')))
  		return;

  	var proposalGr = new GlideRecord(this.TABLE_NAME_PROPOSAL);
  	if (!proposalGr.get(versionGr.getValue('std_change_proposal')))
  		return;

  	var relatedTaskTemplateGr = new GlideRecord(this.TABLE_NAME_RELATED_TASK);
  	relatedTaskTemplateGr.addQuery('std_change_proposal', proposalGr.getUniqueValue());
  	relatedTaskTemplateGr.orderBy('order');
  	relatedTaskTemplateGr.query();

  	var changeTaskGr = new GlideRecord(this.TABLE_NAME_CHANGE_TASK);
  	while (relatedTaskTemplateGr.next()) {
  		changeTaskGr.initialize();
  		changeTaskGr.setValue(this.TABLE_NAME_CHANGE, changeGr.getUniqueValue());
  		changeTaskGr.applyEncodedQuery(relatedTaskTemplateGr.getValue('template'));
  		changeTaskGr.insert();
  	}

  },

  _getPropertyValue: function(propertyName) {
  	if (!this._propGr) {
  		var gr = new GlideRecord(this.TABLE_NAME_PROPERTIES);
  		// Using number instead of sysid since if ever this row
  		// gets deleted then a row with same number can be
  		// recreated. But row with same sysid is impossible to create.
  		gr.addQuery(this.ATTR_INTERNAL_NAME, this.PROP_ROW_KEY);
  		gr.query();
  		if (!gr.next()) {
  			this.log.logErr('Property row not found!');
  			return;
  		}
  		this._propGr = gr;
  	}
  	var returnVal = this._propGr.getValue(propertyName);
  	if (returnVal === null)
  		returnVal = '';
  	return j2js(returnVal);
  },

  _getBooleanPropertyValue: function(propertyName, defaultValue) {
  	var val = this._getPropertyValue(propertyName) + '';
  	if (val)
  		return val === '1';
  	return defaultValue;
  },

  _getCsvPropertyValue: function(propertyName) {
  	var val = this._getPropertyValue(propertyName);
  	if (val) // No need for elaborate parsing as in ChangeUtils since this is system saved.
  		return val.split(',');
  	else
  		return [];
  },

  getValue: function(property) {
  	return this._getPropertyValue(property);
  },

  // prepare a name , value array from encodedQuery.
  // It creates two arrays of name and value each.
  _parseEncodedQuery: function(query, noEscape) {
  	return this._parseEncodedQueryByTable(this.TABLE_NAME_CHANGE, query, noEscape);
  },

  _parseEncodedQueryByTable: function(tableName, query, noEscape) {
  	var fieldsAndVals = {
  		names: [],
  		vals: []
  	};
  	if (!query)
  		return fieldsAndVals;

  	if (!GlideTableDescriptor.isValid(tableName))
  		return fieldsAndVals;

  	var qs = new GlideQueryString(tableName, query);
  	qs.deserialize();

  	var terms = qs.getTerms();

  	for (var i = 0; i < terms.size(); i++) {
  		var term = terms.get(i);
  		var fieldName = term.getField();
  		if (!fieldName)
  			continue;

  		var fieldValue = "" + term.getValue();
  		if (!noEscape)
  			fieldValue = fieldValue.replaceAll("^", "^^");

  		fieldsAndVals.names.push("" + fieldName);
  		fieldsAndVals.vals.push(fieldValue);
  	}

  	return fieldsAndVals;
  },

  _formQuery: function(q) {
  	// We are not encoding q.vals[i], so if the value
  	// has ^ then the formed query will not be properly
  	// parsable. However, in our system too I see no
  	// code which parses the ^. So, I don't know how to
  	// escape that.
  	var val = '';
  	for (var i = 0; i < q.names.length; i++) {
  		if (i > 0)
  			val = val + '^';
  		val = val + q.names[i] + '=' + q.vals[i];
  	}
  	if (val)
  		val = val + '^EQ';
  	return val;
  },

  // This is a simple converter.
  // resp should be single level deep object.
  // Each of its attributes are converted into tags under root element.
  // Data in the object referred by that key is set as attributes in
  // in the generated tag. If the key refers to an array instead, then
  // the key is repeated for each item in the array and the above logic
  // applies. If the key refered to a string instead then that value is
  // set into the tag with value as attribute name.
  _toXmlAndSet: function(resp) {
  	for (var k in resp)
  		if (resp.hasOwnProperty(k))
  			this._addToAttr(resp[k], k);
  },

  _addToAttr: function(o, k) {
  	if (o instanceof Array)
  		for (var i = 0; i < o.length; i++) {
  			o[i].idx = i;
  			this._addAttr(k, o[i]);
  		}
  	else {
  		if (typeof o === 'string')
  			o = { value: o };
  		this._addAttr(k, o);
  	}
  },

  _addAttr: function(tagName, o) {
  	var item = this.newItem(tagName);
  	for (var attr in o)
  		if (o.hasOwnProperty(attr))
  			item.setAttribute(attr, o[attr] + '');
  },

  _attrToLabel: function(attr) {
  	var chgAttrMeta = sys_meta[this.TABLE_NAME_CHANGE][attr];
  	var valid = chgAttrMeta ? true : false;
  	var active = valid ? (chgAttrMeta.active === "true") : false;
  	var label = chgAttrMeta.label;
  	return {
  		name: attr,
  		label: label,
  		valid: valid,
  		active: active
  	};
  },

  _attrsToLabel: function(attrs) {
  	var ans = [];
  	for (var i = 0; i < attrs.length; i++) {
  		var attr = attrs[i];
  		ans.push(this._attrToLabel(attr));
  	}
  	return ans;
  },

  ajaxFunction_getFieldConfigs: function() {
  	this._toXmlAndSet(this._getFieldConfigs());
  	return 'ok';
  },

  getFieldConfigsJson: function(chgReqId) {
  	return new JSON().encode(this._getFieldConfigs(chgReqId));
  },

  _getFieldConfigs: function(chgReqId) {
  	var resp = {};
  	this._addDefaultValuesConfig(resp, true, chgReqId);
  	this._addMandatoryFieldsConfig(resp);
  	this._addUnmodifiableFieldsConfig(resp);
  	return resp;
  },

  _isRecordChanged: function(unionSampleChgId, q, isChanged) {
  	var copyAttrs = this._getCsvPropertyValue(this.PROP_FIELDS_TO_COPY);
  	var chgReqGR = new GlideRecord(this.TABLE_NAME_CHANGE);

  	if (!chgReqGR.get(unionSampleChgId))
  		return isChanged;
  	for (var i = 0; i < copyAttrs.length; i++) {
  		var field = copyAttrs[i];
  		if (chgReqGR.isValidField(field)) {
  			var value = chgReqGR.getValue(field);
  			if (value === null)
  				value = '';
  			value = value + ''; // Making it string.
  			var idx = this.arrayUtil.indexOf(q.names, field);
  			if (idx === -1) {
  				q.names.push(field);
  				q.vals.push(value);
  			} else
  				q.vals[idx] = value;
  			isChanged = true;
  		} else
  			this.log.logWarning("_addDefaultValuesConfig: Invalid field '" + field + "' provided for table 'change_request'.");
  	}
  	return isChanged;
  },

  _addDefaultValuesConfig: function(resp, unionMandatoryFields, unionSampleChgId) {
  	var val = this._getPropertyValue(this.PROP_DEFAULT_VALUES);
  	if (unionMandatoryFields || unionSampleChgId) {
  		var q = this._parseEncodedQuery(val);
  		var isChanged = false;

  		if (unionMandatoryFields)
  			isChanged = this._addMandatoryFieldsToTemplateVals(q);

  		if (unionSampleChgId)
  			isChanged = this._isRecordChanged(unionSampleChgId, q, isChanged);

  		if (isChanged)
  			val = this._formQuery(q);
  	}
  	resp.default_values = val;
  },

  addMandatoryFieldsToTemplate: function(template) {
  	var fieldsAndVals = this._parseEncodedQuery(template);

  	this._addMandatoryFieldsToTemplateVals(fieldsAndVals);

  	return this._formQuery(fieldsAndVals);
  },

  _addMandatoryFieldsToTemplateVals: function(fieldsAndVals) {
  	var isChanged = false;

  	if (!fieldsAndVals)
  		fieldsAndVals = {
  			names: [],
  			vals: []
  		};

  	var mandatory = this._getCsvPropertyValue(this.PROP_MANDATORY_FIELDS);
  	for (var i = 0; i < mandatory.length; i++)
  		if (!this.arrayUtil.contains(fieldsAndVals.names, mandatory[i])) {
  			fieldsAndVals.names.push(mandatory[i]);
  			fieldsAndVals.vals.push('');
  			isChanged = true;
  		}
  	return isChanged;
  },

  _addMandatoryFieldsConfig: function(resp) {
  	var val = this._attrsToLabel(this._getCsvPropertyValue(this.PROP_MANDATORY_FIELDS));
  	resp.mandatory = val;
  },

  _addUnmodifiableFieldsConfig: function(resp) {
  	var val = this._attrsToLabel(this._getCsvPropertyValue(this.PROP_UNMODIFIABLE_FIELDS));
  	resp.unmodifiable = val;
  },

  _checkRestrictedFields: function(q, recordLabel, displayErrors) {
  	var result = {
  		validationPassed: "true",
  		errorMsgs: []
  	};
  	var attemptedSet = '';
  	var unmodifiable = this._getCsvPropertyValue(this.PROP_UNMODIFIABLE_FIELDS);
  	if (q && unmodifiable) {
  		for (var i = 0; i < q.names.length; i++) {
  			var f = q.names[i];
  			var idx = this.arrayUtil.indexOf(unmodifiable, f);
  			if (idx !== -1)
  				attemptedSet = attemptedSet + this._attrToLabel(unmodifiable[idx]).label + ', ';
  		}
  	}
  	if (attemptedSet) {
  		result.validationPassed = false;
  		var errorMsg = gs.getMessage('The following "{0} values" are not allowed to be set in a template: {1}',
  			[recordLabel, attemptedSet.substring(0, attemptedSet.length - 2)]);
  		result.errorMsgs.push(errorMsg);
  		if (displayErrors)
  			gs.addErrorMessage(errorMsg);
  		return result;
  	} else {
  		return result;
  	}
  },

  _getUnfilledValues: function(q, m, unfilledValues) {
  	var label = this._attrToLabel(m).label;
  	if (q) {
  		var idx = this.arrayUtil.indexOf(q.names, m);
  		if (idx === -1 || q.vals[idx] === '')
  			unfilledValues = unfilledValues + label + ', ';
  	} else
  		unfilledValues = unfilledValues + label + ', ';
  	return unfilledValues;
  },

  _checkMandatoryFields: function(q, recordLabel, displayErrors) {
  	var result = {
  		validationPassed: "true",
  		errorMsgs: []
  	};
  	var unfilledValues = '';
  	var mandatory = this._getCsvPropertyValue(this.PROP_MANDATORY_FIELDS);
  	if (mandatory)
  		for (var i = 0; i < mandatory.length; i++)
  			unfilledValues = this._getUnfilledValues(q, mandatory[i], unfilledValues);
  	if (unfilledValues) {
  		result.validationPassed = false;
  		var errorMsg = gs.getMessage('"{0} values" have not been provided: {1}', [recordLabel, unfilledValues.substring(0, unfilledValues.length - 2)]);
  		result.errorMsgs.push(errorMsg);
  		if (displayErrors)
  			gs.addErrorMessage(errorMsg);
  		return result;
  	} else
  		return result;
  },

  getTemplateValidationResult: function(proposalGr, displayErrors) {
  	var result = {
  		validationPassed: false,
  		errorMsgs: []
  	};

  	if (!proposalGr || !proposalGr.getUniqueValue()) {
  		this.log.logError("validateTemplateValueCompliance: no Standard Change Proposal record supplied");
  		return result;
  	}

  	return this.getTemplateValueValidationResult(proposalGr.getValue('template_value'));
  },

  getTemplateValueValidationResult: function(templateValue, displayErrors) {
  	var result = {
  		validationPassed: false,
  		errorMsgs: []
  	};

  	if (!templateValue)
  		templateValue = "";

  	var q = this._parseEncodedQuery(templateValue);
  	var mandatoryResult = this._checkMandatoryFields(q, this.TABLE_LABEL_CHANGE, displayErrors);
  	var restrictedResult = this._checkRestrictedFields(q, this.TABLE_LABEL_CHANGE, displayErrors);

  	result.validationPassed = mandatoryResult.validationPassed && restrictedResult.validationPassed;
  	result.errorMsgs = mandatoryResult.errorMsgs.concat(restrictedResult.errorMsgs);
  	return result;
  },

  ajaxFunction_validateTemplateValue: function() {
  	var templateValue = this.getParameter("sysparm_template_value");

  	return JSON.stringify(this.getTemplateValueValidationResult(templateValue));
  },

  validateProposal: function(proposalGr, displayErrors, checkTasks) {
  	var result = false;
  	if (!proposalGr || !proposalGr.getUniqueValue()) {
  		this.log.logError("validateProposal: no Standard Change Proposal record supplied");
  		return result;
  	}

  	var categoryResult = this.validateCategorisation(proposalGr);
  	result = categoryResult === 'success';

  	// If this is a retire type proposal there is no other validation to do
  	if (this.isRetireTemplate(proposalGr))
  		return result;

  	var templateResult = this.validateTemplateValueCompliance(proposalGr, displayErrors);
  	result = result && templateResult;
  	if (!checkTasks)
  		return result;

  	var taskUtils = new StdChangeTaskUtils();
  	var chgTaskTemplateGr = new GlideRecord(this.TABLE_NAME_RELATED_TASK);
  	chgTaskTemplateGr.addQuery("std_change_proposal", proposalGr.getUniqueValue());
  	chgTaskTemplateGr.query();
  	while (chgTaskTemplateGr.next()) {
  		var taskResult = taskUtils.getTemplateValidationResult(chgTaskTemplateGr, false);
  		if (!taskResult.validationPassed) {
  			result = false;
  			this._handleErrorMsgs(chgTaskTemplateGr, taskResult, displayErrors);
  		}
  	}
  	return result;
  },

  _handleErrorMsgs: function(chgTaskTemplateGr, taskResult, displayErrors) {
  	var link = '<a target="_blank" href="' + chgTaskTemplateGr.getLink(true) + '"><b>' + chgTaskTemplateGr.getDisplayValue() + '</b></a>';
  	var errorMsg = gs.getMessage("Validation failed for Change Task template {0}:<br/>", link);
  	for (var i = 0; i < taskResult.errorMsgs.length; i++)
  		errorMsg += '<span style="margin-left: 10px;">' + taskResult.errorMsgs[i] + '</span><br/>';

  	if (displayErrors)
  		gs.addErrorMessage(errorMsg);
  },

  validateTemplateValueCompliance: function(proposalGr, displayErrors) {
  	if (!proposalGr || !proposalGr.getUniqueValue()) {
  		this.log.logError("validateTemplateValueCompliance: no Standard Change Proposal record supplied");
  		return false;
  	}

  	// if no "displayErrors" argument is passed assume true
  	if (typeof displayErrors === "undefined")
  		displayErrors = true;

  	var q = this._parseEncodedQuery(proposalGr.getValue('template_value'));
  	var mandatoryResult = this._checkMandatoryFields(q, this.TABLE_LABEL_CHANGE, displayErrors);
  	var restrictedResult = this._checkRestrictedFields(q, this.TABLE_LABEL_CHANGE, displayErrors);

  	return mandatoryResult.validationPassed && restrictedResult.validationPassed;
  },

  _isInvalidState: function(state) {
  	if (state === this.STATE_CLOSED || state === this.STATE_CANCELLED || (state === this.STATE_WIP && !gs.hasRole(this.ROLE_CHG_MANAGER)))
  		return true;
  	return false;
  },

  reviewedFieldsAcl: function(proposalGr) {
  	if (!proposalGr || !proposalGr.getUniqueValue())
  		return false;

  	// If this is a retire type proposal there is no other validation to do
  	if (this.isRetireTemplate(proposalGr))
  		return false;

  	var state = proposalGr.getValue(this.ATTR_STATE) - 0;
  	if (this._isInvalidState(state))
  		return false;
  	return true;
  },

  ajaxFunction_getFieldNamesFromTemplateValue: function() {
  	var sydId = this.getParameter('sysparm_sysId');
  	var tableName = this.getParameter('sysparm_tableName');
  	var gr = new GlideRecordSecure(tableName);
  	gr.addQuery('sys_id', sydId);
  	gr.query();

  	var q = {};
  	if (gr.next())
  		q = this._parseEncodedQuery(gr.std_change_producer_version.std_change_proposal.template_value);
  	return q.names.toString();
  },

  getFieldNamesFromTemplate: function(encodedQuery) {
  	var q = this._parseEncodedQuery(encodedQuery);
  	return q.names;
  },

  getVersionNumber: function(stdVersionGr) {
  	var countGa = new GlideAggregate(this.TABLE_NAME_VERSION);
  	countGa.addAggregate('COUNT');
  	countGa.addQuery(this.ATTR_STD_CHG_PRODUCER,
  		stdVersionGr.getValue(this.ATTR_STD_CHG_PRODUCER));
  	countGa.query();
  	var count = 0;
  	if (countGa.next())
  		count = countGa.getAggregate('COUNT');
  	return (count * 1) + 1;
  },

  getVersionName: function(stdVersionGr) {
  	return stdVersionGr.std_change_producer.getDisplayValue() + ' - ' + stdVersionGr.version;
  },

  _getTemplateValue: function(tableName, gr /*Pass gr after querying*/) {
  	var value = '';
  	if (gr) {
  		if (tableName.equals(this.TABLE_NAME_PRODUCER))
  			value = gr.template.template;
  		else if (tableName.equals(this.TABLE_NAME_TEMPLATE))
  			value = gr.template;
  		else if (tableName.equals(this.TABLE_NAME_VERSION))
  			value = gr.std_change_proposal.template_value;

  		if (value !== '') {
  			if (this.log.debugOn())
  				this.log.logDebug('template' + value);
  			return j2js(value);
  		}
  	}
  	return;
  },

  _isValidCatalog: function(catalogSysId) {
  	var catalogGr = new GlideRecord(this.TABLE_NAME_CATALOG);
  	if (!catalogGr.get(catalogSysId)) {
  		gs.addErrorMessage(gs.getMessage('Parent "Catalog" specified in "Standard Change Properties" does not exist. Please ask admin to correct the setup.'));
  		return false;
  	}
  	return true;
  },

  _isValidCategory: function(catalogSysId, categorySysId) {
  	var categoryGr = new GlideRecord(this.TABLE_NAME_CATEGORY);
  	if (categoryGr.get(categorySysId)) {
  		if (!categoryGr.sc_catalog) {
  			gs.addErrorMessage(gs.getMessage('Parent "Category" specified in "Standard Change Properties" does not belong to any Catalog. Please ask admin to correct the setup.'));
  			return false;
  		} else if (catalogSysId !== j2js(categoryGr.sc_catalog)) {
  			gs.addErrorMessage(gs.getMessage('Parent "Category" specified is no longer attached to the specified "Catalog" in "Standard Change Properties". Please ask admin to correct the setup.'));
  			return false;
  		}
  	} else {
  		gs.addErrorMessage(gs.getMessage('Parent "Category" specified in "Standard Change Properties" does not exist. Please ask admin to correct the setup.'));
  		return false;
  	}
  	return true;
  },

  isStandardChangeSetupValid: function() {
  	var catalogSysId = this._getPropertyValue(this.ATTR_CATALOG);
  	var categorySysId = this._getPropertyValue(this.ATTR_CATEGORY);

  	if (JSUtil.notNil(catalogSysId) && JSUtil.notNil(categorySysId)) {
  		if (!this._isValidCatalog(catalogSysId))
  			return false;

  		if (!this._isValidCategory(catalogSysId, categorySysId))
  			return false;
  	} else {
  		gs.addErrorMessage(gs.getMessage('Parent "Catalog" or "Category" is not specified in "Standard Change Properties". Please ask admin to correct the setup.'));
  		return false;
  	}
  	return true;
  },

  isChangeStandardAndTwoStepEnabled: function(changeRequestGr) {
  	if (changeRequestGr && changeRequestGr.type + '' === 'standard' && changeRequestGr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION) && this._getBooleanPropertyValue(this.PROP_TWO_STEP, false))
  		return true;
  	return false;
  },

  ajaxFunction_getTemplNCategoryForModify: function() {
  	var tableNameProducer = this.getParameter('sysparm_tableName');
  	var producerGr = new GlideRecord(tableNameProducer);
  	var producerSysId = this.getParameter('sysparm_sysid');
  	var addMandatoryFields = this.getParameter('add_mandatory_fields') + '';
  	var ans = {};
  	if (producerGr.get(producerSysId)) {
  		ans.template = this._getTemplateValue(tableNameProducer, producerGr);
  		if (addMandatoryFields === "true") {
  			var fieldsAndVals = this._parseEncodedQuery(ans.template);
  			this._addMandatoryFieldsToTemplateVals(fieldsAndVals);
  			ans.template = this._formQuery(fieldsAndVals);
  		}
  		ans.catalog = j2js(producerGr.sc_catalogs);
  		ans.category = j2js(producerGr.category);
  		ans.templateName = j2js(producerGr.name);
  		this._toXmlAndSet(ans);
  	}
  },

  ajaxFunction_getTemplateValue: function() {
  	var tableName = this.getParameter('sysparm_tableName');
  	var gr = new GlideRecordSecure(tableName);
  	var sysId = this.getParameter('sysparm_sysid');
  	if (gr.get(sysId))
  		return this._getTemplateValue(tableName, gr);
  	return;
  },

  _addQueryAllClosedChanges: function(changeGa /*GlideAggregate*/) {
  	changeGa.addNotNullQuery(this.ATTR_CLOSE_CODE);
  	changeGa.addQuery('active', false);
  },

  _addQueryAllUnsuccessfulChanges: function(changeGa /*GlideAggregate*/) {
  	changeGa.addQuery(this.ATTR_CLOSE_CODE, this.CLOSE_CODES_UNSUCCESSFUL);
  	changeGa.addQuery('active', false);
  },

  calculateVersionAndTemplateStats: function(versionId) {
  	var startTime = new GlideDateTime();

  	// if we've been passed a Version Id check it's valid and get associated producer Id
  	var producerId;
  	if (versionId) {
  		var stdChgProducerVersionGr = new GlideRecord(this.TABLE_NAME_VERSION);
  		if (stdChgProducerVersionGr.get(versionId))
  			producerId = stdChgProducerVersionGr.getValue(this.ATTR_STD_CHG_PRODUCER);
  		else {
  			this.log.logWarning('calculateVersionAndTemplateStats: no matching ' + stdChgProducerVersionGr.sys_meta.label + ' record found for sys_id "' + versionId + '"');
  			return;
  		}
  	}

  	var chgGr = new GlideAggregate(this.TABLE_NAME_CHANGE);
  	chgGr.addInactiveQuery();
  	chgGr.addNotNullQuery(this.ATTR_CLOSE_CODE);
  	chgGr.addNotNullQuery(this.ATTR_STD_CHG_PRODUCER_VERSION);
  	if (producerId)
  		chgGr.addQuery(this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER, "=", producerId);
  	chgGr.groupBy(this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER);
  	chgGr.groupBy(this.ATTR_STD_CHG_PRODUCER_VERSION);
  	chgGr.groupBy(this.ATTR_CLOSE_CODE);
  	chgGr.addAggregate("COUNT");
  	chgGr.orderBy(this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER);
  	chgGr.orderBy(this.ATTR_STD_CHG_PRODUCER_VERSION);
  	chgGr.query();

  	var queryEndTime = new GlideDateTime();

  	var closeCode;
  	var currentProducerVersionId;
  	var currentProducerId;
  	var versionUnsuccessfulCount = 0;
  	var versionTotalCount = 0;
  	var producerUnsuccessfulCount = 0;
  	var producerTotalCount = 0;
  	var usedProducerVersionIds = [];
  	var usedProducerIds = [];
  	var processedRecords = 0;
  	var allRecordsProcessed = !chgGr.hasNext();
  	var lastRecord = false;

  	while (chgGr.next() || allRecordsProcessed === false) {
  		var count = parseInt(chgGr.getAggregate('COUNT'), 10);

  		if (isNaN(count)) {
  			lastRecord = !chgGr.hasNext();
  			continue;
  		}

  		if (lastRecord)
  			allRecordsProcessed = true;

  		// We've got Change Request data for a new template so save the current collected values into the previous template
  		if (currentProducerId && (currentProducerId !== chgGr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER) || lastRecord)) {
  			this._updateTemplateStats(currentProducerVersionId, currentProducerId, { total: producerTotalCount, totalUnsuccessful: producerUnsuccessfulCount });
  			usedProducerIds.push(currentProducerId);
  			producerUnsuccessfulCount = 0;
  			producerTotalCount = 0;
  		}

  		// We've got Change Request data for a new template version so save the current collected values into the previous template version
  		if (currentProducerVersionId && (currentProducerVersionId !== chgGr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION) || lastRecord)) {
  			this._updateVersionStats(currentProducerVersionId, { total: versionTotalCount, totalUnsuccessful: versionUnsuccessfulCount });
  			usedProducerVersionIds.push(currentProducerVersionId);
  			versionUnsuccessfulCount = 0;
  			versionTotalCount = 0;
  		}

  		currentProducerId = chgGr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER);
  		currentProducerVersionId = chgGr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION);
  		closeCode = chgGr.getValue(this.ATTR_CLOSE_CODE);

  		versionTotalCount += count;
  		producerTotalCount += count;
  		if (this._isCloseCodeUnsuccessful(closeCode)) {
  			versionUnsuccessfulCount += count;
  			producerUnsuccessfulCount += count;
  		}

  		processedRecords++;
  		lastRecord = !chgGr.hasNext();
  	}

  	/* Any template versions that we didn't update above means those versions haven't been used on any 
  	   Change Request so update with 0 values */
  	stdChgProducerVersionGr = new GlideRecord(this.TABLE_NAME_VERSION);
  	if (producerId)
  		stdChgProducerVersionGr.addQuery(this.ATTR_STD_CHG_PRODUCER, "=", producerId);
  	stdChgProducerVersionGr.addQuery('sys_id', 'NOT IN', usedProducerVersionIds);
  	stdChgProducerVersionGr.query();
  	while (stdChgProducerVersionGr.next())
  		this._updateVersionStats(stdChgProducerVersionGr.getUniqueValue(), { total: 0, totalUnsuccessful: 0 });

  	/* And finally, update any Templates with 0 values where the sum of closed Change Requests across all
  	   template versions is 0 */
  	stdChgProducerVersionGr = new GlideAggregate(this.TABLE_NAME_VERSION);
  	if (producerId)
  		stdChgProducerVersionGr.addQuery(this.ATTR_STD_CHG_PRODUCER, producerId);
  	stdChgProducerVersionGr.addAggregate('SUM', 'closed_change_count');
  	stdChgProducerVersionGr.addHaving('SUM', 'closed_change_count', '=', 0);
  	stdChgProducerVersionGr.groupBy(this.ATTR_STD_CHG_PRODUCER);
  	stdChgProducerVersionGr.query();

  	while (stdChgProducerVersionGr.next())
  		this._updateTemplateStats(stdChgProducerVersionGr.getUniqueValue(), stdChgProducerVersionGr.getValue(this.ATTR_STD_CHG_PRODUCER), { total: 0, totalUnsuccessful: 0 });

  	this.log.logInfo("calculateVersionAndTemplateStats: completed\n" +
  		(producerId ? 'Standard Change Template id: ' + producerId : '') +
  		'Records processed: ' + processedRecords +
  		'\nQuery time: ' + new GlideDuration(queryEndTime.getNumericValue() - startTime.getNumericValue()).getDurationValue() +
  		'\nTotal time: ' + new GlideDuration(new GlideDateTime().getNumericValue() - startTime.getNumericValue()).getDurationValue());
  },

  updateVersionStats: function(versionId /*sys_id string*/) {
  	this._updateVersionStats(versionId);
  	this._updateTemplateStats(versionId);
  },

  _getStatsResult: function(queryColumn, sysId) {
  	var total = 0;
  	var totalUnsuccessful = 0;

  	var ga = new GlideAggregate(this.TABLE_NAME_CHANGE);
  	ga.addAggregate('COUNT');
  	ga.addQuery(queryColumn, sysId);
  	this._addQueryAllClosedChanges(ga);
  	ga.query();
  	if (ga.next())
  		total = ga.getAggregate('COUNT') * 1;

  	ga = new GlideAggregate(this.TABLE_NAME_CHANGE);
  	ga.addAggregate('COUNT');
  	ga.addQuery(queryColumn, sysId);
  	this._addQueryAllUnsuccessfulChanges(ga);
  	ga.query();
  	if (ga.next())
  		totalUnsuccessful = ga.getAggregate('COUNT') * 1;

  	var percentSuccess = (total === 0) ? 0 : ((total - totalUnsuccessful) * 100.0) / total;

  	var statsResult = {};
  	statsResult.total = total;
  	statsResult.totalUnsuccessful = totalUnsuccessful;
  	statsResult.percentSuccess = percentSuccess;
  	return statsResult;
  },

  _updateVersionStats: function(versionId /*sys_id string*/, stats) {
  	var gr = new GlideRecord(this.TABLE_NAME_VERSION);
  	if (!gr.get(versionId))
  		return;

  	if (!stats || isNaN(stats.total) || isNaN(stats.totalUnsuccessful))
  		stats = this._getStatsResult(this.ATTR_STD_CHG_PRODUCER_VERSION, versionId);

  	gr.closed_change_count = stats.total;
  	gr.unsuccessful_change_count = stats.totalUnsuccessful;

  	if (!stats.hasOwnProperty('percentSuccess'))
  		gr.percent_successful = stats.total === 0 ? 0 : (((stats.total - stats.totalUnsuccessful) / stats.total) * 100).toFixed(2);
  	else
  		gr.percent_successful = parseInt(stats.percentSuccess);

  	return gr.update();
  },

  _updateTemplateStats: function(versionId /*sys_id string*/, templateId, stats) {
  	if (!templateId) {
  		var gr = new GlideRecord(this.TABLE_NAME_VERSION);
  		if (!gr.get(versionId))
  			return;
  		else
  			templateId = gr.std_change_producer;
  	}

  	var templateGr = new GlideRecord(this.TABLE_NAME_PRODUCER);
  	if (!templateGr.get(templateId))
  		return;

  	if (!stats || isNaN(stats.total) || isNaN(stats.totalUnsuccessful))
  		stats = this._getStatsResult(this.ATTR_STD_CHG_PRODUCER_VERSION + "." + this.ATTR_STD_CHG_PRODUCER, templateId);

  	templateGr.closed_change_count = stats.total;
  	templateGr.unsuccessful_change_count = stats.totalUnsuccessful;

  	if (!stats.hasOwnProperty('percent_success'))
  		templateGr.percent_successful = stats.total === 0 ? 0 : (((stats.total - stats.totalUnsuccessful) / stats.total) * 100).toFixed(2);
  	else
  		templateGr.percent_successful = parseInt(stats.percentSuccess);

  	return templateGr.update();
  },

  getReadOnlyFields: function() {
  	return this._getCsvPropertyValue(this.PROP_READONLY_FIELDS);
  },

  ajaxFunction_getReadOnlyFields: function() {
  	var tableName = this.getParameter('sysparm_tableName') + '';
  	var gr = new GlideRecord(tableName);
  	var sysId = this.getParameter('sysparm_sysId');

  	if (tableName === this.TABLE_NAME_CHANGE && gr.get(sysId) && gr.canRead() && gr.getValue(this.ATTR_STD_CHG_PRODUCER_VERSION))
  		return this.getReadOnlyFields().toString();

  	return '';
  },

  ajaxFunction_getReadOnlyFieldsOnInsert: function() {
  	return this.getReadOnlyFields().toString();
  },

  ajaxFunction_getCategory: function() {
  	return this._getPropertyValue(this.ATTR_CATEGORY);
  },

  /*****************************************************************************************************************
   * Utility function to get list of categories under a particular category.
   * Usage for a reference qualifier below :
   * javascript:'active=true^sc_catalog=e0d08b13c3330100c8b837659bba8fb4^'+
   * +new StdChangeUtils().getCategoriesInHierarchy('b0fdfb01932002009ca87a75e57ffbe9',3)
   *******************************************************************************************************************/

  getCategoriesInHierarchy: function(startWithCategoryId, depth) {
  	return GlideappCategory.get(startWithCategoryId).getHierarchicalCategoryQueryString(depth);
  },

  /*************************************************************************************************************
   * Utility function to merge two encoded queries, and return back the sorted unique labels and display values.
   *************************************************************************************************************/
  getMergedLabelValueFromEncodedQueries: function(query1, query2) {

  	// Parse the encoded query, get the name and value pair
  	var query1NameValues = this._parseEncodedQuery(query1);
  	var query2NameValues = this._parseEncodedQuery(query2);

  	// Convert the names to labels, this will return us the name and label pair
  	var query1NameLabels = this._attrsToLabel(query1NameValues.names);
  	var query2NameLabels = this._attrsToLabel(query2NameValues.names);

  	// Merge and sort the two name label arrays of objects into a single array.
  	// The comparison for merging (in order to get unique values) and sorting is based on the field name
  	var mergedNameLabels = this._mergeQueryNameLabels(query1NameLabels, query2NameLabels);

  	// Below dummy glide records are needed to get display values, else we will not be able to get display values for reference fields.
  	var mergedLabelDisplayValues = [];
  	var sampleChangeGr = new GlideRecord(this.TABLE_NAME_CHANGE);
  	sampleChangeGr.initialize();

  	// Loop through the merged name labels, and assign display values and valid/active flags.
  	var ml;
  	for (var k = 0; k < mergedNameLabels.length; k++) {
  		ml = mergedNameLabels[k];
  		if (!ml.valid || ml.valid && sampleChangeGr[ml.name].canRead()) {
  			var displayValue1 = this._getDisplayValueFromGlideRecord(ml.name, query1NameLabels, query1NameValues, sampleChangeGr);
  			var displayValue2 = this._getDisplayValueFromGlideRecord(ml.name, query2NameLabels, query2NameValues, sampleChangeGr);
  			mergedLabelDisplayValues.push({
  				label: ml.label,
  				name: ml.name,
  				displayValue1: displayValue1,
  				displayValue2: displayValue2,
  				valid: ml.valid,
  				active: ml.active
  			});
  		}
  	}

  	// The returned value is an array of objects which structure is as in the below example:
  	// {label : "Description", name : "description", displayValue1 : "desc 1", displayValue1 : "", active : true, valid : true};
  	return mergedLabelDisplayValues;
  },

  _mergeQueryNameLabels: function(query1NameLabels, query2NameLabels) {
  	var tempMergedQueryNameLabels = query1NameLabels.concat(query2NameLabels);
  	var l = tempMergedQueryNameLabels.length;
  	var mergedQueryNameLabels = [];
  	for (var i = 0; i < l; i++) {
  		for (var j = i + 1; j < l; j++)
  			if (tempMergedQueryNameLabels[i].name === tempMergedQueryNameLabels[j].name)
  				j = ++i;
  		mergedQueryNameLabels.push(tempMergedQueryNameLabels[i]);
  	}
  	return mergedQueryNameLabels.sort(this._queryNameLabelsComparator);
  },

  _queryNameLabelsComparator: function(a, b) {
  	return (a.label.toLowerCase() > b.label.toLowerCase()) ? 1 : ((b.label.toLowerCase() > a.label.toLowerCase()) ? -1 : 0);
  },

  _getDisplayValueFromGlideRecord: function(name, queryNameLabels, queryNameValues, sampleChangeGr) {
  	var displayValue = "";
  	var fieldName;
  	var fieldValue;
  	var queryCtr = -1;
  	for (var i = 0; i < queryNameLabels.length; i++)
  		if (queryNameLabels[i].name === name)
  			queryCtr = i;
  	if (queryCtr >= 0) {
  		fieldName = queryNameValues.names[queryCtr];
  		fieldValue = queryNameValues.vals[queryCtr];

  		if (fieldValue.startsWith('javascript:')) {
  			var scriptEvaluator = new GlideScriptEvaluator();
  			scriptEvaluator.setEnforceSecurity(true);
  			fieldValue = scriptEvaluator.evaluateString(fieldValue, false) + "";
  		}

  		if (queryNameLabels[queryCtr].valid) {
  			sampleChangeGr.setValue(fieldName, fieldValue);
  			displayValue = sampleChangeGr.getDisplayValue(fieldName);
  		} else
  			displayValue = fieldValue;
  	}

  	// The Differ that ends up using these values dies spectacularly when given a null value to diff
  	if (typeof displayValue === "undefined" || displayValue == null)
  		return "";

  	return displayValue;
  },

  //Creates required URL the honours two-step property value for creating change from template form
  getActionURL: function(template) {
  	var gu;

  	if (this._getBooleanPropertyValue(this.PROP_TWO_STEP, false)) {
  		gu = new GlideURL(this.TABLE_NAME_CHANGE + '.do');
  		gu.set('sys_id', '-1');
  		gu.set('sysparm_query', this.getQueryParams(template));
  	} else {
  		gu = new GlideURL('std_change_processor.do');
  		gu.set('sysparm_id', template.getUniqueValue());
  		gu.set('sysparm_action', 'execute_producer');
  		gu.set('sysparm_catalog', template.sc_catalogs);
  		gu.set('sysparm_ck', gs.getSessionToken());
  	}

  	return gu.toString();
  },

  getQueryParams: function(template) {
  	if (!template)
  		return '';

  	var chgType = '';
  	if (gs.getProperty('com.snc.change_management.change_model.type_compatibility', false) === 'true')
  		chgType = this.ATTR_TYPE + '=' + this.DEFAULT_CHG_TYPE;
  	else
  		chgType = this.ATTR_CHG_MODEL + '=' + this.DEFAULT_CHG_MODEL;

  	return chgType + '^std_change_producer_version=' + template.getValue('current_version');
  },

  //Creates a URL to be used when creating a Standard Change from another task (eg: incident)
  getURLForTask: function(record, field) {
  	if (!record || !field)
  		return;

  	var target = 'table:' + record.getTableName();
  	target += ';sysid:' + record.getUniqueValue();
  	target += ';field:' + field;

  	var gu = new GlideURL('com.glideapp.servicecatalog_category_view.do');
  	gu.set('v', '1');
  	gu.set('sysparm_parent', this._getPropertyValue(this.ATTR_CATEGORY));
  	gu.set('sysparm_cartless', 'true');
  	gu.set('sysparm_processing_hint', target);

  	return gu.toString();
  },

  //Reapplies the values for the readonly fields from the template associated to the change record
  reapplyReadonlyTemplateFields: function(record) {
  	if (!record)
  		return;

  	var evalObj = new GlideScriptEvaluator();
  	evalObj.setEnforceSecurity(true);

  	var template = new GlideRecord(this.TABLE_NAME_TEMPLATE);
  	if (template.get(record.std_change_producer_version.std_change_producer.template)) {
  		var readonlyFields = this.getReadOnlyFields();
  		var queryValues = this._parseEncodedQuery(template.getValue('template'), true /* noEscape */);

  		for (var j = 0; j < readonlyFields.length; j++) {
  			var nameIndex = queryValues.names.indexOf(readonlyFields[j]);
  			if (nameIndex > -1) {
  				var value = queryValues.vals[nameIndex];
  				if (value.startsWith("javascript:"))
  					value = evalObj.evaluateString(value, false);

  				record.setValue(readonlyFields[j], value);
  			}
  		}
  	}
  },

  hasPropertyFieldChanged: function(propertyGr) {
  	return propertyGr.mandatory_fields.changes() || propertyGr.restricted_fields.changes() || propertyGr.default_values.changes() || propertyGr.fields_to_copy.changes();
  },

  checkFieldsToCopyWithMandatory: function(mandatoryFields, defaultValues, fieldsToCopy) {
  	var changeGr = new GlideRecord(this.TABLE_NAME_CHANGE);
  	changeGr.initialize();

  	/* First check that the combination of defaulted fields and fields to copy
  	   covers all the mandatory fields - if it doesn't we can't copy any Change */
  	var missingFields = [];
  	var fieldName;
  	var defaultFields = this._parseEncodedQuery(defaultValues).names;
  	for (var i = 0; i < mandatoryFields.length; i++) {
  		fieldName = mandatoryFields[i];
  		if (!this.arrayUtil.contains(defaultFields, fieldName) && !this.arrayUtil.contains(fieldsToCopy, fieldName))
  			missingFields.push(changeGr[fieldName].getLabel());
  	}

  	return missingFields;
  },

  _createScriptContent: function(versionId, producerId) {
  	var scriptContent = 'current.' + this.ATTR_STD_CHG_PRODUCER_VERSION + ' = "' + versionId + '";\n';
  	scriptContent += 'if (gs.getProperty("com.snc.change_management.change_model.type_compatibility", "false") === "true")\n';
  	scriptContent += '	current.' + this.ATTR_TYPE + ' = "' + this.DEFAULT_CHG_TYPE + '";\n';
  	scriptContent += 'else\n';
  	scriptContent += '	current.' + this.ATTR_CHG_MODEL + ' = "' + this.DEFAULT_CHG_MODEL + '";\n';
  	scriptContent += 'GlideSysAttachment.copy("' + this.TABLE_NAME_PRODUCER + '", "' + producerId + '", current.getTableName(), current.getUniqueValue());\n';

  	return scriptContent;
  },

  _isCloseCodeUnsuccessful: function(closeCode) {
  	return Array.isArray(this.CLOSE_CODES_UNSUCCESSFUL) && this.CLOSE_CODES_UNSUCCESSFUL.indexOf(closeCode) >= 0;
  },

  type: 'StdChangeUtilsSNC'
});

Sys ID

f10d6bf3931002003b7a7a75e57ffb73

Offical Documentation

Official Docs: