Script Actionでサブタスクの完了率からタスクの進捗率を出してみました

やりたいこと
サブタスクの完了状態からタスクの進捗率を出したいって話、時々聞きませんか?
通常のルール機能だと自タスクの状態しか見れないので対応が難しいです。

考えた方法
Script Actionを利用して進捗率を計算できないか考えてみました。

  • 進捗率の計算方法

    • タスクの進捗率は0 - 100ルールを採用する(つまりタスクの完了まで進捗率は0%)
    • ただし、サブタスクが存在する時は、以下のルールで進捗率を計算する
      • タスク完了時:100%
      • タスク未完了時:以下のいずれかの方法で計算する
        • 完了率で計算する:サブタスク完了数 ÷ 総サブタスク数
        • 完了した予定時間で計算する:完了したサブタスクの予定時間 ÷ サブタスクの総予定時間
  • 進捗率計算処理の利用方法

    • ScriptActionによるルールを進捗率自動計算プロジェクト内に作成する
    • サブタスクの完了率で計算したいタスクは進捗率自動計算プロジェクトにマルチホームして利用することとする
      • なお、セクションを利用して進捗率の計算方法の変更を可能な仕組みにします
  • 利用したカスタムフィールド

    • 進捗率(%):数値フィールド
    • 予定時間:Asanaが作成する予定時間を利用
    • 進捗率更新フラグ:更新依頼中、更新依頼済のいずれか
    • 進捗率更新方法:完了済タスク数、完了済タスク総予定時間のいずれか
      • なお、セクションと進捗率更新方法カスタムフィールドの値が連動するルールを準備しておきます
  • ルールの実行タイミング

    • 手動ルール「進捗率再計算実行」を実行した時
    • 進捗率更新フラグが「更新依頼中」になった時

    なお、進捗率更新フラグを「更新依頼中」に変更するルールも準備し、以下のタイミングで実行します

    • セクションにタスクが移動したとき
    • 進捗率更新方法が変更されたとき
    • タスクの完了状態が変更されたとき(このルールだけ、タスク限定で実行)
  • 制限

    • サブタスクを追加した時、削除した時は進捗率を計算できないので、手動ルールで再計算する必要があります。
    • タスクとサブタスクまで計算の対象です。サブサブタスク以降は計算対象外です。
  • 作成したコード
    以下のコードを作成しました。
    解説しようかと思ったのですが、ボリュームがあるのでコードのみ載せます。


 //定数宣言部
const CF_GID_PROGRESS_RATE = "<gid値を設定>";                       //進捗率(%)カスタムフィールドGID
const CF_GID_ESTIMATE_TIME = "<gid値を設定>";                        //予定時間カスタムフィールドGID
const CF_GID_AUTO_UPD_FLG = "<gid値を設定>";                         //進捗率自動更新フラグカスタムフィールドGID
const CF_GID_AUTO_UPD_METHOD = "<gid値を設定>";                      //進捗率自動更新方法カスタムフィールドGID
const CF_GID_AUTO_UPD_METHOD_COMP_TASK_COUNT = "<gid値を設定>";      // 完了済タスク数
const CF_GID_AUTO_UPD_METHOD_COMP_MINUTES = "<gid値を設定>";         // 完了済タスク総予定時間
const CF_GID_UPDATE_FLG = "<gid値を設定";                            //進捗率更新フラグGID
const CF_GID_UPDATE_FLG_UPDATING = "<gid値を設定>";                  //進捗率更新フラグ:更新中
const CF_GID_UPDATE_FLG_UPDATED = "<gid値を設定>";                   //進捗率更新フラグ:更新済
const DEFAULT_ESTIMATE_TIME = 480;                                   // デフォルトの予定時間(480分 = 8時間)
const MAX_CONCURRENT_LIMIT = 6;                                      // 並列数制限(更新系は15なので、余裕みて6に制限)
const RESPONSE_ITEM_LIMIT = 100;
const WAIT_LIMIT_MS = 1000;                                          //API実行で待つ時間

/***
 * 予定時間を修正する処理
 * 引数:orgEstimaetTimeCF  元の予定時間のフィールド
 *      (orgEstimaetTimeCF.number_valueが元の予定時間(分))
 * 返値:予定時間(分)
 */
