Project state / Project task state

Susan Davidson
Tera Guru

We have a need for when project tasks (all those active) are placed on hold then the parent project also sets to onhold
The current plannedtaskstateutil sets it to Work In Progress instead if there is one or more closed tasks.
So if in this example We have 3 tasks on hold and one in closed complete the parent task is defaulting to Work In Progress - it should instead ignore the closed complete task in this case and show the parent task as ON HOLD (state 5) as all active tasks are on hold.
It should only pay attention to the closed complete when ALL tasks are closed complete.

I attempted to update the plannedtaskstateutil to achieve this but it is still not working.
If all tasks are placed in ON hold that DOES now work and set the parent on hold also but as soon as one task is in closed it ignores that rule.
How do we modify this/achieve this requirement; this is my current util script

/*
 * Planned Task State Management Utility
 * primarily used by the Task Active State Management business rule to set the active
 * field based on state changes
 * Can be called by any server script to determine inactive states, default work, or default
 * close states for a given table
 */

/*
 * Usage:
 * var stateUtil = new PlannedTaskStateUtil(current);
 * var openState = stateUtil.getDefaultOpenState();
 * var skippedState = stateUtil.getDefaultSkippedState();
 */

var PlannedTaskStateUtil = Class.create();

PlannedTaskStateUtil.PENDING_STATES = "pending_states";
PlannedTaskStateUtil.OPEN_STATES = "open_states";
PlannedTaskStateUtil.WORK_IN_PROGRESS = "work_in_progress_states";
PlannedTaskStateUtil.CLOSE_STATES = "close_states";
PlannedTaskStateUtil.SKIPPED_STATES = "skipped_states";
PlannedTaskStateUtil.ONHOLD_STATES = "onhold_states";

