Files
EasySoft-ZenTaoPMS/module/programplan/tao.php
T
2023-11-07 14:48:28 +08:00

610 lines
25 KiB
PHP

<?php
declare(strict_types=1);
/**
* The tao file of programplan module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author lanzongjun <lanzongjun@easycorp.ltd>
* @link https://www.zentao.net
*/
class programplanTao extends programplanModel
{
/**
* 更新项目阶段。
* update program plan.
*
* @param object $plan
* @param array $conditions
* @access protected
* @return bool
*/
protected function updateRow(object $plan, object $oldPlan, object|bool $parentStage): bool
{
$requiredFields = $this->config->programplan->edit->requiredFields ?? '';
$getname = '';
if($plan->relatedExecutionsID && $oldPlan->project && $parentStage && $oldPlan->parent) $getname = true;
$relatedExecutionsID = $plan->relatedExecutionsID;
$setCode = $plan->setCode;
unset($plan->relatedExecutionsID, $plan->setCode);
$this->dao->update(TABLE_PROJECT)->data($plan)
->autoCheck()
->batchCheckIF($requiredFields, $requiredFields, 'notempty')
->checkIF($plan->end != '0000-00-00', 'end', 'ge', $plan->begin)
->checkIF(!empty($plan->percent), 'percent', 'float')
->checkIF(!empty($plan->name) && $getname, 'name', 'unique', "id in ({$relatedExecutionsID}) and type in ('sprint','stage') and `project` = {$oldPlan->project} and `deleted` = '0' and `parent` = {$oldPlan->parent}")
->checkIF(!empty($plan->code) && $setCode, 'code', 'unique', "id != {$plan->id} and type in ('sprint','stage','kanban') and `deleted` = '0'")
->where('id')->eq($plan->id)
->exec();
return !dao::isError();
}
/**
* 获取父阶段键值对。
* Get parent stage list.
*
* @param int $executionID
* @param int $planID
* @param int $productID
* @access protected
* @return array|false
*/
protected function getParentStages(int $executionID, int $planID, int $productID): array|false
{
$parentStage = $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT)
->alias('t1')->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t1.product')->eq($productID)
->andWhere('t2.project')->eq($executionID)
->andWhere('t2.type')->eq('stage')
->andWhere('t2.deleted')->eq(0)
->andWhere('t2.path')->notlike("%,$planID,%")
->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
->orderBy('t2.id desc')
->fetchPairs();
if(dao::isError()) return false;
return $parentStage;
}
/**
* 根据action获取excution。
* Get new parent and action.
*
* @param array $statusCount
* @param object $parent
* @param int $startTasks
* @param string $action
* @access protected
* @return array
*/
protected function getNewParentAndAction(array $statusCount, object $parent, int $startTasks, string $action): array
{
$count = count($statusCount);
$newParent = null;
$parentAction = '';
$this->loadModel('execution');
if(isset($statusCount['wait']) && $count == 1 && helper::isZeroDate($parent->realBegan) && $startTasks == 0)
{
if($parent->status != 'wait')
{
$newParent = $this->execution->buildExecutionByStatus('wait');
$parentAction = 'waitbychild';
}
}
elseif(isset($statusCount['closed']) && $count == 1)
{
if($parent->status != 'closed')
{
$newParent = $this->execution->buildExecutionByStatus('closed');
$parentAction = 'closedbychild';
}
}
elseif(isset($statusCount['suspended']) && ($count == 1 || (isset($statusCount['closed']) && $count == 2)))
{
if($parent->status != 'suspended')
{
$newParent = $this->execution->buildExecutionByStatus('suspended');
$parentAction = 'suspendedbychild';
}
}
else
{
if($parent->status != 'doing')
{
$newParent = $this->execution->buildExecutionByStatus('doing');
$parentAction = $parent->status == 'wait' ? 'startbychildstart' : 'startbychild' . $action;
}
}
return array('newParent' => $newParent, 'parentAction' => $parentAction);
}
/**
* 校验提交数据是否必须。
* Check required items.
*
* @param object $oldPlan
* @param object $plan
* @param int $projectID
* @access protected
* @return bool
*/
protected function checkRequiredItems(object $oldPlan, object $plan, int $projectID): bool
{
/* 校验开始结束时间是否正确。 */
/* check begin and end date. */
if($plan->begin == '0000-00-00') dao::$errors['begin'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->begin);
if($plan->end == '0000-00-00') dao::$errors['end'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->end);
if(dao::isError()) return false;
if($plan->parent) $parentStage = $this->getByID($plan->parent);
if(isset($parentStage) && $plan->begin < $parentStage->begin)
{
dao::$errors['begin'] = sprintf($this->lang->programplan->error->letterParent, $parentStage->begin);
return false;
}
if(isset($parentStage) && $plan->end > $parentStage->end)
{
dao::$errors['end'] = sprintf($this->lang->programplan->error->greaterParent, $parentStage->end);
return false;
}
if($projectID) $this->loadModel('execution')->checkBeginAndEndDate($projectID, $plan->begin, $plan->end);
if(dao::isError()) return false;
$setCode = isset($this->config->setCode) && $this->config->setCode == 1;
if($setCode && empty($plan->code))
{
dao::$errors['code'][] = sprintf($this->lang->error->notempty, $this->lang->execution->code);
return false;
}
/* 对于是否有子阶段进行判断处理。 */
/* check parent stage. */
$setPercent = isset($this->config->setPercent) && $this->config->setPercent == 1;
if($plan->parent > 0)
{
$plan->attribute = $parentStage->attribute == 'mix' ? $plan->attribute : $parentStage->attribute;
$plan->acl = $parentStage->acl;
if($setPercent)
{
$childrenTotalPercent = $this->getTotalPercent($parentStage, true);
$childrenTotalPercent = $plan->parent == $oldPlan->parent ? ($childrenTotalPercent - $oldPlan->percent + $plan->percent) : ($childrenTotalPercent + $plan->percent);
if($childrenTotalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver;
}
/* 如果子阶段有里程碑,那么父阶段的更新为0。 */
/* If child plan has milestone, update parent plan set milestone eq 0 . */
if($plan->milestone && $parentStage->milestone) $this->dao->update(TABLE_PROJECT)->set('milestone')->eq(0)->where('id')->eq($oldPlan->parent)->exec();
}
else
{
/* Synchronously update sub-phase permissions. */
$childrenIDList = $this->dao->select('id')->from(TABLE_PROJECT)->where('parent')->eq($oldPlan->id)->fetchAll('id');
if(!empty($childrenIDList)) $this->dao->update(TABLE_PROJECT)->set('acl')->eq($plan->acl)->where('id')->in(array_keys($childrenIDList))->exec();
/* 相同父阶段的子阶段工作量占比之和不超过100%。 */
/* The workload of the parent plan cannot exceed 100%. */
$oldPlan->parent = $plan->parent;
if($setPercent)
{
$totalPercent = $this->getTotalPercent($oldPlan);
$totalPercent = $totalPercent + $plan->percent;
if($totalPercent > 100) return dao::$errors['percent'][] = $this->lang->programplan->error->percentOver;
}
}
return true;
}
/**
* 组装里程碑数据。
* Format milestones use '/'.
*
* @param array $milestones
* @param int $projectID
* @access protected
* @return array
*/
protected function formatMilestones(array $milestones, int $projectID): array
{
$allStages = $this->dao->select('id,name')->from(TABLE_EXECUTION)
->where('project')->eq($projectID)
->andWhere('type')->notin('program,project')
->fetchPairs();
foreach($milestones as $id => $path)
{
$paths = explode(',', trim($path, ','));
$stageName = '';
foreach($paths as $stage)
{
if(isset($allStages[$stage])) $stageName .= '/' . $allStages[$stage];
}
$milestones[$id] = trim($stageName, '/');
}
return $milestones;
}
/** 设置阶段基线数据。
* Set stage baseline data.
*
* @param array $oldPlans
* @param array $plans
* @access protected
* @return array
*/
protected function setStageBaseline(array $oldPlans, array $plans): array
{
foreach($oldPlans as $id => $oldPlan)
{
if(!isset($plans[$id])) continue;
$plans[$id]->version = $oldPlan->version;
$plans[$id]->name = $oldPlan->name;
$plans[$id]->milestone = $oldPlan->milestone;
$plans[$id]->begin = $oldPlan->begin;
$plans[$id]->end = $oldPlan->end;
}
return $plans;
}
/**
* 设置任务基线数据。
* Set task baseline data.
*
* @param array $oldTasks
* @param array $tasks
* @access protected
* @return array
*/
protected function setTaskBaseline(array $oldTasks, array $tasks): array
{
foreach($oldTasks as $id => $oldTask)
{
if(!isset($tasks[$id])) continue;
$tasks[$id]->version = $oldTask->version;
$tasks[$id]->name = $oldTask->name;
$tasks[$id]->estStarted = $oldTask->estStarted;
$tasks[$id]->deadline = $oldTask->deadline;
}
return $tasks;
}
/**
* 设置甘特图阶段数据。
* Set the Gantt chart stage data.
*
* @param array $plans
* @param array $datas
* @param array $stageIndex
* @param array $planIdList
* @access protected
* @return void
*/
protected function setPlan(array $plans, array &$datas, array &$stageIndex, array &$planIdList): void
{
$this->app->loadLang('stage');
$today = helper::today();
$isMilestone = "<icon class='icon icon-flag icon-sm red'></icon> ";
foreach($plans as $plan)
{
$plan->isParent = false;
if(isset($plans[$plan->parent])) $plans[$plan->parent]->isParent = true;
}
foreach($plans as $plan)
{
$planIdList[$plan->id] = $plan->id;
$start = helper::isZeroDate($plan->begin) ? '' : $plan->begin;
$end = helper::isZeroDate($plan->end) ? '' : $plan->end;
$realBegan = helper::isZeroDate($plan->realBegan) ? '' : $plan->realBegan;
$realEnd = helper::isZeroDate($plan->realEnd) ? '' : $plan->realEnd;
$data = new stdclass();
$data->id = $plan->id;
$data->type = 'plan';
$data->text = empty($plan->milestone) ? $plan->name : $plan->name . $isMilestone ;
$data->name = $plan->name;
if(isset($this->config->setPercent) and $this->config->setPercent == 1) $data->percent = $plan->percent;
$data->attribute = zget($this->lang->stage->typeList, $plan->attribute);
$data->milestone = zget($this->lang->programplan->milestoneList, $plan->milestone);
$data->owner_id = $plan->PM;
$data->status = $this->processStatus('execution', $plan);
$data->begin = $start;
$data->deadline = $end;
$data->realBegan = $realBegan ? substr($realBegan, 0, 10) : '';
$data->realEnd = $realEnd ? substr($realEnd, 0, 10) : '';
$data->parent = $plan->grade == 1 ? 0 :$plan->parent;
$data->isParent = $plan->isParent;
$data->open = true;
$data->start_date = $start;
$data->endDate = $end;
$data->duration = 1;
$data->color = $this->lang->execution->gantt->stage->color;
$data->progressColor = $this->lang->execution->gantt->stage->progressColor;
$data->textColor = $this->lang->execution->gantt->stage->textColor;
$data->bar_height = $this->lang->execution->gantt->bar_height;
/* Determines if the object is delay. */
$data->delay = $this->lang->programplan->delayList[0];
$data->delayDays = 0;
if($today > $end)
{
$data->delay = $this->lang->programplan->delayList[1];
$data->delayDays = helper::diffDate($today, substr($end, 0, 10));
}
if($data->endDate > $data->start_date) $data->duration = helper::diffDate(substr($data->endDate, 0, 10), substr($data->start_date, 0, 10)) + 1;
if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
if($data->start_date == '' or $data->endDate == '') $data->duration = 1;
$datas['data'][$plan->id] = $data;
$stageIndex[$plan->id] = array('planID' => $plan->id, 'parent' => $plan->parent, 'progress' => array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalReal' => 0));
}
}
/**
* 设置甘特图任务数据。
* Set the Gantt chart task data.
*
* @param array $tasks
* @param array $plans
* @param string $selectCustom
* @param array $datas
* @param array $stageIndex
* @access protected
* @return void
*/
protected function setTask(array $tasks, array $plans, string $selectCustom, array &$datas, array &$stageIndex): void
{
$taskPri = "<span class='label-pri label-pri-%s' title='%s'>%s</span> ";
$today = helper::today();
$taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account');
$users = $this->loadModel('user')->getPairs('noletter');
foreach($tasks as $task)
{
$execution = zget($plans, $task->execution, array());
$priIcon = sprintf($taskPri, $task->pri, $task->pri, $task->pri);
$estStart = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted;
$estEnd = helper::isZeroDate($task->deadline) ? '' : $task->deadline;
$realBegan = helper::isZeroDate($task->realStarted) ? '' : $task->realStarted;
$realEnd = (in_array($task->status, array('done', 'closed')) and !helper::isZeroDate($task->finishedDate)) ? $task->finishedDate : '';
$start = $realBegan ? $realBegan : $estStart;
$end = $realEnd ? $realEnd : $estEnd;
if(empty($start) and $execution) $start = $execution->begin;
if(empty($end) and $execution) $end = $execution->end;
if($start > $end) $end = $start;
$data = new stdclass();
$data->id = $task->execution . '-' . $task->id;
$data->type = 'task';
$data->text = $priIcon . $task->name;
$data->percent = '';
$data->status = $this->processStatus('task', $task);
$data->owner_id = $task->assignedTo;
$data->attribute = '';
$data->milestone = '';
$data->begin = $start;
$data->deadline = $end;
$data->realBegan = $realBegan ? substr($realBegan, 0, 10) : '';
$data->realEnd = $realEnd ? substr($realEnd, 0, 10) : '';
$data->pri = $task->pri;
$data->parent = $task->parent > 0 ? $task->execution . '-' . $task->parent : $task->execution;
$data->open = true;
$progress = $task->consumed ? round($task->consumed / ($task->left + $task->consumed), 3) : 0;
$data->progress = $progress;
$data->taskProgress = ($progress * 100) . '%';
$data->start_date = $start;
$data->endDate = $end;
$data->duration = 1;
$data->estimate = $task->estimate;
$data->consumed = $task->consumed;
$data->color = zget($this->lang->execution->gantt->color, $task->pri, $this->lang->execution->gantt->defaultColor);
$data->progressColor = zget($this->lang->execution->gantt->progressColor, $task->pri, $this->lang->execution->gantt->defaultProgressColor);
$data->textColor = zget($this->lang->execution->gantt->textColor, $task->pri, $this->lang->execution->gantt->defaultTextColor);
$data->bar_height = $this->lang->execution->gantt->bar_height;
/* Determines if the object is delay. */
$data->delay = $this->lang->programplan->delayList[0];
$data->delayDays = 0;
if($today > $end)
{
$data->delay = $this->lang->programplan->delayList[1];
$data->delayDays = helper::diffDate($today, substr($end, 0, 10));
}
/* If multi task then show the teams. */
if($task->mode == 'multi' and !empty($taskTeams[$task->id]))
{
$teams = array_keys($taskTeams[$task->id]);
$assigneds = array();
foreach($teams as $assignedTo) $assigneds[] = zget($users, $assignedTo);
$data->owner_id = implode(',', $assigneds);
}
if($data->endDate > $data->start_date) $data->duration = helper::diffDate(substr($data->endDate, 0, 10), substr($data->start_date, 0, 10)) + 1;
if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
if($data->start_date == '' or $data->endDate == '') $data->duration = 1;
if(strpos($selectCustom, 'task') !== false) $datas['data'][$data->id] = $data;
foreach($stageIndex as $index => $stage)
{
if($stage['planID'] == $task->execution)
{
$stageIndex[$index]['progress']['totalEstimate'] += $task->estimate;
$stageIndex[$index]['progress']['totalConsumed'] += $task->parent == '-1' ? 0 : $task->consumed;
$stageIndex[$index]['progress']['totalReal'] += ($task->left + $task->consumed);
$parent = $stage['parent'];
if(isset($stageIndex[$parent]))
{
$stageIndex[$parent]['progress']['totalEstimate'] += $task->estimate;
$stageIndex[$parent]['progress']['totalConsumed'] += $task->parent == '-1' ? 0 : $task->consumed;
$stageIndex[$parent]['progress']['totalReal'] += ($task->left + $task->consumed);
}
}
}
}
}
/**
* 设置关联任务数据。
* Set relation task data.
*
* @param array $planIdList
* @param array $datas
* @access protected
* @return array
*/
protected function setRelationTask(array $planIdList, array $datas): array
{
$datas['links'] = array();
if($this->config->edition == 'open') return $datas;
$relations = $this->dao->select('*')->from(TABLE_RELATIONOFTASKS)->where('execution')->in($planIdList)->orderBy('task,pretask')->fetchAll();
foreach($relations as $relation)
{
$link['source'] = $relation->execution . '-' . $relation->pretask;
$link['target'] = $relation->execution . '-' . $relation->task;
$link['type'] = $this->config->execution->gantt->linkType[$relation->condition][$relation->action];
$datas['links'][] = $link;
}
return $datas;
}
/**
* 获取阶段信息。
* Get stage list.
*
* @param int $executionID
* @param int $productID
* @param string $browseType
* @param string $orderBy
* @access protected
* @return array
*/
protected function getStageList(int $executionID, int $productID, string $browseType, $orderBy = 'id_asc')
{
if(empty($executionID)) return array();
$projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch('model');
return $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1')
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project')
->where('1 = 1')
->beginIF(!in_array($projectModel, array('waterfallplus', 'ipd')))->andWhere('t1.type')->eq('stage')->fi()
->beginIF($productID)->andWhere('t2.product')->eq($productID)->fi()
->beginIF($browseType == 'all')->andWhere('t1.project')->eq($executionID)->fi()
->beginIF($browseType == 'parent')->andWhere('t1.parent')->eq($executionID)->fi()
->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
->andWhere('t1.deleted')->eq('0')
->orderBy($orderBy)
->fetchAll('id');
}
/**
* 获取子阶段数量。
* Get stage count.
*
* @param int $planID
* @param string $mode
* @access protected
* @return int
*/
protected function getStageCount(int $planID, string $mode = ''): int
{
return $this->dao->select('COUNT(*) AS count')->from(TABLE_PROJECT)
->where('parent')->eq($planID)
->beginIF($mode == 'milestone')->andWhere('milestone')->eq(1)->fi()
->andWhere('deleted')->eq(0)
->fetch('count');
}
/**
* 计算阶段在数据库中的位置序号。
* Compute the new order for programplan. if orders is not set then begin with the order in zt_project table.
*
* @param array $orders
* @param array $plans
* @access protected
* @return array
*/
protected function computeOrders(array $orders, array $plans):array
{
if(!isset($orders)) $orders = array();
asort($orders);
if(count($orders) < count($plans))
{
$orderIndex = empty($orders) ? 0 : count($orders);
$lastID = $this->dao->select('id')->from(TABLE_EXECUTION)->orderBy('id_desc')->fetch('id');
for($i = $orderIndex; $i < count($plans); $i ++)
{
$lastID ++;
$orders[$i] = $lastID * 5;
}
}
return $orders;
}
/**
* 插入项目细节。
* Insert project spec.
* @param int $planID
* @param object $plan
* @access protected
* @return bool
*/
protected function insertProjectSpec(int $planID, object $plan): bool
{
$spec = new stdclass();
$spec->project = $planID;
$spec->version = $plan->version;
$spec->name = $plan->name;
$spec->milestone = $plan->milestone;
$spec->begin = $plan->begin;
$spec->end = $plan->end;
$this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec();
return !dao::isError();
}
/**
* 获取阶段百分比。
* Get total percent.
*
* @param object $stage
* @param bool $parent
* @access public
* @return int|float
*/
protected function getTotalPercent(object $stage, bool $parent = false): int|float
{
/* When parent is equal to true, query the total workload of the subphase. */
$executionID = $parent ? $stage->id : $stage->project;
$plans = $this->getStageList((int)$executionID, (int)$stage->product, 'parent');
$totalPercent = 0;
$stageID = $stage->id;
foreach($plans as $id => $stage)
{
if($id == $stageID) continue;
$totalPercent += $stage->percent;
}
return $totalPercent;
}
}