function fixEstimateTime(orgEstimaetTimeCF){  
    let curEstimateTime = 0;    //予定時数カスタムフィールドがないときは、デフォルト値8時間(480分)とする    
    if(!orgEstimaetTimeCF){
        curEstimateTime = DEFAULT_ESTIMATE_TIME;    
    }else{
        curEstimateTime = orgEstimaetTimeCF.number_value;         //未設定時はデフォルト値
        if(curEstimateTime === undefined || curEstimateTime === null){
            curEstimateTime = DEFAULT_ESTIMATE_TIME;      
        }else if(curEstimateTime < 0){//0未満の場合は予定時間0に設定
                curEstimateTime = 0;      
        }    
    }   
    return curEstimateTime;
}


/**
 * サブタスクの進捗更新関数(非同期)
 * 引数:sTask タスクの情報 
 * 返値:タスク更新処理またはnull 
 */
function updateSubtaskProgressAsync(sTask) {
  return updateSubtaskProgressAndEstimateTimeBaseAsync(sTask,true,false);
}


/**
 * サブタスクの進捗と予定時間更新関数(非同期) 
 * 引数:sTask タスクの情報 
 * 返値:タスク更新処理またはnull 
 */
function updateSubtaskProgressAndEstimateTimeAsync(sTask) {  
    return updateSubtaskProgressAndEstimateTimeBaseAsync(sTask,true,true);
}
/**
 * サブタスクの進捗と予定時間更新関数(非同期) 
 * 引数:sTask タスクの情報
 *       checkProgress 進捗修正のチェックをするかどうか 
 *       checkEstimateTime  予定時間修正のチェックをするかどうか 
 * 返値:タスク更新処理またはnull */
function updateSubtaskProgressAndEstimateTimeBaseAsync(sTask,checkProgress,checkEstimateTime) {  
    //更新の必要性フラグ  
    let req_fix = false;    
    //進捗率のみ修正  
    let updateTaskBody = {data: {custom_fields: {}}};
    
    //進捗率を修正時  
    if(checkProgress){    
        //要求される進捗率を取得    
        const reqProgressRate = sTask.completed ? 100 : 0;    
        
        //現在の進捗率を取得    
        let progressRate = sTask.custom_fields.find((item) => item.gid === CF_GID_PROGRESS_RATE);    
        
        // 進捗率(%)カスタムフィールドが存在し、要求値と違うときは更新値に要求値を設定する    
        // ただし未完了時に補正しない場合は無視する    
        if(progressRate !== undefined && 
            progressRate.number_value !== reqProgressRate){      
            
            //完了時、または進捗率未設定時のみ更新する      
            if( sTask.completed ||            
                progressRate === null ||         
                progressRate.number_value === undefined  ||         
                progressRate.number_value === null){              
                
                updateTaskBody.data.custom_fields[CF_GID_PROGRESS_RATE] = reqProgressRate;        
                req_fix = true;      
            }    
        }      
    }  
    //予定時間を修正時  
    if(checkEstimateTime){    
        //現在の進捗率を取得    
        let estimateTime = sTask.custom_fields.find((item) => item.gid === CF_GID_ESTIMATE_TIME);        
        
        //予定時間カスタムフィールドが存在するが未設定の場合はデフォルト値を設定する    
        if(estimateTime !== undefined && (estimateTime.number_value === undefined || estimateTime.number_value === null )){      
            updateTaskBody.data.custom_fields[CF_GID_ESTIMATE_TIME] = DEFAULT_ESTIMATE_TIME;      
            req_fix = true;    
        }  
    }  
    
    //更新が必要なとき  
    if(req_fix){    
        const updTaskOpts = {      
            'opt_fields': "completed,custom_fields.number_value,custom_fields.enum_value,resource_subtype"      
            };      
        //タスク更新処理のみ追加    
        return tasksApiInstance.updateTask(updateTaskBody, sTask.gid,updTaskOpts);      
    }else{    
        return null;  
    }
}

