Join the #BuildWithBuildAgent Challenge! Get recognized, earn exclusive swag, and inspire the ServiceNow Community with what you can build using Build Agent.  Join the Challenge.

Why the validateProposal not found in the OOTB script include global.StdChangeUtilsSNC

June3
Tera Contributor

In the OOB action Validate Standard Change Proposal called by the flow Standard Change - Standard - Proposal, it declares 

var utils = new global.StdChangeUtils();
    if (inputs.proposal.proposal_type != utils.PROPOSAL_TYPE_RETIRE) {
        var result = utils.validateProposal(inputs.proposal, true /* Show errors */, true /* Validate tasks */);
        if (!result) {
But inside the OOB script include StdChangeUtils and StdChangeUtilsSNC, no function validateProposal found. So how the action works since no function determined.
3 REPLIES 3

Shruti
Mega Sage
Mega Sage

Hi,

It is declared in StdChangeUtilsSNC script include

Shruti_0-1764578660717.png

 

June3
Tera Contributor

I am in Zurich, mine is nothing found

June3_0-1764578830190.png

 

I'm also in Zurich. It's there. Try to repair Change Management - Standard Change Catalog plugin

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_SYS_ID: 'sys_id',
	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);
	},

	copyAttachments: function(srcGr, targetGr, removeExisting) {
		if (!srcGr.canRead()) {
			gs.warn("copyAttachments: User is not authorized to perform this action");
			return;
		}

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

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

			while (attachmentGr.next()) 
				attachmentIds.push(j2js(attachmentGr.sys_id));

			this._deleteAttachments(attachmentIds);
		}

		// srcGr declared on line 210
		if (srcGr.hasAttachments())
			j2js(GlideSysAttachment.copy(srcGr.getTableName(), srcGr.getUniqueValue(), targetGr.getTableName(), targetGr.getUniqueValue()));

	},

	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.name.setDisplayValue(stdChgProposalGR.template_name.getDisplayValue());
		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();

		//Update the version record with the new name
		var versionGr = new GlideRecord(this.TABLE_NAME_VERSION);
		if (versionGr.get(versionId)) {
			versionGr.setValue('name', this.getVersionName(versionGr));
			versionGr.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);
			ans.hasAttachments = producerGr.hasAttachments() + '';
			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;
			}
		}

		// The addition of the SYS_ID at the end is important. GlideAgg will automatically add the dot walked fields display value to the group by.
		// We don't really want this as it doesn't play nicely with sorting. We never want the names included in sorting, only the sys_id to prevent issues with Producers with
		// the same name.
		var dotWalkedProducerVersionProducerSysId = this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_STD_CHG_PRODUCER + '.' + this.ATTR_SYS_ID;
		var dotWalkedProducerVersionSysId = this.ATTR_STD_CHG_PRODUCER_VERSION + '.' + this.ATTR_SYS_ID;

		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(dotWalkedProducerVersionProducerSysId);
		chgGr.groupBy(dotWalkedProducerVersionSysId);
		chgGr.groupBy(this.ATTR_CLOSE_CODE);
		chgGr.addAggregate("COUNT");
		chgGr.orderBy(dotWalkedProducerVersionProducerSysId);
		chgGr.orderBy(dotWalkedProducerVersionSysId);
		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(dotWalkedProducerVersionProducerSysId) || 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(dotWalkedProducerVersionSysId) || lastRecord)) {
				this._updateVersionStats(currentProducerVersionId, { total: versionTotalCount, totalUnsuccessful: versionUnsuccessfulCount });
				usedProducerVersionIds.push(currentProducerVersionId);
				versionUnsuccessfulCount = 0;
				versionTotalCount = 0;
			}

			currentProducerId = chgGr.getValue(dotWalkedProducerVersionProducerSysId);
			currentProducerVersionId = chgGr.getValue(dotWalkedProducerVersionSysId);
			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 */
		var dotWalkedProducerSysId = this.ATTR_STD_CHG_PRODUCER + '.' + this.ATTR_SYS_ID;

		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(dotWalkedProducerSysId);
		stdChgProducerVersionGr.query();

		while (stdChgProducerVersionGr.next())
			this._updateTemplateStats(stdChgProducerVersionGr.getUniqueValue(), stdChgProducerVersionGr.getValue(dotWalkedProducerSysId), { 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&colon;'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&colon;')) {
				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&colon;"))
						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;
	},

	skippedApproval: function(proposalGr) {
        //Requery proposal to make sure the record is not stale
        var propGr = new GlideRecord(this.TABLE_NAME_PROPOSAL);
        if (propGr.get(proposalGr.getUniqueValue())) {
            gs.addErrorMessage(gs.getMessage('No Approver present. Please request System Administrator to configure Approvers'));
            propGr.state = this.STATE_NEW;
            propGr.update();
        }
	},

	rejectedProposalComments: function(proposalGr) {
        // Stamping the rejection comment to proposal record
        var app = new GlideRecord('sysapproval_approver');
        app.addQuery('sysapproval', proposalGr.getUniqueValue());
        app.addQuery('state', 'rejected');
        app.orderByDesc('sys_updated_on');
        app.setLimit(1);
        app.query();

		if(app.next()) {
			var commentsGR = new GlideRecord('sys_journal_field');
			commentsGR.addQuery('element_id', app.getUniqueValue());
			commentsGR.orderByDesc('sys_created_on');
			commentsGR.setLimit(1);
			commentsGR.query();
			if(commentsGR.next()) {
                //Requery proposal to make sure the record is not stale
                var propGr = new GlideRecord(this.TABLE_NAME_PROPOSAL);
                if (propGr.get(proposalGr.getUniqueValue())) {
                    propGr.comments = commentsGR.getValue('value');
                    propGr.state = this.STATE_NEW;
                    propGr.update();
                }
			}
		}
	},

	type: 'StdChangeUtilsSNC'
});