Name
global.PlannedTaskCalculator
Description
******* It is highly recommended that you do not modify this record! ******** The PlannedTaskCalculator is used with the Project Management plugin to help calculate updates to projects and tasks based upon dependencies and relationships.
Script
// ******* It is highly recommended that you do not modify this record! *******
///////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORTS
///////////////////////////////////////////////////////////////////////////////////////////////////
var HashSet = Packages.java.util.HashSet;
// /////////////////////////////////////////////////////////////////////////////////////////////////
// UTILITY FUNCTIONS
// /////////////////////////////////////////////////////////////////////////////////////////////////
function indexOf(obj, array) {
for ( var i = 0; i < array.length; i++) {
if (array[i] == obj)
return i;
}
return -1;
}
function inArray(obj, array) {
return indexOf(obj, array) >= 0 ? true : false;
}
function empty(a) {
return JSUtil.nil(a) || a.length == 0;
}
function notEmpty(a) {
return !empty(a);
}
function getTimeDisplay(timeMs) {
if (!timeMs)
return "null";
var gdt = new GlideDateTime();
gdt.setNumericValue(timeMs);
return gdt.getDisplayValueInternal();
}
var PlannedTaskCalculator = Class.create();
PlannedTaskCalculator.prototype = {
// Status code constants that can be used for adding a relationship or other functions that return a value that is
// dynamic.
STATUS_CODE : {
SUCCESS : 1,
DUPLICATE : 2,
RECURSIVE : 3,
INVALID : 4
},
START_ON : "start_on",
ASAP : "asap",
initialize : function() {
if (gs.getProperty("com.snc.planned_task.debug") == "true")
this.debug = true;
delete this.projectTasks;
this.projectTasks = {};
this.top_task = '';
this.onUpdateRelationshipsToRemove = []; // Object {successor_sys_id: , predecessor_sys_id: }
this.onUpdateRelationshipsToAdd = []; // Object {successor_sys_id: , predecessor_sys_id: }
this.count = 0;
this.changes = 0;
this.schedule = null;
this.timezone = '';
this._loadComplete = false;
},
setDebug : function(value) {
this.debug = value;
},
load : function(originalRecord) {
this._loadComplete = false;
if (JSUtil.nil(originalRecord)) {
gs.log("Can't load with an invalid record, aborting.", "PlannedTaskCalculator");
this._loadFailureReason = "Parameter 'originalRecord' was null.";
return;
}
var topTask = originalRecord.top_task.getRefRecord();
if (!topTask.isValidRecord())
topTask = originalRecord;
if (JSUtil.nil(topTask.getUniqueValue())) {
gs.log("Can't load with a null sys_id, aborting.", "PlannedTaskCalculator");
this._loadFailureReason = "topTask found but getUniqueValue() is null.";
return;
}
this.top_task = topTask.getUniqueValue();
var sw;
if (this.debug) {
sw = new GlideStopWatch();
this._debugPrint('load: ' + ' :: Loading project ' + topTask.getDisplayValue());
}
// only for project top task
if (topTask.sys_class_name == "pm_project")
this._setScheduleAndTimezone(topTask);
this._iterateRecords(topTask);
this._iteratePredecessors();
this._iterateSuccessors();
this._iterateChildren();
if (this.debug) {
this.printProject(false);
sw.log(this.type + ' :: Finished loading project.');
}
this._loadComplete = true;
},
loadValid: function(){
if (!this._loadComplete)
gs.log("Execution cancelled, PlannedTaskCalculator has not been loaded correctly.", "PlannedTaskCalculator");
return this._loadComplete;
},
getTopTaskShortDescription : function() {
return this.projectTasks[this.top_task].short_description;
},
printProject : function(boolPrintProperties) {
this._debugPrint("printProject: Printing project ...");
// Iterate through project objects
for ( var task in this.projectTasks) {
var currentProjectTask = this.projectTasks[task];
this._debugPrint('printProject: Project task - ' + currentProjectTask.short_description);
if (boolPrintProperties)
this.printTask(this.projectTasks[task]);
}
},
printTask : function(task) {
this._debugPrint('printTask: Printing properties for task [' + task.short_description + ' ] ----');
for ( var prop in task)
this._debugPrint('printTask : > ' + prop + ': ' + task[prop]);
},
changeProjectTask : function(taskSysID, fieldName, newValue) {
if (!this.loadValid())
return;
this._debugPrint("-----> changeProjectTask " + taskSysID + ", " + fieldName + ", " + newValue);
var task = this.projectTasks[taskSysID];
task.changed = 'true';
// If the value is for a date we need to update the display and duration
// times as well.
if (fieldName == 'start_date') {
task.start_date = newValue > task.end_date ? task.end_date : parseInt(newValue);
// if schedule see if new date is in schedule otherwise default to
// latest work time
if (this.schedule) {
var startGDT = new GlideDateTime();
startGDT.setNumericValue(task.start_date);
if (!this.schedule.isInSchedule(startGDT)) {
// get first work time after startGDT
var diff = this.schedule.whenNext(startGDT);
this._debugPrint(" changeProjectTask: found schedule difference of " + diff + " for a date of " + startGDT.getDisplayValue());
// if successful then use schedule date, otherwise take what
// we were given
if (diff != -1) {
startGDT.add(diff);
task.start_date = startGDT.getNumericValue();
}
}
}
task.duration = Math.max(0, this._calcDurationFromSchedule(parseInt(task.start_date), parseInt(task.end_date)));
} else if (fieldName == 'end_date') {
task.end_date = newValue < task.start_date ? task.start_date : parseInt(newValue);
// if schedule see if new date is in schedule otherwise default to latest work time
if (this.schedule) {
var endGDT = new GlideDateTime();
endGDT.setNumericValue(task.end_date);
if (!this.schedule.isInSchedule(endGDT)) {
// get latest work time before endGDT
var diff = this.schedule.whenLast(endGDT);
this._debugPrint(" changeProjectTask: found schedule difference of " + diff + " for a date of " + endGDT.getDisplayValue());
// if successful then use schedule date, otherwise take what we were given
if (diff != -1) {
endGDT.add(diff);
task.end_date = endGDT.getNumericValue();
}
}
}
if (task.work_start != '') {
task.duration = Math.max(0, this._calcDurationFromSchedule(parseInt(task.work_start), parseInt(task.end_date)));
this._debugPrint(" changeProjectTask: calc duration from actual end date, new duration = " + task.duration);
} else {
task.duration = Math.max(0, this._calcDurationFromSchedule(parseInt(task.start_date), parseInt(task.end_date)));
this._debugPrint(" changeProjectTask: calc duration from planned end date, new duration = " + task.duration);
}
} else if (fieldName == 'duration') {
task.end_date = parseInt(this._calcEndDateFromSchedule(task.start_date, task.duration));
task.duration = parseInt(newValue);
} else if (fieldName == 'parent') {
this.validateRelationships();
}
else {
task[fieldName] = newValue;
}
},
shiftProjectTask : function(taskSysID, newStartTimeMs) {
if (!this.loadValid())
return;
this._debugPrint("shiftProjectTask " + taskSysID + ", " + newStartTimeMs);
var task = this.projectTasks[taskSysID];
// There is one pre-check that needs to be done for
task.time_constraint = this.START_ON;
task.changed = 'true';
task.start_date = parseInt(newStartTimeMs);
task.end_date = this._calcEndDateFromSchedule(parseInt(newStartTimeMs), parseInt(task.duration));
},
isNewRelationshipValid : function(predecessorId, successorId) {
if (!this.loadValid())
return;
var pred = this.projectTasks[predecessorId];
// Check to see if this relationship already exists?
for ( var i in pred.successors) {
if (pred.successors[i] == successorId)
return this.STATUS_CODE.DUPLICATE;
}
return this.isExistingRelationshipValid(predecessorId, successorId);
},
/**
* Returns a value specified by the [ this.STATUS_CODE ] constant that specifies whether the actual existing relationship or theoretical (not yet
* created) relationship is valid. It is up to the caller to appropriately handle the recourse action.
*/
isExistingRelationshipValid : function(predecessorId, successorId) {
var pred = this.projectTasks[predecessorId];
// Check to see if this relationship creates a recursive relationship?
// This is a very complicated loop that determines all the nodes that affect the successor. If the successor
// matches any of the found nodes, recursion will result.
var setInvalidNodes = this._getAffectedChildrenNodes(pred, new HashSet());
var currentTask = pred;
while (true) {
setInvalidNodes = this._getAffectedPredChildrenNodes(currentTask, setInvalidNodes);
if (currentTask.parent == '')
break;
currentTask = this.projectTasks[currentTask.parent];
}
if (this.debug) {
this._debugPrint('isExistingRelationshipValid ---- List of Invalid Successor Nodes For [' + pred.short_description + '] ----');
var it = setInvalidNodes.iterator();
while (it.hasNext())
this._debugPrint('isExistingRelationshipValid :: [+] ' + this.projectTasks[it.next()].short_description);
}
if (setInvalidNodes.contains(successorId))
return this.STATUS_CODE.RECURSIVE;
return this.STATUS_CODE.SUCCESS;
},
/**
* This function adds the specified relationship. Note it does not check to see if the relationship is valid. It is up to the caller to execute [
* this.isNewRelationshipValid() ] and handle the appropriate action prior to adding a relationship.
*/
addRelationship : function(predecessorId, successorId) {
var succ = this.projectTasks[successorId];
var pred = this.projectTasks[predecessorId];
// Update the internal object.
pred.successors.push(successorId.toString());
succ.predecessors.push({
relationship_sys_id : '',
predecessor_sys_id : predecessorId.toString()
});
this.onUpdateRelationshipsToAdd.push({
successor_sys_id : successorId.toString(),
predecessor_sys_id : predecessorId.toString()
});
// Mark both as changed and shift the successor (if necessary)
pred.changed = 'true';
succ.changed = 'true';
if (parseInt(succ.start_date) < parseInt(pred.end_date)) {
succ.time_constraint = this.ASAP;
succ.start_date = pred.end_date;
succ.end_date = this._calcEndDateFromSchedule(parseInt(succ.start_date), parseInt(succ.duration));
}
},
/**
* Removes the specified relationship from the current object and adds this relationship to the array that holds relationships to delete on
* update().
*/
removeRelationship : function(predecessorId, successorId) {
// Remove the successor from the predecessor
var index = indexOf(this.projectTasks[predecessorId].successors, successorId);
this.projectTasks[predecessorId].successors.splice(index, 1);
this.projectTasks[predecessorId].changed = 'true';
this.projectTasks[successorId].changed = 'true';
// Remove the predecessor from the successor and store the sys_id
for ( var i = 0; i < this.projectTasks[successorId].predecessors.length; i++) {
if (this.projectTasks[successorId].predecessors[i].predecessor_sys_id == predecessorId) {
// Add the IDs to the list to remove
this.onUpdateRelationshipsToRemove.push(this.projectTasks[successorId].predecessors[i].relationship_sys_id);
this.projectTasks[successorId].predecessors.splice(i, 1);
break;
}
}
},
changeParent : function(itemTaskSysId, newParentSysId) {
this._debugPrint("changeParent " + itemTaskSysId + ", " + newParentSysId);
var status = this.checkNewParentValidity(itemTaskSysId, newParentSysId);
if (status != this.STATUS_CODE.SUCCESS)
return status;
var task = this.projectTasks[itemTaskSysId];
// See if the new parent is a child of the current task. If this is the case, then we have a problem.
if (this._isParentAChildOfTask(itemTaskSysId, newParentSysId))
return this.STATUS_CODE.INVALID;
else if (task.parent == newParentSysId)
return this.STATUS_CODE.DUPLICATE;
// Update the old parent's [ children Array ] ..(Splice after finding index. IndexOf doesn't work)
var oldParent = this.projectTasks[task.parent];
for ( var i in oldParent.children) {
if (itemTaskSysId == oldParent.children[i]) {
oldParent.changed = 'true';
oldParent.children.splice(i, 1);
if (empty(oldParent.children))
oldParent.rollup = false;
break;
}
}
// Update the new parent's [ children Array ] with the newly added
// child.
var newParent = this.projectTasks[newParentSysId];
newParent.children.push(itemTaskSysId);
newParent.rollup = true;
newParent.changed = 'true';
// Update the current task with new parent
task.parent = newParentSysId;
task.changed = 'true';
task.start_date = newParent.start_date;
task.end_date = this._calcEndDateFromSchedule(task.start_date, task.duration);
task.time_constraint = this.ASAP;
if (this.debug)
this._debugPrint('changeParent: > Setting [' + task.short_description + ']\'s parent to: [' + this.projectTasks[task.parent].short_description + ']');
},
getTask : function(id) {
return this.projectTasks[id];
},
recalcProject : function() {
if (!this.loadValid())
return;
this._recalcProject(false);
},
recalcProjectEndDates : function() {
this._recalcProject(true);
},
_addPreBubbleChange : function() {
this.impactedChanges++;
},
_addBubbleChange : function() {
this.bubbleChanges++;
this.impactedChanges++;
},
_recalcProject : function(boolCalcEndDates) {
this._debugPrint("_recalcProject " + boolCalcEndDates);
if (this.debug) {
var sw = new GlideStopWatch();
this._debugPrint('_recalcProject: Begin recalcProject()' + (boolCalcEndDates ? ' (including end dates)' : ''));
}
this.bubbleChanges = 0;
this.impactedChanges = 0;
this._recalcProjectBubble(boolCalcEndDates);
this._processCriticalPath();
if (this.debug) {
var count = 0; // Get the total number of changed items
for ( var i in this.projectTasks) {
if (this.projectTasks[i].changed == 'true')
count++;
}
sw.log(this.type + ' :: Finished recalcProject() -- Updated ' + count + ' tasks (' + this.impactedChanges + ' impacted changes)');
}
return this.STATUS_CODE.SUCCESS;
},
_recalcProjectBubble : function(boolCalcEndDates) {
this.bubbleChanges = 0;
this._debugPrint("");
this._debugPrint("");
this._debugPrint("");
this._debugPrint("");
this._debugPrint("_recalcProjectBubble " + boolCalcEndDates);
var json = new JSON();
var text = json.encode(this.projectTasks);
this._debugPrint("_recalcProjectBubble - JSON= " + text);
for ( var i in this.projectTasks) {
if (boolCalcEndDates)
this._recalcScheduleEndDate(this.projectTasks[i]);
this._recalcStartDate(this.projectTasks[i]);
this._recalcEndDate(this.projectTasks[i]);
}
this._debugPrint("_recalcProjectBubble - bubbleChanges = " + this.bubbleChanges);
if (this.bubbleChanges > 0 && (this._validData(this.projectTasks[i])))
this._recalcProjectBubble();
},
_validData : function(task) {
if (isNaN(task.start_date) || isNaN(task.end_date)) {
this._debugPrint("_validData - task.start_date = " + task.start_date + ", end_date=" + task.start_date);
return false;
}
if (task.countRecalcProjectBubble > 5) {
this._debugPrint("_validData - _recalcProjectBubble is ignored now for " + task.number + " countRecalcProjectBubble=" + task.countRecalcProjectBubble);
return false;
} else {
this._debugPrint("_validData - _recalcProjectBubble unexpected for task=" + task.number + " countRecalcProjectBubble=" + task.countRecalcProjectBubble);
task.countRecalcProjectBubble++;
}
return true;
},
_setScheduleAndTimezone : function(parentRecord) {
var project = new GlideRecord("pm_project");
if (project.get(parentRecord.getUniqueValue())) {
if (!JSUtil.nil(project.schedule)) {
this._debugPrint("_setScheduleAndTimezone: using schedule " + project.schedule.getDisplayValue() + ":" + project.schedule);
this.schedule = new GlideSchedule(project.schedule);
if (!this.schedule.isValid())
this.schedule = null;
} else
this._debugPrint("_setScheduleAndTimezone: no project schedule specified");
}
},
_iterateRecords : function(parentRecord) {
// Query for all tasks within this project
var projectTask = new GlideRecord('planned_task');
projectTask.addQuery('top_task', parentRecord.getUniqueValue());
projectTask.orderBy('start_date');
projectTask.query();
this._debugPrint("_iterateRecords query: " + projectTask.getEncodedQuery() + " = " + projectTask.getRowCount());
while (projectTask.next()) {
// Create an object for each task
this._createProjectTask(projectTask);
}
},
_createProjectTask : function(projectTask) {
var newProjectTask = {};
newProjectTask.short_description = projectTask.short_description.toString();
newProjectTask.children = [];
newProjectTask.predecessors = [];
newProjectTask.successors = [];
newProjectTask.state = projectTask.state.toString();
newProjectTask.state_display = projectTask.state.getDisplayValue();
newProjectTask.number = projectTask.number.toString();
newProjectTask.assigned_to = projectTask.assigned_to.getDisplayValue().toString();
newProjectTask.sys_id = projectTask.getUniqueValue();
newProjectTask.schedule = projectTask.schedule;
newProjectTask.timezone = projectTask.timezone;
newProjectTask.start_date = projectTask.start_date.getGlideObject().getNumericValue(); // planned start
newProjectTask.work_start = projectTask.work_start.getGlideObject().getNumericValue(); // actual start
newProjectTask.end_date = projectTask.end_date.getGlideObject().getNumericValue(); // planned end
newProjectTask.work_end = projectTask.work_end.getGlideObject().getNumericValue(); // actual end
newProjectTask.duration = projectTask.duration.getGlideObject().getNumericValue();
newProjectTask.duration_display = projectTask.duration.getDisplayValue();
newProjectTask.work_duration = projectTask.work_duration.getGlideObject().getNumericValue();
// newProjectTask.work_duration_display = projectTask.work_duration.getDisplayValue();
try {
newProjectTask.work_duration_display = projectTask.work_duration.getDisplayValue();
} catch (e) {
newProjectTask.work_duration_display = "Undetermined";
gs.print("Unable to determine work duration display value for: " + projectTask.sys_id);
}
newProjectTask.time_constraint = projectTask.time_constraint.toString();
newProjectTask.parent = projectTask.parent.toString();
newProjectTask.rollup = projectTask.getValue("rollup");
newProjectTask.active = projectTask.active.toString();
newProjectTask.critical_path = projectTask.critical_path.toString();
newProjectTask.percent_complete = projectTask.percent_complete.toString();
newProjectTask.changed = 'false';
newProjectTask.countRecalcProjectBubble = 0;
// Add the new task object to the big projectTasks object
this.projectTasks[projectTask.sys_id.toString()] = newProjectTask;
},
_iteratePredecessors : function() {
// Create an array of tasks first
var taskArray = [];
for ( var task in this.projectTasks)
taskArray.push(this.projectTasks[task].sys_id);
// Now make a single query
var rel = new GlideRecord('planned_task_rel_planned_task');
rel.addQuery('child', taskArray);
rel.addQuery('type.name', 'Predecessor of::Successor of');
rel.query();
this._debugPrint("_iteratePredecessors Query: " + rel.getEncodedQuery() + " = " + rel.getRowCount());
while (rel.next())
this.projectTasks[rel.child].predecessors.push({
relationship_sys_id : rel.getUniqueValue(),
relationship_table : "planned_task_rel_planned_task",
predecessor_sys_id : rel.parent.toString(),
relationship_lag : rel.lag.getGlideObject().getNumericValue().toString()
});
},
_iterateSuccessors : function() {
// Create an array of tasks first
var taskArray = [];
for ( var task in this.projectTasks)
taskArray.push(this.projectTasks[task].sys_id);
// Now make a single query
var rel = new GlideRecord('planned_task_rel_planned_task');
rel.addQuery('parent', taskArray);
rel.addQuery('type.name', 'Predecessor of::Successor of');
rel.query();
this._debugPrint("_iterateSuccessors Query: " + rel.getEncodedQuery() + " = " + rel.getRowCount());
while (rel.next())
this.projectTasks[rel.parent].successors.push(rel.child.toString());
},
_iterateChildren : function() {
for ( var task in this.projectTasks) {
var currentProjectTask = this.projectTasks[task];
if (currentProjectTask.parent != '' && currentProjectTask.parent != currentProjectTask.sys_id)
this.projectTasks[currentProjectTask.parent].children.push(currentProjectTask.sys_id.toString());
}
},
_isParentAChildOfTask : function(taskId, newParentSysId) {
var task = this.projectTasks[taskId];
for ( var i in task.children) {
if (task.children[i] == newParentSysId || this._isParentAChildOfTask(task.children[i], newParentSysId))
return true;
}
return false;
},
_getAffectedChildrenNodes : function(t, set) {
// short-circuit if t is already in set
if (inArray(t.sys_id.toString(), set))
return set;
for ( var i in t.children)
set = this._getAffectedChildrenNodes(this.projectTasks[t.children[i]], set);
set.add(t.sys_id.toString());
return set;
},
_getAffectedPredChildrenNodes : function(t, set) {
// short-circuit if t is already in set
if (inArray(t.sys_id.toString(), set))
return set;
for ( var i in t.predecessors) {
var task = this.projectTasks[t.predecessors[i].predecessor_sys_id];
if (notEmpty(task.predecessors))
set = this._getAffectedPredChildrenNodes(task, set);
set.add(task.sys_id);
if (notEmpty(task.children))
set = this._getAffectedChildrenNodes(task, set);
}
set.add(t.sys_id.toString());
return set;
},
validateRelationships : function() {
if (this.debug) {
var sw = new GlideStopWatch();
this._debugPrint('validateRelationships: Begin validateRelationships()');
}
// Since the removeRelationship() method clears out items inside the
// appropriate tasks predecessors and successors
// array, the looping of a tasks predecessors and successors is
// extremely dangerous. Therefore, we store the relationships
// to delete in this temporary cleanList array during pred/succ
// enumeration ... and then delete afterwards.
var tmpCleanList = [];
var removedRelationships = []; // [] = "predecessor.sys_id
// successor.sys_id"
for ( var i in this.projectTasks) {
var t = this.projectTasks[i];
for ( var j = 0; j < t.successors.length; j++) {
var s = this.projectTasks[t.successors[j]];
// ==== Check Invalid Relationship Condition
// ==========================================+
var ret = this.isExistingRelationshipValid(t.sys_id, s.sys_id);
if (ret == this.STATUS_CODE.RECURSIVE)
tmpCleanList.push({
pred_sys_id : t.sys_id,
succ_sys_id : s.sys_id,
desc : this.type + ' :: > Removing [' + t.short_description + ']\'s successor [' + s.short_description + '] because it is an invalid relationship.'
});
// =====================================================================================+
} // ~end successors loop
} // ~end task loop
// Iterate through the list of items that need to be cleaned and clean
// them.
for ( var i = 0; i < tmpCleanList.length; i++) {
var obj = tmpCleanList[i];
// Has this been removed?
if (inArray(obj.pred_sys_id + ' ' + obj.succ_sys_id, removedRelationships))
continue;
// Remove the relationship
this.removeRelationship(obj.pred_sys_id, obj.succ_sys_id);
// Output debug information
this._debugPrint("validateRelationships: obj.desc = " + obj.desc);
// Add this to the array of removed relationships
removedRelationships.push(obj.pred_sys_id + ' ' + obj.succ_sys_id);
}
if (this.debug)
sw.log(this.type + ' :: End validateRelationships()');
},
/**
* 0 -
*/
_recalcScheduleEndDate : function(task) {
this._debugPrint("_recalcScheduleEndDate " + task.number);
if (notEmpty(task.children))
return;
var start;
if (task.work_start > 0)
start = task.work_start;
else
start = task.start_date;
var schEndDate = this._calcEndDateFromSchedule(parseInt(start), parseInt(task.duration));
if (schEndDate != parseInt(task.end_date)) {
this._addBubbleChange();
task.changed = 'true';
task.end_date = schEndDate;
if (this.debug)
this._debugPrint('_recalcScheduleEndDate: > Adjusting [' + task.short_description + '] to end date to reflect the current schedule.');
}
},
/**
* Adjust the start date for all tasks. A tasks start date is defined as: [time_constraint == 'start_on'] = > Task's current start date for fixed
* start date rollup tasks (with children) set start to earliest child start
*
* All asap rollup task's dates are derived from their children.
*
* [time_constraint == 'asap' ] > Latest between predecessor's end date and first parent with predecessor's start date [If no predecessor, then] >
* First parent with predecessor or project start date if no parent exists [If no parent or predecessor's exist,then: ] > Task's current start
* date
*/
_recalcStartDate : function(task) {
this._debugPrint("_recalcStartDate " + task.number + "(" + task.short_description + ")");
// no need to calc fixed start date tasks with no children
if (this._isStartDateFixed(task) && empty(task.children))
return;
// for fixed start rollup tasks, override the start with the earliest
// child task
if (this._isStartDateFixed(task) && task.changed != "true") {
this._debugPrint("--->_recalcStartDate: fixed start task rollup, check children for earliest start: " + task.number + "(" + task.short_description + ")"
+ " starts on " + task.start_date);
this.printTask(task);
this._debugPrint("--->_recalcStartDate: Fixed date rollup task, get earliest child: " + task.number);
var earliestChildStart = null;
this._forEach(task.children, function(elem) {
var childTask = this.projectTasks[elem];
if (earliestChildStart == null || this._getStartDate(childTask) < earliestChildStart) {
earliestChildStart = this._getStartDate(childTask);
this._debugPrint("------>_recalcStartDate: setting fixed task " + task.number + "(" + task.short_description + ")" + " start date from " + childTask.number);
}
});
if (task.start_date != earliestChildStart) {
this._debugPrint("--->_recalcStartDate setting start to " + earliestChildStart + " it was " + task.start_date);
task.start_date = earliestChildStart;
task.duration = this._calcDurationFromSchedule(parseInt(this._getStartDate(task)), parseInt(this._getEndDate(task)));
this._addBubbleChange();
task.changed = 'true';
}
return;
}
if (this._isStartDateFixed(task) && task.changed == "true") {
this._debugPrint("--->_recalcStartDate fixed task " + task.number + "(" + task.short_description + ")" + " was already changed, skipping start date calc");
return;
}
// Set parent start date to the earliest child
if (notEmpty(task.children)) {
// set planned start from children
this._debugPrint("--->_recalcStartDate set rollup start based on children of: " + task.number + "(" + task.short_description + ")");
var earliestChildStart = null; // planned start from children
var earliestChildWorkStart = 0; // actual start from children
// Steps over all children taking note of start_date or work_start
this._forEach(task.children, function(elem) {
var childTask = this.projectTasks[elem];
var childStart = parseInt(childTask.start_date);
var childWorkStart = parseInt(childTask.work_start);
if (this.debug) {
this._debugPrint("------>_recalcStartDate checking child " + childTask.number + "(" + childTask.short_description + ")" + " starts on "
+ getTimeDisplay(childStart) + " compared to " + getTimeDisplay(earliestChildStart));
this._debugPrint("------>_recalcStartDate checking child " + childTask.number + "(" + childTask.short_description + ")" + " work start on "
+ getTimeDisplay(childWorkStart) + " compared to " + getTimeDisplay(earliestChildWorkStart));
}
if (earliestChildStart == null || childTask.start_date < earliestChildStart) {
earliestChildStart = childTask.start_date;
if (this.debug)
this._debugPrint("------>_recalcStartDate moving earliestChildStart to " + getTimeDisplay(earliestChildStart));
}
if (childWorkStart > 0) {
if (earliestChildWorkStart == 0 || (earliestChildWorkStart > 0 && childWorkStart < earliestChildWorkStart)) {
earliestChildWorkStart = childWorkStart;
if (this.debug)
this._debugPrint("------>_recalcStartDate moving earliestChildWorkStart to " + getTimeDisplay(earliestChildWorkStart));
}
}
});
// If a change was made above we register the and bubble it
if (task.start_date != earliestChildStart) {
if (this.debug)
this._debugPrint("------> _recalcStartDate setting start to " + getTimeDisplay(earliestChildStart) + " it was " + getTimeDisplay(task.start_date));
task.start_date = earliestChildStart;
task.duration = this._calcDurationFromSchedule(parseInt(this._getStartDate(task)), parseInt(this._getEndDate(task)));
this._addBubbleChange();
task.changed = 'true';
}
if (task.work_start != earliestChildWorkStart) {
if (this.debug)
this._debugPrint("------> _recalcStartDate setting work_start to " + getTimeDisplay(earliestChildWorkStart) + " it was " + getTimeDisplay(task.work_start));
task.work_start = earliestChildWorkStart;
this._addBubbleChange();
task.changed = 'true';
}
return;
}
// If the current task doesn't have a parent and predecessor don't change it!
if (JSUtil.nil(task.parent) && empty(task.predecessors))
return;
/*
* For all other ASAP (non-rollup) tasks!
*/
this._debugPrint("--->_recalcStartDate: RECALCING PLANNED TASK PREDECESSORS, count= " + task.predecessors.length);
// Get the next parent that has a predecessor, or the top task.
var parent = this._getSignificantParent(task);
// If the task has no predecessors go and find the next parent in its hierarchy that has a predecessor.
if (empty(task.predecessors)) {
// Check if the parent's start_date is it's predecessors end_date. If not, move child's
// start_date and return. The parent's dates will be modified in the next iteration.
if (notEmpty(parent.predecessors)) {
var latest = this._getLatestPredecessor(parent);
if (parent.start_date != latest.endDate) {
if (this.debug)
this._debugPrint("------>_recalcStartDate: Parent has a predecessor, setting " + task.number + "(" + task.short_description + ")" + " from "
+ getTimeDisplay(task.start_date) + " to " + parent.number + " (" + parent.short_description + ")" + " end_date - "
+ getTimeDisplay(latest.endDate));
task.start_date = latest.endDate;
task.changed = 'true';
return;
}
}
// Set task's start date to parent's.
if (task.start_date != parent.start_date) {
if (this.debug)
this._debugPrint("------>_recalcStartDate: Setting task " + task.number + "(" + task.short_description + ")" + " from " + getTimeDisplay(task.start_date)
+ " to a parent's (" + parent.number + " (" + parent.short_description + ")" + ") start_date - " + getTimeDisplay(parent.start_date));
task.start_date = parent.start_date;
task.changed = 'true';
}
return;
}
// Get the latest predecessor task and date and check/set the tasks.
// If the the tasks's parent has a predecessor the task needs to be set to the latest of the two.
var latest = this._getLatestPredecessor(task);
if (notEmpty(parent.predecessors) && parent.start_date > latest.endDate) {
if (this.debug)
this._debugPrint("------>_recalcStartDate: Parent has a pred later than task's pred, setting " + task.number + "(" + task.short_description + ")" + " from "
+ getTimeDisplay(task.start_date) + " to " + parent.number + " (" + parent.short_description + ")" + " end_date - " + getTimeDisplay(latest.endDate));
task.start_date = parent.start_date;
task.end_date = this._calcEndDateFromSchedule(parseInt(this._getStartDate(task)), parseInt(task.duration));
task.changed = 'true';
} else {
if (this.debug)
this._debugPrint("--->_recalcStartDate: comparing parent start " + getTimeDisplay(this._getParentTask(task.parent).start_date) + " to latest predecessor "
+ getTimeDisplay(latest.endDate));
if (task.start_date != latest.endDate) {
if (this.debug)
this._debugPrint('--->_recalcStartDate: > Setting [' + task.short_description + '] to begin at the end of its latest predecessor ['
+ latest.task.short_description + ' - ' + getTimeDisplay(latest.endDate) + '].');
this._addBubbleChange();
task.changed = 'true';
task.start_date = latest.endDate;
task.end_date = this._calcEndDateFromSchedule(parseInt(this._getStartDate(task)), parseInt(task.duration));
}
}
},
/**
* 2 - Adjust the end date for all tasks. A tasks start date is defined as: > [no children] = Task's current end date > [has children] = Latest
* of: > Task's immediate children > Task's start date
*/
_recalcEndDate : function(task) {
this._debugPrint("_recalcEndDate " + task.number);
this._debugPrint(" _recalcEndDate start_date: " + task.start_date);
this._debugPrint(" _recalcEndDate work_start: " + task.work_start);
// calc end date from actual start if we have it
startDate = this._getStartDate(task);
// if (task.work_start > 0)
// var startDate = task.work_start;
// else
// var startDate = task.start_date;
// no children, calc end date from actual start + planned duration
if (empty(task.children)) {
endDate = this._calcEndDateFromSchedule(startDate, task.duration);
this._debugPrint("_recalcEndDate - no children, calc endDate from start or actual start, endDate = " + endDate + ", duration = " + task.duration);
if (task.end_date != endDate) {
this._addBubbleChange();
task.changed = 'true';
task.end_date = endDate;
this._debugPrint('_recalcEndDate: > Adjusting [' + task.short_description + ']\'s end date, calculated from either planned start or actual start plus duration');
}
return;
}
// check children for latest end date
var endDate = startDate;
var endDateTask;
for ( var i = 0; i < task.children.length; i++) {
var childTask = this.projectTasks[task.children[i]];
if (this._getEndDate(childTask) > endDate) {
endDate = this._getEndDate(childTask);
endDateTask = childTask;
}
}
this._debugPrint("_recalcEndDate endDate = " + endDate);
if (task.end_date != endDate) {
this._addBubbleChange();
task.changed = 'true';
task.end_date = endDate;
task.duration = this._calcDurationFromSchedule(parseInt(this._getStartDate(task)), parseInt(this._getEndDate(task)));
if (this.debug)
this._debugPrint('_recalcEndDate: > Adjusting [' + task.short_description + ']\'s end date to the latest end date of it\'s immediate children: ['
+ endDateTask.short_description + ']');
}
},
// Returns the latest predecessor and it's date including any lag time specified
_getLatestPredecessor : function(task) {
var latestPredEndDate = 0;
var latestPredTask = null;
var predEndDate = null;
this._forEach(task.predecessors, function(elem) {
var predTask = this.projectTasks[elem.predecessor_sys_id];
var predLag = elem.relationship_lag;
predEndDate = parseInt(this._getEndDate(predTask));
if (predLag)
predEndDate += parseInt(predLag);
// Compare against latest predecessor
if (latestPredEndDate < predEndDate) {
latestPredEndDate = predEndDate;
latestPredTask = predTask;
}
if (this.debug)
this._debugPrint("------>_recalcStartDate: found latest predecessor: " + latestPredTask.number + "(" + latestPredTask.short_description + ")" + " end date of "
+ getTimeDisplay(latestPredEndDate));
});
return !latestPredTask ? null : {
task : latestPredTask,
endDate : latestPredEndDate
};
},
// Returns the first significant parent in a tasks hierarchy.
// A significant parent is one that has a precedessor or has a "start_on" time constraint.
_getSignificantParent : function(task) {
var latestStartDate = 0;
var latestID = null;
var parent = null;
while (task.parent) {
parent = this.projectTasks[task.parent];
if (notEmpty(parent.predecessors) || this._isStartDateFixed(parent))
if (parent.start_date > latestStartDate) {
latestStartDate = parent.start_date;
latestID = parent.sys_id;
}
task = parent;
}
if (!latestID || !parent)
latestID = this.top_task;
return this.projectTasks[latestID];
},
_calcEndDateFromSchedule : function(startDateMS, durationMS) {
this._debugPrint("_calcEndDateFromSchedule - start=" + startDateMS + ", duration=" + durationMS);
if (isNaN(startDateMS)) {
this._debugPrint('_calcEndDateFromSchedule - overwrite with 0 when invalid startDateMS=' + startDateMS + ', durationMS=' + durationMS );
return 0;
}
var endDateMS = 0;
if (this.schedule) {
var startDateGDT = new GlideDateTime();
startDateGDT.setNumericValue(startDateMS);
var durationGD = new GlideDuration(durationMS);
var newEndDateGDT = this.schedule.add(startDateGDT, durationGD);
endDateMS = newEndDateGDT.getNumericValue();
// var dc = this._getDurationCalc();
// dc.setStartDateTime(startDateGDT);
// dc.calcDuration(parseInt(durationMS) / 1000);
// var newEndDate = dc.getEndDateTime();
// var newEndDateGDT = new GlideDateTime(newEndDate);
// endDateMS = newEndDateGDT.getNumericValue();
} else {
endDateMS = startDateMS + durationMS;
}
this._debugPrint("_calcEndDateFromSchedule returning " + endDateMS.toString());
return endDateMS;
},
_calcDurationFromSchedule : function(startDateMS, endDateMS) {
this._debugPrint("_calcDurationFromSchedule start=" + startDateMS + ", end=" + endDateMS);
var currentDuration = endDateMS - startDateMS;
var durationMS = currentDuration;
if (this.schedule) {
var startDateGDT = new GlideDateTime();
startDateGDT.setNumericValue(startDateMS);
var endDateGDT = new GlideDateTime();
endDateGDT.setNumericValue(endDateMS);
var durationGD = this.schedule.duration(startDateGDT, endDateGDT);
durationMS = durationGD.getNumericValue();
}
this._debugPrint("_calcDurationFromSchedule: durationMS=" + durationMS);
return durationMS;
},
_isRollup : function(task) {
return this.projectTask[task].rollup;
},
updateProject : function(insertRel) {
if (this.debug) {
var sw = new GlideStopWatch();
this._debugPrint('updateProject: Begin updateProject()');
}
if (typeof (insertRel) === 'undefined')
insertRel = true; // default - insert the entry
// Update individual tasks that have changed start times
var updateCount = 0; // Get the total number of changed items
for ( var i in this.projectTasks) {
if (this.projectTasks[i].changed != 'true')
continue;
var task = this.projectTasks[i];
this._debugPrint("updateProject: saving " + task.number);
this.printTask(task);
var gr = new GlideRecord('planned_task');
if (gr.get(task.sys_id)) {
gr.setWorkflow(false);
gr.time_constraint = task.time_constraint;
if (!isNaN(task.start_date))
gr.start_date.getGlideObject().setNumericValue(task.start_date);
if (!JSUtil.nil(task.work_start) && task.work_start != 0)
gr.work_start.getGlideObject().setNumericValue(task.work_start);
gr.end_date.getGlideObject().setNumericValue(task.end_date);
gr.critical_path = task.critical_path;
gr.duration.setDateNumericValue(task.duration);
gr.parent = task.parent;
gr.rollup = task.rollup;
gr.update();
updateCount++;
}
}
// Update relationships that need to be deleted
for ( var i = 0; i < this.onUpdateRelationshipsToRemove.length; i++) {
var gr = new GlideRecord('planned_task_rel_planned_task');
if (gr.get(this.onUpdateRelationshipsToRemove[i])) {
gr.setWorkflow(false);
gr.deleteRecord();
}
}
// Add relationships that need to be added
if (insertRel)
for ( var i = 0; i < this.onUpdateRelationshipsToAdd.length; i++) {
var gr = new GlideRecord('planned_task_rel_planned_task');
gr.initialize();
gr.type.setDisplayValue('Predecessor of::Successor of');
gr.setValue('parent', this.onUpdateRelationshipsToAdd[i].predecessor_sys_id);
gr.setValue('child', this.onUpdateRelationshipsToAdd[i].successor_sys_id);
gr.setWorkflow(false);
gr.insert();
}
this._debugPrint('updateProject: > Updated ' + updateCount + ' existing planned tasks.');
this._debugPrint('updateProject: > Removed ' + this.onUpdateRelationshipsToRemove.length + ' planned task relationships.');
this._debugPrint('updateProject: > Added ' + this.onUpdateRelationshipsToAdd.length + ' planned task relationships.');
if (this.debug)
sw.log(this.type + ' :: End updateProject()');
},
_processCriticalPath : function() {
if (this.debug) {
var sw = new GlideStopWatch();
this._debugPrint('_processCriticalPath: Begin of Critical Path Processing ...');
}
// Create array to hold changed tasks
var criticalTasks = Array();
// Now process the tasks starting with the top level task
this._markCritical(this.top_task, criticalTasks);
// Update tasks that have changed
for ( var i in this.projectTasks) {
var task = this.projectTasks[i];
if (task.critical_path == 'true' && !inArray(task.sys_id, criticalTasks)) {
task.critical_path = 'false';
task.changed = 'true';
this._addBubbleChange();
this._debugPrint('_processCriticalPath: > [' + task.short_description + '] is no longer apart of the critical path.');
} else if (task.critical_path != 'true' && inArray(task.sys_id, criticalTasks)) {
task.critical_path = 'true';
task.changed = 'true';
this._addBubbleChange();
this._debugPrint('_processCriticalPath: > [' + task.short_description + '] is now apart of the critical path.');
}
}
if (this.debug)
sw.log(this.type + ' :: Finished Processing Critical Path');
},
_markCritical : function(projectTaskId, criticalTasks) {
var projectTask = this.projectTasks[projectTaskId];
// Mark this task as critical
criticalTasks.push(projectTask.sys_id);
// Now process your children
if (notEmpty(projectTask.children)) {
for ( var i = 0; i < projectTask.children.length; i++) {
var childTask = this.projectTasks[projectTask.children[i]];
// If the child has the same end date as its parent, reprocess
// it as critical
if (childTask.end_date == projectTask.end_date)
this._markCritical(childTask.sys_id, criticalTasks);
}
}
// Now process task's predecessors
if (notEmpty(projectTask.predecessors)) {
for ( var i = 0; i < projectTask.predecessors.length; i++) {
var predTask = this.projectTasks[projectTask.predecessors[i].predecessor_sys_id];
// If the predecessor has the same end date as your start date,
// reprocess it as critical
if (predTask.end_date == projectTask.start_date)
this._markCritical(predTask.sys_id, criticalTasks);
}
}
},
/*
* get a tasks start date, actual wins if exists
*/
_getStartDate : function(task) {
if (!JSUtil.nil(task.work_start) && task.work_start > 0)
return task.work_start;
return task.start_date;
},
/*
* get a tasks start date, actual wins if exists
*/
_getEndDate : function(task) {
if (!JSUtil.nil(task.work_end) && task.work_end > 0)
return task.work_end;
return task.end_date;
},
_getDurationCalc : function() {
var dc = new DurationCalculator();
dc.setSchedule(this.schedule);
return dc;
},
_getParentTask : function(parentID) {
if (!parentID)
return null;
var parent = this.projectTasks[parentID];
return !parent ? null : parent;
},
_isStartDateFixed : function(task) {
return task.time_constraint == this.START_ON;
},
/*
* Output to console when debug enabled
*/
_debugPrint : function(msg) {
if (!this.debug)
return;
gs.log(this.type + ":: " + msg, this.type);
},
/*
* Sick of writing array iteration loops! This function does that FOR you (weak). Pass in the array and a function that you want to iteratate
* over. If you need the loop to break return true from your function.
*
* The current element and index are passed into the function call.
*/
_forEach : function(arr, func) {
var l = arr.length;
for ( var i = 0; i < l; i++)
if (func.call(this, arr[i], i))
break;
},
/*
* Any of the itemTask's successor (successor's successors) should not be new parent
* Any of the itemTask's children's successor should not be new parent
*/
checkNewParentValidity : function(itemTaskSysId, newParentSysId) {
if (typeof this.projectTasks[newParentSysId] == "undefined") {
this._debugPrint("checkNewParentValidity PTC has no effect on non planned_task parent for task " + this._info(itemTaskSysId));
return this.STATUS_CODE.SUCCESS; //From PTC perspective no effect
}
this._debugPrint("checkNewParentValidity " + this._info(itemTaskSysId) + " with new parent " + this._info(newParentSysId));
if (!this.loadValid())
return this.STATUS_CODE.INVALID;
if (this._parentInSuccessor(itemTaskSysId, newParentSysId))
return this.STATUS_CODE.INVALID;
var itemTask = this.projectTasks[itemTaskSysId];
for ( var i in itemTask.children) {
if (this._parentInSuccessor(itemTask.children[i], newParentSysId))
return this.STATUS_CODE.INVALID;
}
return this.STATUS_CODE.SUCCESS;
},
_parentInSuccessor : function(itemTaskSysId, newParentSysId) {
var itemTask = this.projectTasks[itemTaskSysId];
for ( var i in itemTask.successors) {
if (itemTask.successors[i] == newParentSysId) {
this._debugPrint("_parentInSuccessor found as predecessor " + this._info(itemTaskSysId) + " with successor " + this._info(newParentSysId));
return true;
}
if (this._parentInSuccessor(itemTask.successors[i], newParentSysId))
return true;
}
return false;
},
_info : function(id) {
var itemTask = this.projectTasks[id];
return itemTask.number + ":" + itemTask.short_description;
},
type : 'PlannedTaskCalculator',
};
Sys ID
3e3d0041ff320000dadaebcfebffadc0