/** 
 * タスクの進捗率と進捗フラグ更新処理 
 * 引数:progress 設定する進捗値 
 * 返値:タスク 
 */
function updateTaskProgressAndUpdateFlgAsync(progress) {  
    const updTaskOpts = {    
            'opt_fields': "completed,custom_fields.number_value,custom_fields.enum_value,resource_subtype"      
        };  
        
    //進捗率のみ修正  
    let updateTaskBody = {"data": {"custom_fields": { }}};  
    updateTaskBody.data.custom_fields[CF_GID_PROGRESS_RATE] = progress;  
    updateTaskBody.data.custom_fields[CF_GID_UPDATE_FLG] = CF_GID_UPDATE_FLG_UPDATED;  
    //タスク更新処理のみ追加  
    return tasksApiInstance.updateTask(updateTaskBody, task_gid,updTaskOpts);
}
/**
 * タスクの進捗フラグ更新処理 
 * 返値:タスク 
 */
function updateUpdateFlgAsync() {  
     const updTaskOpts = {    
         'opt_fields': "completed,custom_fields.number_value,custom_fields.enum_value,resource_subtype"      
     };  
     
     //進捗率のみ修正  
     let updateTaskBody = {"data": {"custom_fields": { }}};  
     updateTaskBody.data.custom_fields[CF_GID_UPDATE_FLG] = CF_GID_UPDATE_FLG_UPDATED;  
     
     //タスク更新処理のみ追加  
     return tasksApiInstance.updateTask(updateTaskBody, task_gid,updTaskOpts);
}

/** 
 * サブタスク完了による進捗率加算関数
 * サブタスク完了時は100、未完了時は0を加算する 
 * 引数:acc 加算前の進捗値 
 *      curValue 現在のサブタスクの値 
 * 返値:進捗値 
 */
function addProgressByTaskComplete(acc, curValue) {  
    //タスク完了時は進捗率100加算  
    if(curValue.completed){    
        return acc + 100;  
        
    //タスク未完了時は加算なし  
    }else{    
        return acc;  
    }
}

/** 
 * タスクの進捗率加算関数 
 * サブタスク完了時は100×予定時間、未完了時は0を加算する 
 * 引数:acc 加算前の進捗値 
 *      curValue 現在のサブタスクの値 
 * 返値:進捗値 
 */
function addProgressByEstimateTime(acc, curValue) {  
    let curEstimateTime = 0;  
    let curProgresTime = 0;  
    
    //タスク完了時のみ予定時間×100を加算  
    if(curValue.completed){    
        let estimateTime = curValue.custom_fields.find((item) => {return item["gid"] === CF_GID_ESTIMATE_TIME;});
            
        //予定時間×100を加算    
        curProgresTime =  100 * fixEstimateTime(estimateTime);  
    }  
    log("curProgresTime:" + curProgresTime);  
    return acc + curProgresTime;
}

/** 
 * タスクの進捗率加算関数 
 * サブタスク完了時は100×予定時間、未完了時は進捗率×予定時間を加算する 
 * 引数:acc 加算前の進捗値 *      curValue 現在のサブタスクの値 
 * 返値:進捗値 */
function addProgressByEstimateTimeAndProgressRate(acc, curValue) {  
    let curProgresRate = 0;  
    let curProgresTime = 0;  
    let estimateTime = curValue.custom_fields.find((item) => {return item["gid"] === CF_GID_ESTIMATE_TIME;});
    //タスク完了時は予定時間×100を加算  
    if(curValue.completed){    
        curProgresTime =  100 * fixEstimateTime(estimateTime);  
      
    //タスク未完了時は進捗率×予定時間×100を加算  
    }else{    
        let progressRate = curValue.custom_fields.find((item) => {return item["gid"] === CF_GID_PROGRESS_RATE});    
      
        //進捗率(%)カスタムフィールドがあるときはその値を利用する    
        //進捗率(%)カスタムフィールドがないときは無視する    
        if(progressRate !== undefined && progressRate !== null){      
            //サブタスクの進捗率を取得      
            curProgressRate = progressRate.number_value;      
            //未定義やnull、0未満は0に補正      
            if(curProgressRate === undefined || curProgressRate === null || curProgressRate < 0){        
                curProgressRate  = 0;      
            }else if (curProgressRate > 100){ 
                curProgressRate  = 100;      
            }      
          //進捗率×予定時間で計算する      
          curProgresTime =  curProgressRate * fixEstimateTime(estimateTime);    
      }  
  }    
  log("curProgresTime:" + curProgresTime);  
  return acc + curProgresTime;
}



