model.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964
  1. <?php
  2. /**
  3. * The model file of programplan module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package programplan
  9. * @link https://www.zentao.net
  10. */
  11. class programplanModel extends model
  12. {
  13. /**
  14. * 根据id获取阶段。
  15. * Get plan by id.
  16. *
  17. * @param int $planID
  18. * @access public
  19. * @return object|false
  20. */
  21. public function getByID($planID)
  22. {
  23. $plan = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->eq($planID)->fetch();
  24. if(empty($plan)) return false;
  25. return $this->processPlan($plan);
  26. }
  27. /**
  28. * 获取阶段列表。
  29. * Get stages list.
  30. *
  31. * @param int $executionID
  32. * @param int $productID
  33. * @param string $browseType all|parent
  34. * @param string $orderBy
  35. * @access public
  36. * @return array
  37. */
  38. public function getStage($executionID = 0, $productID = 0, $browseType = 'all', $orderBy = 'id_asc')
  39. {
  40. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStats();
  41. $plans = $this->programplanTao->getStageList($executionID, $productID, $browseType, $orderBy);
  42. return $this->processPlans($plans);
  43. }
  44. /**
  45. * 根据id 查询项目列表。
  46. * Get project by idList.
  47. *
  48. * @param array $idList
  49. * @access public
  50. * @return array
  51. */
  52. public function getByList($idList = array())
  53. {
  54. $plans = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($idList)->andWhere('type')->eq('project')->fetchAll('id');
  55. return $this->processPlans($plans);
  56. }
  57. /**
  58. * 获取阶段列表。
  59. * Get plans.
  60. *
  61. * @param int $executionID
  62. * @param int $productID
  63. * @param string $orderBy
  64. * @access public
  65. * @return array
  66. */
  67. public function getPlans($executionID = 0, $productID = 0, $orderBy = 'id_asc')
  68. {
  69. $plans = $this->getStage($executionID, $productID, 'all', $orderBy);
  70. if(!$plans) return array();
  71. $parents = array();
  72. $children = array();
  73. foreach($plans as $planID => $plan)
  74. {
  75. if($plan->grade == 1) $parents[$planID] = $plan;
  76. if($plan->grade > 1) $children[$plan->parent][] = $plan;
  77. }
  78. foreach($parents as $planID => $plan) $parents[$planID]->children = isset($children[$planID]) ? $children[$planID] : array();
  79. return $parents;
  80. }
  81. /**
  82. * 获取项目中的阶段数据键值对。
  83. * Get stade pairs for project.
  84. *
  85. * @param int $executionID
  86. * @param int $productID
  87. * @param string $type all|leaf
  88. * @access public
  89. * @return array
  90. */
  91. public function getPairs($executionID, $productID = 0, $type = 'all')
  92. {
  93. $plans = $this->getStage($executionID, $productID, $type);
  94. $parents = array();
  95. $pairs = array();
  96. if(strpos($type, 'leaf') !== false) array_map(function($plan) use(&$parents){$parents[$plan->parent] = true;}, $plans);
  97. foreach($plans as $planID => $plan)
  98. {
  99. if(strpos($type, 'leaf') !== false and isset($parents[$plan->id])) continue;
  100. $paths = array_slice(explode(',', trim($plan->path, ',')), 1);
  101. $planName = '';
  102. foreach($paths as $path)
  103. {
  104. if(isset($plans[$path])) $planName .= '/' . $plans[$path]->name;
  105. }
  106. $pairs[$planID] = $planName;
  107. }
  108. return $pairs;
  109. }
  110. /**
  111. * 获取甘特图页面数据。
  112. * Get gantt data.
  113. *
  114. * @param int $projectID
  115. * @param int $productID
  116. * @param int $baselineID
  117. * @param string $selectCustom
  118. * @param bool $returnJson
  119. * @param string $browseType
  120. * @param int $queryID
  121. * @access public
  122. * @return string|array
  123. */
  124. public function getDataForGantt($projectID, $productID, $baselineID = 0, $selectCustom = '', $returnJson = true, $browseType = '', $queryID = 0)
  125. {
  126. $plans = $this->getStage($projectID, $productID, 'all', 'order');
  127. $project = $this->loadModel('project')->getById($projectID);
  128. /* Set plan baseline data. */
  129. if($baselineID)
  130. {
  131. $baseline = $this->loadModel('cm')->getByID($baselineID);
  132. $oldData = json_decode($baseline->data);
  133. $plans = $this->programplanTao->setPlanBaseline((array)$oldData->stage, $plans);
  134. }
  135. /* Set task baseline data. */
  136. $tasks = $this->getGanttTasks($projectID, array_keys($plans), $browseType, $queryID);
  137. if($baselineID) $this->programplanTao->setTaskBaseline(isset($oldData->task) ? $oldData->task : array(), $tasks); // Set task baseline.
  138. if($browseType == 'bysearch')
  139. {
  140. $taskExecutions = array_column($tasks, 'execution');
  141. $plans = array_filter($plans, function($plan) use($taskExecutions) {return in_array($plan->id, $taskExecutions);});
  142. }
  143. /* Set plan for gantt view. */
  144. $result = $this->programplanTao->initGanttPlans($plans, $browseType);
  145. $datas = $result['datas'];
  146. $planIdList = $result['planIdList'];
  147. $stageIndex = $result['stageIndex'];
  148. $reviewDeadline = $result['reviewDeadline'];
  149. /* Judge whether to display tasks under the stage. */
  150. if(empty($selectCustom)) $selectCustom = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=programplan&section=browse&key=stageCustom");
  151. /* Set task for gantt view. */
  152. $result = $this->programplanTao->setTask($tasks, $plans, $selectCustom, $datas, $stageIndex);
  153. $datas = $result['datas'];
  154. $stageIndex = $result['stageIndex'];
  155. /* Build data for ipd. */
  156. if($project->model == 'ipd' and $datas) $datas = $this->programplanTao->buildGanttData4IPD($datas, $projectID, $productID, $selectCustom, $reviewDeadline);
  157. /* Calculate the progress of the phase. */
  158. $datas = $this->programplanTao->setStageSummary($datas, $stageIndex);
  159. foreach($tasks as $task) $task->id = $task->execution . '-' . $task->id;
  160. /* Set relation task data. */
  161. $datas['links'] = $this->programplanTao->buildGanttLinks($projectID, $tasks);
  162. $datas['data'] = isset($datas['data']) ? array_values($datas['data']) : array();
  163. return $returnJson ? json_encode($datas) : $datas;
  164. }
  165. /**
  166. * 获取按照指派给分组甘特图相关数据。
  167. * Gets Gantt chart related data as assigned to the group.
  168. *
  169. * @param int $executionID
  170. * @param int $productID
  171. * @param int $baselineID
  172. * @param string $selectCustom
  173. * @param bool $returnJson
  174. * @param string $browseType
  175. * @param int $queryID
  176. * @access public
  177. * @return string|array
  178. */
  179. public function getDataForGanttGroupByAssignedTo($executionID, $productID, $baselineID = 0, $selectCustom = '', $returnJson = true, $browseType = '', $queryID = 0)
  180. {
  181. $datas = array();
  182. $stageIndex = array();
  183. $plans = $this->getStage($executionID, $productID);
  184. $planIdList = array_column($plans, 'id');
  185. $users = $this->loadModel('user')->getPairs('noletter');
  186. $tasks = $this->getGanttTasks($executionID, $planIdList, $browseType, $queryID);
  187. $tasksGroup = $this->programplanTao->buildTaskGroup($tasks);
  188. /* Judge whether to display tasks under the stage. */
  189. if(empty($selectCustom)) $selectCustom = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=programplan&section=browse&key=stageCustom");
  190. $begin = $end = helper::today();
  191. $deadlineList = array();
  192. foreach($tasksGroup as $group => $tasks)
  193. {
  194. foreach($tasks as $taskID => $task)
  195. {
  196. $deadline = helper::isZeroDate($task->deadline) && !empty($plans[$task->execution]->end) ? $plans[$task->execution]->end : $task->deadline;
  197. if(helper::isZeroDate($deadline)) continue;
  198. $begin = $deadline < $begin ? $deadline : $begin;
  199. $deadlineList[$taskID] = $deadline;
  200. }
  201. }
  202. $groupID = 0;
  203. $datas['data'] = array();
  204. $workingDays = $this->loadModel('holiday')->getActualWorkingDays($begin, $end);
  205. foreach($tasksGroup as $group => $tasks)
  206. {
  207. if(!$group) $group = '/'; // 未指派
  208. $groupID ++;
  209. $groupKey = $groupID . $group;
  210. $datas['data'][$groupKey] = $this->programplanTao->buildGroupDataForGantt($groupID, $group, $users);
  211. $realStartDate = array();
  212. $realEndDate = array();
  213. $totalTask = count($tasks);
  214. foreach($tasks as $taskID => $task)
  215. {
  216. $dateLimit = $this->programplanTao->getTaskDateLimit($task, zget($plans, $task->execution, null));
  217. if(strpos($selectCustom, 'task') !== false)
  218. {
  219. $data = $this->programplanTao->buildTaskDataForGantt($task, $dateLimit, $groupID, $tasks);
  220. $data->id = $groupID . '-' . $task->id;
  221. $data->parent = $task->parent > 0 && isset($tasks[$task->parent]) ? $groupID . '-' . $task->parent : $groupID;
  222. /* Delayed or not?. */
  223. $isNotCancel = !in_array($task->status, array('cancel', 'closed')) || ($task->status == 'closed' && !helper::isZeroDate($task->finishedDate) && $task->closedReason != 'cancel');
  224. $isComputeDelay = $isNotCancel && !empty($deadlineList[$taskID]);
  225. if($isComputeDelay) $task = $this->task->computeDelay($task, $deadlineList[$taskID], $workingDays);
  226. $data->delay = $this->lang->programplan->delayList[0];
  227. $data->delayDays = 0;
  228. if(isset($task->delay) && $task->delay > 0)
  229. {
  230. $data->delay = $this->lang->programplan->delayList[1];
  231. $data->delayDays = $task->delay;
  232. }
  233. $datas['data'][$task->id] = $data;
  234. }
  235. if(!empty($dateLimit['start'])) $realStartDate[] = strtotime($dateLimit['start']);
  236. if(!empty($dateLimit['end'])) $realEndDate[] = strtotime($dateLimit['end']);
  237. if(!isset($stageIndex[$groupKey]['totalConsumed'])) $stageIndex[$groupKey]['totalConsumed'] = 0;
  238. if(!isset($stageIndex[$groupKey]['totalReal'])) $stageIndex[$groupKey]['totalReal'] = 0;
  239. if(!isset($stageIndex[$groupKey]['totalEstimate'])) $stageIndex[$groupKey]['totalEstimate'] = 0;
  240. $stageIndex[$groupKey]['totalConsumed'] += $task->consumed;
  241. $stageIndex[$groupKey]['totalReal'] += $task->left + $task->consumed;
  242. $stageIndex[$groupKey]['totalEstimate'] += $task->estimate;
  243. }
  244. /* Calculate group realBegan and realEnd. */
  245. if(!empty($realStartDate)) $datas['data'][$groupKey]->realBegan = date('Y-m-d', min($realStartDate));
  246. if(!empty($realEndDate) and (count($realEndDate) == $totalTask)) $datas['data'][$groupKey]->realEnd = date('Y-m-d', max($realEndDate));
  247. }
  248. $datas = $this->programplanTao->setStageSummary($datas, $stageIndex);
  249. $datas['links'] = $this->programplanTao->buildGanttLinks($executionID, $datas['data']);
  250. $datas['data'] = isset($datas['data']) ? array_values($datas['data']) : array();
  251. return $returnJson ? json_encode($datas) : $datas;
  252. }
  253. /**
  254. * 批量查询阶段关联的项目和属性并过滤日期。
  255. * Get product and attribute for stage correlation.
  256. *
  257. * @param array $plans
  258. * @access public
  259. * @return array
  260. */
  261. public function processPlans($plans)
  262. {
  263. foreach($plans as $planID => $plan) $plans[$planID] = $this->processPlan($plan);
  264. return $plans;
  265. }
  266. /**
  267. * 查询阶段关联的项目和属性并过滤日期。
  268. * Get product and attribute for stage correlation.
  269. *
  270. * @param object $plan
  271. * @access public
  272. * @return object
  273. */
  274. public function processPlan($plan)
  275. {
  276. $plan->setMilestone = true;
  277. if($plan->parent)
  278. {
  279. $attribute = $this->dao->select('attribute')->from(TABLE_PROJECT)->where('id')->eq($plan->parent)->fetch('attribute');
  280. $plan->attribute = $attribute == 'develop' ? $attribute : $plan->attribute;
  281. }
  282. else
  283. {
  284. $milestones = $this->programplanTao->getStageCount($plan->id, 'milestone');
  285. if($milestones > 0)
  286. {
  287. $plan->milestone = 0;
  288. $plan->setMilestone = false;
  289. }
  290. }
  291. $plan->begin = helper::isZeroDate($plan->begin) ? '' : $plan->begin;
  292. $plan->end = helper::isZeroDate($plan->end) ? '' : $plan->end;
  293. $plan->realBegan = helper::isZeroDate($plan->realBegan) ? '' : $plan->realBegan;
  294. $plan->realEnd = helper::isZeroDate($plan->realEnd) ? '' : $plan->realEnd;
  295. $plan->product = $this->loadModel('product')->getProductIDByProject($plan->id);
  296. $plan->productName = $this->dao->findByID($plan->product)->from(TABLE_PRODUCT)->fetch('name');
  297. return $plan;
  298. }
  299. /**
  300. * 获取时间段内工作时间间隔天数。
  301. * Get duration.
  302. *
  303. * @param string $begin
  304. * @param string $end
  305. * @access protected
  306. * @return int
  307. */
  308. protected function getDuration($begin, $end)
  309. {
  310. $duration = $this->loadModel('holiday')->getActualWorkingDays($begin, $end);
  311. return count($duration);
  312. }
  313. /**
  314. * 创建/设置一个项目阶段。
  315. * Create/Set a project plan/phase.
  316. *
  317. * @param array $plans
  318. * @param int $projectID
  319. * @param int $productID
  320. * @param int $parentID
  321. * @param int $totalSyncData
  322. * @access public
  323. * @return bool
  324. */
  325. public function create($plans, $projectID = 0, $productID = 0, $parentID = 0, $totalSyncData = 0)
  326. {
  327. if(empty($plans)) dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->programplan->name);
  328. if(dao::isError()) return false;
  329. /* Get linked product by projectID. */
  330. $this->loadModel('action');
  331. $this->loadModel('execution');
  332. $linkProducts = $this->programplanTao->getLinkProductsForCreate($projectID, $productID);
  333. $project = $this->fetchByID($projectID, 'project');
  334. /* Set each plans. */
  335. $updateUserViewIdList = array();
  336. $enabledPoints = array();
  337. $parallel = 0;
  338. $parents = array();
  339. $prevSyncData = null;
  340. $prevLevel = 0;
  341. foreach($plans as $plan)
  342. {
  343. $level = isset($plan->level) ? $plan->level : 0;
  344. $syncData = isset($plan->syncData) ? $plan->syncData : null;
  345. unset($plan->level, $plan->syncData);
  346. $parallel = isset($plan->parallel) ? $plan->parallel : 0;
  347. if(!empty($plan->point)) $enabledPoints = array_merge($enabledPoints, $plan->point);
  348. if($plan->id)
  349. {
  350. $stageID = $plan->id;
  351. $parents[$level] = $stageID;
  352. unset($plan->id, $plan->type);
  353. $changes = $this->programplanTao->updateRow($stageID, $projectID, $plan);
  354. if(dao::isError()) return false;
  355. if(!empty($changes))
  356. {
  357. $actionID = $this->action->create('execution', $stageID, 'edited');
  358. $this->action->logHistory($actionID, $changes);
  359. /* Add PM to stage teams and project teams. */
  360. if(!empty($plan->PM)) $this->execution->addExecutionMembers($stageID, array($plan->PM));
  361. if($plan->acl != 'open') $updateUserViewIdList[] = $stageID;
  362. $this->updateSubStageAttr($stageID, $plan->attribute);
  363. }
  364. }
  365. else
  366. {
  367. if($level > 0 && isset($parents[$level - 1])) $plan->parent = $parents[$level - 1];
  368. $stageID = $this->programplanTao->insertStage($plan, $projectID, $productID, $level > 0 ? $plan->parent : $parentID);
  369. if(dao::isError()) return false;
  370. $parents[$level] = $stageID;
  371. $extra = ($project && $project->hasProduct and !empty($linkProducts['products'])) ? implode(',', $linkProducts['products']) : '';
  372. $this->action->create('execution', $stageID, 'opened', '', $extra);
  373. $this->execution->updateProducts($stageID, $linkProducts);
  374. if($plan->acl != 'open') $updateUserViewIdList[] = $stageID;
  375. }
  376. if(!$totalSyncData && $prevSyncData && $prevLevel == $level - 1) $this->programplanTao->syncParentData($stageID, $parents[$prevLevel]);
  377. if($totalSyncData && $prevSyncData === null && $parentID) $this->programplanTao->syncParentData($stageID, $parentID);
  378. $prevSyncData = $syncData;
  379. $prevLevel = $level;
  380. }
  381. if($project && $project->model == 'ipd') $this->dao->update(TABLE_PROJECT)->set('parallel')->eq($parallel)->where('id')->eq($projectID)->exec();
  382. if($updateUserViewIdList) $this->loadModel('user')->updateUserView($updateUserViewIdList, 'sprint');
  383. if($enabledPoints) $this->programplanTao->updatePoint($projectID, $enabledPoints);
  384. return true;
  385. }
  386. /**
  387. * 设置阶段在层级中路径。
  388. * Set stage tree path.
  389. *
  390. * @param int $planID
  391. * @access public
  392. * @return bool
  393. */
  394. public function setTreePath($planID)
  395. {
  396. $stage = $this->dao->select('id,type,parent,path,grade')->from(TABLE_PROJECT)->where('id')->eq($planID)->fetch();
  397. $parent = $this->dao->select('id,type,parent,path,grade')->from(TABLE_PROJECT)->where('id')->eq($stage->parent)->fetch();
  398. $this->loadModel('execution');
  399. if(empty($parent))
  400. {
  401. $path['path'] = ",{$stage->id},";
  402. $path['grade'] = 1;
  403. }
  404. elseif($parent && $parent->type == 'project')
  405. {
  406. $path['path'] = ",{$parent->id},{$stage->id},";
  407. $path['grade'] = 1;
  408. }
  409. elseif(isset($this->lang->execution->typeList[$parent->type]))
  410. {
  411. $path['path'] = $parent->path . "{$stage->id},";
  412. $path['grade'] = $parent->grade + 1;
  413. }
  414. $children = $this->execution->getChildExecutions($planID);
  415. $this->dao->update(TABLE_PROJECT)->set('path')->eq($path['path'])->set('grade')->eq($path['grade'])->where('id')->eq($stage->id)->exec();
  416. if(empty($children)) return !dao::isError();
  417. foreach($children as $id => $child) $this->setTreePath($id);
  418. return !dao::isError();
  419. }
  420. /**
  421. * 更新阶段。
  422. * Update a plan.
  423. *
  424. * @param int $planID
  425. * @param int $projectID
  426. * @param object $plan
  427. * @access public
  428. * @return bool
  429. */
  430. public function update($planID = 0, $projectID = 0, $plan = null)
  431. {
  432. if(empty($plan)) return false;
  433. $changes = $this->programplanTao->updateRow($planID, $projectID, $plan);
  434. if(dao::isError()) return false;
  435. /* Synchronously update sub-phase permissions. */
  436. $childIdList = $this->dao->select('id')->from(TABLE_PROJECT)->where('path')->like("%,$planID,%")->fetchPairs();
  437. if(!empty($childIdList)) $this->dao->update(TABLE_PROJECT)->set('acl')->eq($plan->acl)->where('id')->in($childIdList)->exec();
  438. $this->setTreePath($planID);
  439. $this->updateSubStageAttr($planID, $plan->attribute);
  440. if($plan->acl != 'open')
  441. {
  442. $this->loadModel('user')->updateUserView($childIdList, 'sprint');
  443. }
  444. if($changes)
  445. {
  446. $actionID = $this->loadModel('action')->create('execution', $planID, 'edited');
  447. $this->action->logHistory($actionID, $changes);
  448. }
  449. return true;
  450. }
  451. /**
  452. * 根据计划ID判断是否创建了任务。
  453. * Is create task.
  454. *
  455. * @param int $planID
  456. * @access public
  457. * @return bool
  458. */
  459. public function isCreateTask($planID)
  460. {
  461. if(empty($planID)) return true;
  462. $task = $this->dao->select('*')->from(TABLE_TASK)->where('execution')->eq($planID)->andWhere('deleted')->eq('0')->limit(1)->fetch();
  463. return empty($task);
  464. }
  465. /**
  466. * 根据父id获取父阶段的子类型。
  467. * Get parent stage's children types by parentID.
  468. *
  469. * @param int $parentID
  470. * @access public
  471. * @return array|bool
  472. */
  473. public function getParentChildrenTypes($parentID)
  474. {
  475. if(empty($parentID)) return true;
  476. return $this->dao->select('type')->from(TABLE_EXECUTION)->where('parent')->eq($parentID)->andWhere('deleted')->eq('0')->fetchPairs();
  477. }
  478. /**
  479. * 是否可以点击.
  480. * Is clickable.
  481. *
  482. * @param object $plan
  483. * @param string $action
  484. * @static
  485. * @access public
  486. * @return bool
  487. */
  488. public static function isClickable($plan, $action)
  489. {
  490. if(strtolower($action) != 'create') return true;
  491. global $dao;
  492. if(empty($plan->id)) return true;
  493. $task = $dao->select('*')->from(TABLE_TASK)->where('execution')->eq($plan->id)->andWhere('deleted')->eq('0')->limit(1)->fetch();
  494. return empty($task);
  495. }
  496. /**
  497. * 获取父阶段列表。
  498. * Get parent stage list.
  499. *
  500. * @param int $executionID
  501. * @param int $planID
  502. * @param int $productID
  503. * @param string $param withParent|noclosed
  504. * @access public
  505. * @return array
  506. */
  507. public function getParentStageList($executionID, $planID, $productID, $param = '')
  508. {
  509. $parentStage = $this->programplanTao->getParentStages($executionID, $planID, $productID, $param);
  510. if(!$parentStage) return array(0 => $this->lang->programplan->emptyParent);
  511. $plan = $this->getByID($planID);
  512. $parents = array();
  513. $withParent = strpos($param, 'withparent') !== false;
  514. $isStage = strpos("|$param|", '|stage|') !== false || strpos($param, 'stage') === false;
  515. $allExecutions = $withParent ? $this->dao->select('id,name,parent,grade,path,type')->from(TABLE_EXECUTION)
  516. ->where('type')->notin(array('program', 'project'))
  517. ->andWhere('deleted')->eq('0')
  518. ->beginIf($executionID)->andWhere('project')->eq($executionID)->fi()
  519. ->fetchAll('id') : array();
  520. foreach($allExecutions as $execution) $parents[$execution->parent] = isset($allExecutions[$execution->parent]) ? $allExecutions[$execution->parent] : array();
  521. foreach($parentStage as $key => $stage)
  522. {
  523. $isCreate = $this->isCreateTask($key);
  524. $parentTypes = $this->getParentChildrenTypes($key);
  525. if(!empty($plan))
  526. {
  527. if(!$isCreate && $key != $plan->parent) unset($parentStage[$key]);
  528. if($plan->type == 'stage' && (isset($parentTypes['sprint']) || isset($parentTypes['kanban']))) unset($parentStage[$key]);
  529. if(($plan->type == 'sprint' || $plan->type == 'kanban') && isset($parentTypes['stage'])) unset($parentStage[$key]);
  530. }
  531. else
  532. {
  533. if(!$isCreate) unset($parentStage[$key]); // 隐藏有数据的阶段
  534. if($isStage && (isset($parentTypes['sprint']) || isset($parentTypes['kanban']))) unset($parentStage[$key]); // 如果是阶段,隐藏叶子节点是迭代和看板的数据
  535. if(!$isStage && (isset($parentTypes['stage']) || isset($parentTypes['stage']))) unset($parentStage[$key]); // 如果不是阶段,隐藏叶子节点是阶段的数据
  536. }
  537. /* Set stage name. */
  538. if($withParent && isset($parentStage[$key]) && !empty($allExecutions))
  539. {
  540. $currentStage = $allExecutions[$key];
  541. $paths = array_slice(explode(',', trim($currentStage->path, ',')), 1);
  542. $executionName = '';
  543. foreach($paths as $path)
  544. {
  545. if(isset($allExecutions[$path])) $executionName .= '/' . $allExecutions[$path]->name;
  546. }
  547. $parentStage[$key] = $executionName;
  548. }
  549. }
  550. $project = $this->fetchByID($executionID);
  551. if((!empty($plan) && $plan->type == 'stage') || $project->model == 'waterfall' || $isStage) $parentStage[0] = $this->lang->programplan->emptyParent;
  552. ksort($parentStage);
  553. return $parentStage;
  554. }
  555. /**
  556. * 通过计算获取阶段状态。
  557. * Compute stage status.
  558. *
  559. * @param int $stage
  560. * @param string $action
  561. * @param bool $isParent
  562. * @access public
  563. * @return bool|array
  564. * @param int $stageID
  565. */
  566. public function computeProgress($stageID, $action = '', $isParent = false)
  567. {
  568. $stage = $this->loadModel('execution')->fetchByID($stageID);
  569. if(empty($stage) || empty($stage->path)) return false;
  570. $project = $this->loadModel('project')->fetchByID($stage->project);
  571. $model = zget($project, 'model', '');
  572. if(empty($stage) or empty($stage->path) or (!in_array($model, array('waterfall','waterfallplus','ipd','research')))) return false;
  573. $action = strtolower($action);
  574. $parentIdList = array_reverse(explode(',', trim($stage->path, ',')));
  575. foreach($parentIdList as $id)
  576. {
  577. $parent = $this->execution->fetchByID((int)$id);
  578. if(empty($this->lang->execution->typeList[$parent->type]) || (!$isParent && $id == $stageID)) continue;
  579. /** 获取子阶段关联开始任务数以及状态下子阶段数量。 */
  580. /** Get the number of sub-stage associated start tasks and the number of sub-stages under the state. */
  581. $statusCount = array();
  582. $children = $this->execution->getChildExecutions($parent->id);
  583. $allChildren = $this->dao->select('id')->from(TABLE_EXECUTION)->where('deleted')->eq(0)->andWhere('path')->like("{$parent->path}%")->andWhere('id')->ne($id)->fetchPairs();
  584. $startTasks = $this->dao->select('count(1) as count')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('execution')->in($allChildren)->andWhere('consumed')->ne(0)->fetch('count');
  585. foreach($children as $childExecution)
  586. {
  587. if(empty($statusCount[$childExecution->status])) $statusCount[$childExecution->status] = 0;
  588. $statusCount[$childExecution->status] ++;
  589. }
  590. if(empty($statusCount)) continue;
  591. $result = $this->getNewParentAndAction($statusCount, $parent, (int)$startTasks, $action, $project);
  592. $newParent = $result['newParent'] ?? null;
  593. $parentAction = $result['parentAction'] ?? '';
  594. /* 如果当前是顶级阶段,并且由于交付物不能关闭,则跳转到顶级阶段的关闭页面。 */
  595. if(isset($newParent->status) && $newParent->status == 'closed')
  596. {
  597. $isTopStage = $parent->grade == 1 && $parent->type != 'project' && $stageID != $id && $parent->status == 'doing';
  598. if(in_array($this->config->edition, array('max', 'ipd')) && $isTopStage && !$this->execution->canCloseByDeliverable($parent))
  599. {
  600. $url = helper::createLink('execution', 'close', "executionID={$parent->id}");
  601. return array('result' => 'fail', 'callback' => "zui.Modal.confirm('{$this->lang->execution->cannotAutoCloseParent}').then((res) => {if(res) {loadModal('$url', '.modal-dialog');} else {loadPage();}});");
  602. }
  603. }
  604. /** 更新状态以及记录日志。 */
  605. /** Update status and save log. */
  606. if(isset($newParent) && $newParent)
  607. {
  608. $this->dao->update(TABLE_EXECUTION)->data($newParent)->where('id')->eq($id)->exec();
  609. $this->loadModel('action')->create('execution', (int)$id, $parentAction, '', $parentAction);
  610. }
  611. unset($newParent, $parentAction);
  612. }
  613. return true;
  614. }
  615. /**
  616. * 根据阶段ID,检查阶段是否是叶子阶段。
  617. * Check if the stage is a leaf stage.
  618. *
  619. * @param int $stageID
  620. * @access public
  621. * @return bool
  622. */
  623. public function checkLeafStage($stageID)
  624. {
  625. if(empty($stageID)) return false;
  626. $subStageNumbers = $this->dao->select('COUNT(`id`) AS total')->from(TABLE_EXECUTION)
  627. ->where('parent')->eq($stageID)
  628. ->andWhere('deleted')->eq(0)
  629. ->fetch('total');
  630. return $subStageNumbers == 0;
  631. }
  632. /**
  633. * 检查是否为顶级。
  634. * Check whether it is the top stage.
  635. *
  636. * @param int $planID
  637. * @access public
  638. * @return bool
  639. */
  640. public function isTopStage($planID)
  641. {
  642. $parentID = $this->dao->select('parent')->from(TABLE_EXECUTION)->where('id')->eq($planID)->fetch('parent');
  643. $parentType = $this->dao->select('type')->from(TABLE_EXECUTION)->where('id')->eq($parentID)->fetch('type');
  644. return $parentType == 'project';
  645. }
  646. /**
  647. * 更新子阶段的属性值.
  648. * Update sub-stage attribute.
  649. *
  650. * @param int $planID
  651. * @param string $attribute
  652. * @access public
  653. * @return true
  654. */
  655. public function updateSubStageAttr($planID, $attribute)
  656. {
  657. if($attribute == 'mix') return true;
  658. $subStageList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('parent')->eq($planID)->andWhere('deleted')->eq(0)->fetchAll('id');
  659. if(empty($subStageList)) return true;
  660. $this->dao->update(TABLE_EXECUTION)->set('attribute')->eq($attribute)->where('id')->in(array_keys($subStageList))->exec();
  661. foreach($subStageList as $childID => $subStage) $this->updateSubStageAttr($childID, $attribute);
  662. return true;
  663. }
  664. /**
  665. * 获取阶段当前和子集信息。
  666. * Get plan and its children.
  667. *
  668. * @param string|int|array $planIdList
  669. * @access public
  670. * @return array
  671. */
  672. public function getSelfAndChildrenList($planIdList)
  673. {
  674. if(is_numeric($planIdList)) $planIdList = (array)$planIdList;
  675. $planList = $this->dao->select('t2.*')->from(TABLE_EXECUTION)->alias('t1')
  676. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('FIND_IN_SET(t1.id,t2.`path`)')
  677. ->where('t1.id')->in($planIdList)
  678. ->andWhere('t2.deleted')->eq(0)
  679. ->fetchAll('id');
  680. $selfAndChildrenList = array();
  681. foreach($planIdList as $planID)
  682. {
  683. if(!isset($selfAndChildrenList[$planID])) $selfAndChildrenList[$planID] = array();
  684. foreach($planList as $plan)
  685. {
  686. if(strpos($plan->path, ",$planID,") !== false) $selfAndChildrenList[$planID][$plan->id] = $plan;
  687. }
  688. }
  689. return $selfAndChildrenList;
  690. }
  691. /**
  692. * 获取阶段同一层级信息。
  693. * Get plan's siblings.
  694. *
  695. * @param string|int|array $planIdList
  696. * @access public
  697. * @return array
  698. */
  699. public function getSiblings($planIdList)
  700. {
  701. if(is_numeric($planIdList)) $planIdList = (array)$planIdList;
  702. $siblingsList = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1')
  703. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.parent=t2.parent')
  704. ->where('t2.id')->in($planIdList)
  705. ->andWhere('t1.deleted')->eq(0)
  706. ->fetchAll('id');
  707. $siblingStages = array();
  708. foreach($planIdList as $planID)
  709. {
  710. if(!isset($siblingStages[$planID])) $siblingStages[$planID] = array();
  711. foreach($siblingsList as $sibling)
  712. {
  713. if($siblingsList[$planID]->parent == $sibling->parent) $siblingStages[$planID][$sibling->id] = $sibling;
  714. }
  715. }
  716. return $siblingStages;
  717. }
  718. /**
  719. * 获取阶段ID的属性。
  720. * Get stageID attribute.
  721. *
  722. * @param int $stageID
  723. * @access public
  724. * @return false|string
  725. */
  726. public function getStageAttribute($stageID)
  727. {
  728. return $this->dao->select('attribute')->from(TABLE_EXECUTION)->where('id')->eq($stageID)->fetch('attribute');
  729. }
  730. /**
  731. * 保存自定义配置
  732. * Save custom setting.
  733. *
  734. * @param object $settings
  735. * @param string $owner
  736. * @param string $module
  737. * @access protected
  738. * @return void
  739. */
  740. protected function saveCustomSetting($settings, $owner, $module)
  741. {
  742. $zooming = zget($settings, 'zooming', '');
  743. $stageCustom = zget($settings, 'stageCustom', '');
  744. $ganttFields = zget($settings, 'ganttFields', '');
  745. $this->loadModel('setting');
  746. $this->setting->setItem("$owner.$module.browse.stageCustom", $stageCustom);
  747. $this->setting->setItem("$owner.$module.ganttCustom.ganttFields", $ganttFields);
  748. $this->setting->setItem("$owner.$module.ganttCustom.zooming", $zooming);
  749. }
  750. /**
  751. * 获取甘特图的任务.
  752. * Get tasks in gantt.
  753. *
  754. * @param int $projectID
  755. * @param array $planIdList
  756. * @param string $browseType
  757. * @param int $queryID
  758. * @param object $pager
  759. * @access public
  760. * @return array
  761. */
  762. public function getGanttTasks($projectID, $planIdList, $browseType, $queryID, $pager = null)
  763. {
  764. $tasks = array();
  765. if($browseType == 'bysearch')
  766. {
  767. $query = $this->loadModel('search')->getQuery($queryID);
  768. if($query)
  769. {
  770. $this->session->set('projectTaskQuery', $query->sql);
  771. $this->session->set('projectTaskForm', $query->form);
  772. }
  773. elseif(!$this->session->projectTaskQuery)
  774. {
  775. $this->session->set('projectTaskQuery', ' 1 = 1');
  776. }
  777. if(strpos($this->session->projectTaskQuery, "deleted =") === false) $this->session->set('projectTaskQuery', $this->session->projectTaskQuery . " AND deleted = '0'");
  778. $projectTaskQuery = $this->session->projectTaskQuery;
  779. $projectTaskQuery .= " AND `project` = '$projectID'";
  780. $projectTaskQuery .= " AND `execution` " . helper::dbIN($planIdList);
  781. $this->session->set('projectTaskQueryCondition', $projectTaskQuery, $this->app->tab);
  782. $this->session->set('projectTaskOnlyCondition', true, $this->app->tab);
  783. $tasks = $this->loadModel('execution')->getSearchTasks($projectTaskQuery, 'execution_asc,order_asc,id_asc', $pager, 'projectTask');
  784. }
  785. elseif(!empty($planIdList))
  786. {
  787. $tasks = $this->dao->select('t1.*,t2.version AS latestStoryVersion, t2.status AS storyStatus')->from(TABLE_TASK)->alias('t1')
  788. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  789. ->where('t1.deleted')->eq(0)
  790. ->andWhere('t1.project')->eq($projectID)
  791. ->andWhere('t1.execution')->in($planIdList)
  792. ->orderBy('execution_asc, order_asc, id_asc')
  793. ->fetchAll('id');
  794. }
  795. $isGantt = $this->app->rawModule == 'programplan' && $this->app->rawMethod == 'browse';
  796. if($isGantt) $plans = $this->loadModel('execution')->getByIdList($planIdList);
  797. $begin = $end = helper::today();
  798. $deadlineList = array();
  799. $taskDateLimit = $this->dao->select('taskDateLimit')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('taskDateLimit');
  800. foreach($tasks as $taskID => $task)
  801. {
  802. if(!$isGantt && helper::isZeroDate($task->deadline)) continue;
  803. $plan = isset($plans[$task->execution]) ? $plans[$task->execution] : null;
  804. $dateLimit = $this->programplanTao->getTaskDateLimit($task, $plan, $taskDateLimit == 'limit' ? zget($tasks, $task->parent, null) : null);
  805. $deadline = substr($dateLimit['end'], 0, 10);
  806. $begin = $deadline < $begin ? $deadline : $begin;
  807. $deadlineList[$taskID] = $deadline;
  808. }
  809. $workingDays = $this->loadModel('holiday')->getActualWorkingDays($begin, $end);
  810. $storyVersionPairs = $this->loadModel('task')->getTeamStoryVersion(array_keys($tasks));
  811. foreach($tasks as $taskID => $task)
  812. {
  813. /* Story changed or not. */
  814. $task->storyVersion = zget($storyVersionPairs, $task->id, $task->storyVersion);
  815. $task->needConfirm = false;
  816. if(!empty($task->storyStatus) && $task->storyStatus == 'active' && !in_array($task->status, array('cancel', 'closed')) && $task->latestStoryVersion > $task->storyVersion)
  817. {
  818. $task->needConfirm = true;
  819. $task->status = 'changed';
  820. }
  821. /* Delayed or not?. */
  822. $isNotCancel = !in_array($task->status, array('cancel', 'closed')) || ($task->status == 'closed' && !helper::isZeroDate($task->finishedDate) && $task->closedReason != 'cancel');
  823. $isComputeDelay = $isNotCancel && !empty($deadlineList[$taskID]);
  824. if($isComputeDelay) $task = $this->task->computeDelay($task, $deadlineList[$taskID], $workingDays);
  825. }
  826. return $tasks;
  827. }
  828. /**
  829. * 根据阶段的开始和结束,计算工作日。
  830. * Calc stage days by stage begin and end.
  831. *
  832. * @param string $start
  833. * @param string $end
  834. * @access public
  835. * @return int
  836. */
  837. public function calcDaysForStage($start, $end)
  838. {
  839. $weekend = $this->config->execution->weekend;
  840. $days = range(strtotime($start), strtotime($end), 86400);
  841. foreach($days as $key => $day)
  842. {
  843. $weekDay = date('N', $day);
  844. if(($weekend == 2 && $weekDay == 6) || $weekDay == 7) unset($days[$key]);
  845. }
  846. return count($days);
  847. }
  848. }