PlannedTaskStateUtil.prototype = {
   
    /*
    * static properties and default values
    */
    ATTR_DEFAULT_OPEN : "default_open_state",
    ATTR_DEFAULT_SKIPPED : "default_skipped_state",
    ATTR_DEFAULT_PENDING : "default_pending_state",
    ATTR_DEFAULT_WORK : "default_work_state",
    ATTR_DEFAULT_CLOSE : "default_close_state",
    ATTR_DEFAULT_ONHOLD : "default_onhold_state",
    SYSTEM_DEFAULT_OPEN : 1// task closed complete state
    SYSTEM_DEFAULT_SKIPPED : 7// task closed skipped state
    SYSTEM_DEFAULT_PENDING : -5,    //task pending state
    SYSTEM_DEFAULT_WORK : 2// task work in progress state
    SYSTEM_PENDING_STATES : [-5],   //as of now only one pending state, user can override in dictionary if more pending states are required.
    SYSTEM_OPEN_STATES   : [1],
    SYSTEM_WORK_IN_PROGRESS : [2],
    SYSTEM_CLOSE_STATES : [3,4,7],
    SYSTEM_ONHOLD_STATES : [5],
   
    /*
    * Init
    * called by new PlannedTaskStateUtil(gr)
    * @Param task GlideRecord
    */
    initialize : function(/*GlideRecord*/ task) {
       
        if (!task)
            return;
        var sysClassName;
        if(task.sys_class_name){
            sysClassName = task.sys_class_name;
        }else{
            sysClassName = task.getTableName();
        }
        var taskRec = new GlideRecord(sysClassName);
        taskRec.initialize();
        this.task = taskRec;
        this.stateBuckets = {};
        this.stateElement = this.task.state;
        this.stateAttributeExists = false// default to false
       
       
        // get optional attributes or use default values
        this._getDefaultOpen();
        this._getDefaultSkipped();
        this._getDefaultPending();
        this._getDefaultWorkState();
        this._getDefaultClose();
        this._getdefaultOnholdState();
        this._getPendingStates();
        this._getOpenStates();
        this._getWorkInProgressStates();
        this._getCloseStates();
        this._getSkippedStates();
        this._getOnholdStates();
        this._populateStateBuckets();
    },

    getDefaultState: function(table) {
        var tableDescriptor = new GlideTableDescriptor(table);
        var elementDescriptor = tableDescriptor.getElementDescriptor('state');
        var defaultState = elementDescriptor.getDefault();
        return defaultState? defaultState : this.getDefaultPendingState();
    },
   
    /*
    * Get the value for the default work state, defaults to 1 if not specified
    * @return int
    */
    getDefaultOpenState : function() {
        return this.defaultOpen;
    },
   
    /*
    * Get the value for the default close state, defaults to 3 if not specified
    * @return int
    */
    getDefaultSkippedState : function() {
        return this.defaultSkipped;
    },
   
    /*
    * Get the value for the default pending state, defaults to -5 if not specified
    * @return int
    */
    getDefaultPendingState : function() {
        return this.defaultPending;
    },
   
    getDefaultWorkState : function() {
        return this.defaultWork;
    },
   
    getDefaultCloseState : function() {
        return this.defaultClose;
    },
    getDefaultOnholdState : function() {
        return this.defaultOnhold;
    },
   
    getPendingStates : function() {
        return this.pendingStates;
    },
   
    getOpenStates : function() {
        return this.openStates;
    },
   
    getWorkInProgressStates : function() {
        return this.workInProgressStates;
    },
   
    getCloseStates : function() {
        return this.closeStates;
    },

    getSkippedStates: function() {
        return this.skippedStates;
    },
    getOnholdStates: function() {
        return this.onholdStates;
    },
   
     /*
    * Get the active status of a given state
    * @Param state value of the state field choice
    * @return boolean true if state is an active state
    */
   isStateInactive : function(state) {
      state = state + "";
      var arrUtil = new ArrayUtil();
      if (arrUtil.contains(this.closeStates, state))
         return true;
     
      return false;
   },
   
   
    isStateOpen : function(state) {
        state = state + "";
        var arrUtil = new ArrayUtil();
        if (arrUtil.contains(this.openStates, state))
            return true;
       
        return false;
    },
    isStateOnhold : function(state) {
        state = state + "";
        var arrUtil = new ArrayUtil();
        if (arrUtil.contains(this.onholdStates, state))
            return true;
       
        return false;
    },
    isStateWIP : function(state) {
        state = state + "";
        var arrUtil = new ArrayUtil();
        if (arrUtil.contains(this.workInProgressStates, state))
            return true;
       
        return false;
    },
   
    isStatePending : function(state) {
        state = state + "";
        var arrUtil = new ArrayUtil();
        if (arrUtil.contains(this.pendingStates, state))
            return true;
       
        return false;
    },
   
    //private methods used in init()
    _getDefaultOpen : function() {
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_OPEN);
        if (value)
            this.defaultOpen = value;
        else
            this.defaultOpen = this.SYSTEM_DEFAULT_OPEN;
    },
   
    _getDefaultSkipped : function() {
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_SKIPPED);
        if (value)
            this.defaultSkipped = value;
        else
            this.defaultSkipped = this.SYSTEM_DEFAULT_SKIPPED;
    },
    _getDefaultOnhold : function() {
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_ONHOLD);
        if (value)
            this.defaultOnhold = value;
        else
            this.defaultOnhold = this.SYSTEM_DEFAULT_ONHOLD;
    },  
    _getDefaultPending : function() {
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_PENDING);
        if (value)
            this.defaultPending = value;
        else
            this.defaultPending = this.SYSTEM_DEFAULT_PENDING;
    },
   
    _getDefaultWorkState: function() {
        // See if a value exists for the default work state attribute
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_WORK);
       
        if (value)
            this.defaultWork = value;
        else
            this.defaultWork = this.SYSTEM_DEFAULT_WORK;
    },
   
    _getDefaultClose : function() {
        var value = this.stateElement.getAttribute(this.ATTR_DEFAULT_CLOSE);
        if (value)
            this.defaultClose = value;
        else
            this.defaultClose = this.SYSTEM_DEFAULT_CLOSE;
    },
   
    _getPendingStates : function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.PENDING_STATES);
        if(attribute){
            var states = attribute.split(";");
            this.pendingStates = states;
        }
        else {
            this.pendingStates = this.SYSTEM_PENDING_STATES;
        }
    },
    _getOnholdStates : function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.ONHOLD_STATES);
        if(attribute){
            var states = attribute.split(";");
            this.OnholdStates = states;
        }
        else {
            this.OnholdStates = this.SYSTEM_ONHOLD_STATES;
        }
    },  
    _getOpenStates : function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.OPEN_STATES);
        if(attribute){
            var states = attribute.split(";");
            this.openStates = states;
        }
        else {
            this.openStates = this.SYSTEM_OPEN_STATES;
        }
    },
   
    _getWorkInProgressStates : function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.WORK_IN_PROGRESS);
        if(attribute){
            var states = attribute.split(";");
            this.workInProgressStates = states;
        }
        else {
            this.workInProgressStates = this.SYSTEM_WORK_IN_PROGRESS;
        }
       
    },
   
    _getCloseStates : function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.CLOSE_STATES);
        if(attribute){
            var states = attribute.split(";");
            this.closeStates = states;
        }
        else {
            this.closeStates = this.SYSTEM_CLOSE_STATES;
        }
    },

    _getSkippedStates: function() {
        var attribute = this.stateElement.getAttribute(PlannedTaskStateUtil.SKIPPED_STATES);
        this.skippedStates = [];
        if(!JSUtil.nil(attribute)){
            var states = attribute.split(";");
            this.skippedStates = states;
        }
    },
   
    _populateStateBuckets : function() {
        var state;
       
        for(var index in this.pendingStates){
            state = this.pendingStates[index];
            this.stateBuckets[state] = PlannedTaskStateUtil.PENDING_STATES;
        }
        for(var index in this.onholdStates){
            state = this.onholdStates[index];
            this.stateBuckets[state] = PlannedTaskStateUtil.ONHOLD_STATES;
        }
        for(index in this.openStates){
            state = this.openStates[index];
            this.stateBuckets[state] = PlannedTaskStateUtil.OPEN_STATES;
        }
        for(index in this.workInProgressStates){
            state = this.workInProgressStates[index];
            this.stateBuckets[state] = PlannedTaskStateUtil.WORK_IN_PROGRESS;
        }
        for(index in this.closeStates){
            state = this.closeStates[index];
            this.stateBuckets[state] = PlannedTaskStateUtil.CLOSE_STATES;
        }
    },
   
    getBucketForState : function(state) {
        if(this.stateBuckets.hasOwnProperty(state)){
            return this.stateBuckets[state];
        }
        return null;
    },
   
    isValidState : function(bucketName, state) {
        if(this.stateBuckets.hasOwnProperty(state)){
            var cl = GlideSysChoice(this.task.getTableName(),'state');
            var stateChoices = cl.getChoices();
            var choice;
            while(stateChoices.next()){
                choice = stateChoices.getValue('value');
                if(choice == state){
                    return (this.stateBuckets[state] == bucketName) ? true : false;
                }
            }
        }
        return false;
    },
   
    getDefaultStateForBucket : function(bucketName){
        if(bucketName == PlannedTaskStateUtil.PENDING_STATES)
            return this.getDefaultPendingState();
        else if(bucketName == PlannedTaskStateUtil.OPEN_STATES)
            return this.getDefaultOpenState();
        else if(bucketName == PlannedTaskStateUtil.WORK_IN_PROGRESS)
            return this.getDefaultWorkState();
        else if(bucketName == PlannedTaskStateUtil.CLOSE_STATES)
            return this.getDefaultCloseState();
        else if(bucketName == PlannedTaskStateUtil.ONHOLD_STATES)
            return this.getDefaultOnholdState();
        else
            return null;
    },
   
    getValidStateForBucket : function(bucket, state) {
        if (this.isValidState(bucket, state)) {
            return state;
        } else {
            var defaultState = this.getDefaultStateForBucket(bucket);
            return defaultState;
        }
    },
   
    getStateBuckets: function() {
        return this.stateBuckets;
    },
   
    type : "PlannedTaskStateUtil"
};
1 REPLY 1

Matthew_13
Kilo Sage

This behavior is actually by design in the out-of-box PlannedTaskStateUtil. As soon as any child task is closed, the utility no longer treats the parent as “all tasks on hold” and defaults the parent to Work In Progress, even if all active tasks are on hold.

There’s also a small issue in your script that will prevent this from working correctly:

this.OnholdStates = states;

Everywhere else you reference:

this.onholdStates

Because JavaScript is case-sensitive, your on-hold states aren’t being picked up correctly.

That said, even fixiing this won’t fully meet your requirement. The OOB util does not ignor closed tasks when calculating the parent state.

Recommended solution

Do not modify PlannedTaskStateUtil. Instead, add a Business Rule on the parent task that:

  • Ignores closed tasks

  • Sets the parent to On Hold when all active tasks are on hold

  • Sets the parent to Closed Complete only when all tasks are closed

This approach is upgrade-safe and gives you full control over the logic. 

Please thumbs up and  accept answer if you find helpful.