/*** 
 * すべてのタスクの進捗更新処理
 * 引数:task タスク情報
 *       calMaxProgressFunc 進捗率最大値計算関数
 *       calProgressFunc 進捗率計算関数
 *       updateSubtaskProgressFunc サブタスク進捗率更新関数
 */
async function updateAllTaskProgressAsync(task,calMaxProgressFunc,calProgressFunc,updateSubtaskProgressFunc){  
    //更新関数の取得  
    let updateFuncs = [];  
    let maxProgress  = 0;  
    let totalProgress = 0;  
    let results = [];      
    //タスク完了時は進捗率100%  
    if(task.data.completed !== undefined && task.data.completed){    
        totalProgress = 100;  
    }  
    //サブタスク取得用パラメータ  
    let subtasksOpts = {    
        'limit': RESPONSE_ITEM_LIMIT,              
        'offset' : undefined,              
        'opt_fields': "completed,custom_fields.number_value,resource_subtype"  
    };
    
    //サブタスクを取得する  
    let subTasks = await tasksApiInstance.getSubtasksForTask(task_gid, subtasksOpts);    
    //サブタスクがある場合の処理  
    if(subTasks.data.length > 0){    
        do{      
            //タスク未完了時は進捗率計算を実行      
            if(task.data.completed === undefined || task.data.completed === false){        
                if(calProgressFunc !== null ){          
                    if(calMaxProgressFunc !== null ){            
                        //最大進捗を計算する            
                        maxProgress = subTasks.data.reduce(calMaxProgressFunc,maxProgress);          
                    }else{            
                        //サブタスク数を追加            
                        maxProgress += subTasks.data.length * 100;          
                    }
                              
                    //進捗率を加算          
                    totalProgress = subTasks.data.reduce(calProgressFunc,totalProgress);        
                }      
            }//タスク未完了時の処理完了      

            //更新関数の配列を取得        
            let curUpdateFuncs = subTasks.data.          
                  map(updateSubtaskProgressFunc).          
                  filter(item => item !== null);        
                  
            //更新関数配列にマージ        
            updateFuncs.push(...curUpdateFuncs);      
               
            //次ページのデータを取得    
            subTasks = await subTasks.nextPage();    
        }while(subTasks.data !== null);  
    }     
    
    //進捗計算関数設定時  
    if(calProgressFunc !== null){    
        //タスク未完了時は進捗率計算を実行    
        if(task.data.completed === undefined || task.data.completed === false){      
            log("maxProgress = "  + maxProgress );      
            log("totalProgress = " + totalProgress );      
            
            //進捗率を補正      
            if(maxProgress > 0){        
                //進捗率を計算        
                totalProgress = totalProgress / maxProgress * 100;      
            }    
        }    
        //更新関数配列に進捗率と進捗率更新フラグ修正処理関数をマージ     
        updateFuncs.push(updateTaskProgressAndUpdateFlgAsync(totalProgress));  
    }else{    
        //更新関数配列に進捗率更新フラグ修正処理関数をマージ     4
        updateFuncs.push(updateUpdateFlgAsync());  
    }  
    
    //最大数を制限して処理  
    let curIndex = 0;  
    
    while(curIndex < updateFuncs.length){    
        //最大15制限のため一部のみ取り込み    
        let curUpdateFuncs = updateFuncs.slice(curIndex, curIndex + MAX_CONCURRENT_LIMIT);        
        //現在の    
        let curResults = Promise.all(curUpdateFuncs).then((values) => {});    
              
        //実行結果をマージ    
        results.push(curResults);    
        
        //次のデータをまとめて実行    
        curIndex += MAX_CONCURRENT_LIMIT;  
    }  
    return results;
}

/** * タスクの進捗最大値取得関数 
 * サブタスクの予定時間を加算して計算する
 * 引数:acc 加算前の進捗最大値
 *      curValue 現在のサブタスクの値 * 返値:予定時間 */
 function addMaxProgressByEstimateTime(acc, curValue) {  
     let curProgresRate = 0;  
     let curProgresTime = 0;  
     let estimateTime = curValue.custom_fields.find((item) => item.gid === CF_GID_ESTIMATE_TIME);;  
     return acc + fixEstimateTime(estimateTime) * 100;
}
 
 
 /** 
  * メイン処理(run関数) 
  */
 async function run() {  
     let maxProgress = 0;  
     let totalProgress = 0;  
     const taskOpts = {    
         'opt_fields': "parent,completed,custom_fields.number_value,custom_fields.enum_value,resource_subtype"      
     };
     const updTaskOpts = {    
         'opt_fields': "completed,custom_fields.number_value,custom_fields.enum_value,resource_subtype"      
     };   
      
     //自タスクの情報を取得  
     const task = await tasksApiInstance.getTask(task_gid,taskOpts);  
     //自タスクの情報を収集  
     const taskProgressRate = task.data.custom_fields.find((item) => item.gid === CF_GID_PROGRESS_RATE);  
     const taskAutoUpdateFlg = task.data.custom_fields.find((item) => item.gid === CF_GID_AUTO_UPD_FLG);  
     const taskAutoUpdateMethod = task.data.custom_fields.find((item) => item.gid === CF_GID_AUTO_UPD_METHOD);  
     log("task ProgressRate = " +taskProgressRate.number_value);    
     
     //タスク進捗計算関数、タスク進捗最大値計算関数、サブタスク進捗更新関数を変数に格納する  
     let calProgressFunc = addProgressByTaskComplete;  let calMaxProgressFunc = null;   
     let updateSubtaskProgressFunc = updateSubtaskProgressAsync;  
     
     //進捗率に関するカスタムフィールドがある場合のみ進捗度を更新する  
     if(taskProgressRate !== undefined &&    
         taskAutoUpdateFlg  !== undefined &&    
         taskAutoUpdateMethod  !== undefined ){    
         log("found custom fields. rate = " +  taskProgressRate.number_value);            
         //自動計算方法未設定の場合は    
         if(taskAutoUpdateMethod.enum_value === null){      
             //デフォルトのタスク進捗計算関数を設定する      
             calProgressFunc = addProgressByTaskComplete;      
             updateSubtaskProgressFunc = updateSubtaskProgressAsync; 
             log("auto update:null.");          
         }else if(taskAutoUpdateMethod.enum_value.gid === CF_GID_AUTO_UPD_METHOD_COMP_TASK_COUNT){      
         
             //完了済タスク数の場合はタスク進捗計算関数をaddProgressByTaskCompleteに設定する      
             calProgressFunc = addProgressByTaskComplete;      
             updateSubtaskProgressFunc = updateSubtaskProgressAsync;      
             log("auto update:sub task count.");          
         }else if(taskAutoUpdateMethod.enum_value.gid === CF_GID_AUTO_UPD_METHOD_COMP_MINUTES){      
             //完了済タスク総予定時間の場合はタスク進捗計算関数をaddProgressByEstimateTimeに設定する      
             calProgressFunc = addProgressByEstimateTime;      
             calMaxProgressFunc = addMaxProgressByEstimateTime;      
             updateSubtaskProgressFunc = updateSubtaskProgressAndEstimateTimeAsync;      
             log("auto update:total minutes of completed sub task.");     
         }        
         
         //更新関数をすべて取得    
         await updateAllTaskProgressAsync(task,calMaxProgressFunc,calProgressFunc,updateSubtaskProgressFunc);      
     }//進捗に関するフィールドがあるときの処理 END      

}
         
// To function properly, the script must end with returning a Promise via run().
run();
  • 結果
    • メリットがあるのかどうかはちょっとわからないのですが、運用上あまり無理のないレベルでサブタスクの完了状態から進捗率を計算する仕組みができたのではないでしょうか。
    • サブタスク追加時に進捗率が出ない等、制限があるので公式にサポートしてもらえると助かります。
3 Likes