tao.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. <?php
  2. /**
  3. * The tao 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 lanzongjun <lanzongjun@easycorp.ltd>
  8. * @link https://www.zentao.net
  9. */
  10. class programplanTao extends programplanModel
  11. {
  12. /**
  13. * 获取阶段信息。
  14. * Get stage list.
  15. *
  16. * @param int $executionID
  17. * @param int $productID
  18. * @param string $browseType
  19. * @param string $orderBy
  20. * @access protected
  21. * @return array
  22. */
  23. protected function getStageList($executionID, $productID, $browseType, $orderBy = 'id_asc')
  24. {
  25. if(empty($executionID)) return array();
  26. $projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch('model');
  27. $stageList = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1')
  28. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project')
  29. ->where('1 = 1')
  30. ->beginIF(!in_array($projectModel, array('waterfallplus', 'ipd')))->andWhere('t1.type')->eq('stage')->fi()
  31. ->beginIF($productID)->andWhere('t2.product')->eq($productID)->fi()
  32. ->beginIF($browseType == 'all' || $browseType == 'leaf')->andWhere('t1.project')->eq($executionID)->fi()
  33. ->beginIF($browseType == 'parent')->andWhere('t1.parent')->eq($executionID)->fi()
  34. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  35. ->andWhere('((t1.enabled')->eq('on')
  36. ->andWhere('t1.deleted')->eq('0')
  37. ->markRight(1)
  38. ->beginIF($this->app->rawModule == 'programplan' && $this->app->rawMethod == 'create')
  39. ->orWhere('(t1.enabled')->eq('off')
  40. ->andWhere('t1.deleted')->eq('1')
  41. ->markRight(1)
  42. ->fi()
  43. ->markRight(1)
  44. ->orderBy($orderBy)
  45. ->fetchAll('id', false);
  46. if($projectModel == 'ipd')
  47. {
  48. $stagePointGroup = $this->dao->select('execution,category,categoryTitle')->from(TABLE_OBJECT)
  49. ->where('execution')->in(array_keys($stageList))
  50. ->andWhere('type')->eq('decision')
  51. ->andWhere('enabled')->eq('1')
  52. ->fetchGroup('execution', 'category');
  53. foreach($stageList as $stage)
  54. {
  55. $stage->enabledPoints = array();
  56. if(isset($stagePointGroup[$stage->id]))
  57. {
  58. foreach($stagePointGroup[$stage->id] as $point) $stage->enabledPoints[$point->category] = $point->categoryTitle;
  59. }
  60. }
  61. }
  62. return $stageList;
  63. }
  64. /**
  65. * 更新项目阶段。
  66. * update program plan.
  67. *
  68. * @param object $plan
  69. * @param object $oldPlan
  70. * @access protected
  71. * @return array|false
  72. * @param int $planID
  73. * @param int $projectID
  74. */
  75. protected function updateRow($planID, $projectID, $plan)
  76. {
  77. if(empty($plan)) return false;
  78. $oldPlan = $this->fetchByID($planID);
  79. $planChanged = ($oldPlan->name != $plan->name || $oldPlan->milestone != $plan->milestone || $oldPlan->begin != $plan->begin || $oldPlan->end != $plan->end);
  80. if($planChanged) $plan->version = $oldPlan->version + 1;
  81. if(empty($plan->parent)) $plan->parent = $projectID;
  82. $parentStage = empty($projectID) ? null : $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($plan->parent)->andWhere('type')->eq('stage')->fetch();
  83. $relatedIdList = $this->loadModel('execution')->getRelatedExecutions($planID);
  84. $relatedIdList = !empty($relatedIdList) ? implode(',', array_keys($relatedIdList)) : '0';
  85. $setCode = !empty($this->config->setCode);
  86. $getName = ($relatedIdList && $oldPlan->project && $parentStage && $oldPlan->parent) ? true : false;
  87. /* Fix bug #22030. Reset field name for show dao error. */
  88. $this->lang->project->name = $this->lang->programplan->name;
  89. $this->lang->project->code = $this->lang->execution->code;
  90. $this->dao->update(TABLE_PROJECT)->data($plan, 'point,defaultPoint')->autoCheck()
  91. ->checkIF(!empty($plan->name) && $getName, 'name', 'unique', "id in ({$relatedIdList}) and type in ('sprint','stage') and `project` = {$oldPlan->project} and `deleted` = '0' and `parent` = {$oldPlan->parent}")
  92. ->checkIF(!empty($plan->code) && $setCode, 'code', 'unique', "id != {$planID} and type in ('sprint','stage','kanban') and `project` = {$oldPlan->project} and `deleted` = '0' and `parent` = {$oldPlan->parent}")
  93. ->where('id')->eq($planID)
  94. ->exec();
  95. /* IPD项目停/启用阶段时,子阶段同样停/启用。*/
  96. if(isset($plan->enabled) && $oldPlan->enabled != $plan->enabled)
  97. {
  98. $this->dao->update(TABLE_PROJECT)
  99. ->set('deleted')->eq($plan->deleted)
  100. ->set('enabled')->eq($plan->enabled)
  101. ->where('project')->eq($projectID)
  102. ->andWhere('grade')->gt(1)
  103. ->andWhere("FIND_IN_SET($planID, `path`)")
  104. ->exec();
  105. }
  106. if($planChanged) $this->insertProjectSpec($planID, $plan);
  107. if(dao::isError()) return false;
  108. return common::createChanges($oldPlan, $plan);
  109. }
  110. /**
  111. * 获取父阶段键值对。
  112. * Get parent stage list.
  113. *
  114. * @param int $executionID
  115. * @param int $planID
  116. * @param int $productID
  117. * @param string $param
  118. * @access protected
  119. * @return array|false
  120. */
  121. protected function getParentStages($executionID, $planID, $productID, $param = '')
  122. {
  123. dao::$filterTpl = 'skip';
  124. $parentStage = $this->dao->select('t1.id, t1.name')->from(TABLE_PROJECT)->alias('t1')
  125. ->beginIF($productID)
  126. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project')
  127. ->fi()
  128. ->where('t1.project')->eq($executionID)
  129. ->beginIF($productID)
  130. ->andWhere('t2.product')->eq($productID)
  131. ->fi()
  132. ->andWhere('t1.type')->eq('stage')
  133. ->andWhere('t1.deleted')->eq(0)
  134. ->andWhere('t1.path')->notlike("%,$planID,%")
  135. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  136. ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
  137. ->orderBy('t1.id desc')
  138. ->fetchPairs();
  139. if(dao::isError()) return false;
  140. return $parentStage;
  141. }
  142. /**
  143. * 根据action获取excution。
  144. * Get new parent and action.
  145. *
  146. * @param array $statusCount
  147. * @param object $parent
  148. * @param int $startTasks
  149. * @param string $action
  150. * @param object $project
  151. * @access protected
  152. * @return array
  153. */
  154. protected function getNewParentAndAction($statusCount, $parent, $startTasks, $action, $project)
  155. {
  156. $count = count($statusCount);
  157. $newParent = null;
  158. $parentAction = '';
  159. $this->loadModel('execution');
  160. if(isset($statusCount['wait']) && $count == 1 && helper::isZeroDate($parent->realBegan) && $startTasks == 0)
  161. {
  162. if($parent->status == 'wait') return array('newParent' => null, 'parentAction' => '');
  163. $newParent = $this->execution->buildExecutionByStatus('wait');
  164. $parentAction = 'waitbychild';
  165. }
  166. elseif(isset($statusCount['closed']) && $count == 1)
  167. {
  168. if($parent->status == 'closed') return array('newParent' => null, 'parentAction' => '');
  169. $newParent = $this->execution->buildExecutionByStatus('closed');
  170. $parentAction = 'closedbychild';
  171. }
  172. elseif(isset($statusCount['suspended']) && ($count == 1 || (isset($statusCount['closed']) && $count == 2)))
  173. {
  174. if($parent->status == 'suspended') return array('newParent' => null, 'parentAction' => '');
  175. $newParent = $this->execution->buildExecutionByStatus('suspended');
  176. $parentAction = 'suspendedbychild';
  177. }
  178. else
  179. {
  180. if($parent->status == 'doing') return array('newParent' => null, 'parentAction' => '');
  181. $newParent = $this->execution->buildExecutionByStatus('doing');
  182. $parentAction = $parent->status == 'wait' ? 'startbychildstart' : 'startbychild' . $action;
  183. }
  184. return array('newParent' => $newParent, 'parentAction' => $parentAction);
  185. }
  186. /**
  187. * 设置任务基线数据。
  188. * Set task baseline data.
  189. *
  190. * @param array $oldTasks
  191. * @param array $tasks
  192. * @access protected
  193. * @return array
  194. */
  195. protected function setTaskBaseline($oldTasks, $tasks)
  196. {
  197. foreach($oldTasks as $id => $oldTask)
  198. {
  199. if(!isset($tasks[$id])) continue;
  200. $tasks[$id]->version = $oldTask->version;
  201. $tasks[$id]->name = $oldTask->name;
  202. $tasks[$id]->estStarted = $oldTask->estStarted;
  203. $tasks[$id]->deadline = $oldTask->deadline;
  204. }
  205. return $tasks;
  206. }
  207. /**
  208. * 设置甘特图阶段数据。
  209. * Set the Gantt chart stage data.
  210. *
  211. * @param array $plans
  212. * @param string $browseType
  213. * @access protected
  214. * @return array
  215. */
  216. protected function initGanttPlans($plans, $browseType = '')
  217. {
  218. $this->app->loadLang('stage');
  219. $datas = $stageIndex = $planIdList = $reviewDeadline = array();
  220. $today = helper::today();
  221. $isMilestone = "<icon class='icon icon-flag icon-sm red'></icon> ";
  222. foreach($plans as $plan)
  223. {
  224. $plan->isParent = false;
  225. if(isset($plans[$plan->parent]))
  226. {
  227. $plans[$plan->parent]->isParent = true;
  228. }
  229. elseif($browseType == 'bysearch')
  230. {
  231. $plans[$plan->id]->parent = 0;
  232. }
  233. }
  234. foreach($plans as $plan)
  235. {
  236. $planIdList[$plan->id] = $plan->id;
  237. $reviewDeadline[$plan->id]['stageEnd'] = $plan->end;
  238. $reviewDeadline[$plan->id]['stageBegin'] = $plan->begin;
  239. $data = $this->buildPlanDataForGantt($plan);
  240. /* Determines if the object is delay. */
  241. $data->delay = $this->lang->programplan->delayList[0];
  242. $data->delayDays = 0;
  243. if($today > $data->endDate and $plan->status != 'closed')
  244. {
  245. $data->delay = $this->lang->programplan->delayList[1];
  246. $data->delayDays = helper::diffDate($today, $data->endDate);
  247. }
  248. $datas['data'][$plan->id] = $data;
  249. $stageIndex[$plan->id] = array('planID' => $plan->id, 'parent' => isset($plans[$plan->parent]) ? $plan->parent : $plan->project, 'totalEstimate' => 0, 'totalConsumed' => 0, 'totalReal' => 0, 'path' => $plan->path);
  250. }
  251. return array('datas' => $datas, 'stageIndex' => $stageIndex, 'planIdList' => $planIdList, 'reviewDeadline' => $reviewDeadline);
  252. }
  253. /**
  254. * 设置甘特图任务数据。
  255. * Set the Gantt chart task data.
  256. *
  257. * @param array $tasks
  258. * @param array $plans
  259. * @param string $selectCustom
  260. * @param array $datas
  261. * @param array $stageIndex
  262. * @access protected
  263. * @return array
  264. */
  265. protected function setTask($tasks, $plans, $selectCustom, $datas, $stageIndex)
  266. {
  267. $this->app->loadLang('task');
  268. $this->loadModel('holiday');
  269. $executions = array();
  270. $today = helper::today();
  271. $taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account');
  272. $users = $this->loadModel('user')->getPairs('noletter');
  273. $firstTask = reset($tasks);
  274. $projectID = $firstTask ? $firstTask->project : 0;
  275. $taskDateLimit = $this->dao->select('taskDateLimit')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('taskDateLimit');
  276. foreach($tasks as $task)
  277. {
  278. $plan = zget($plans, $task->execution, null);
  279. $dateLimit = $this->getTaskDateLimit($task, $plan, $taskDateLimit == 'limit' ? zget($tasks, $task->parent, null) : null);
  280. $data = $this->buildTaskDataForGantt($task, $dateLimit);
  281. $data->id = $task->execution . '-' . $task->id;
  282. $data->parent = $task->parent > 0 && isset($tasks[$task->parent]) ? $task->execution . '-' . $task->parent : $task->execution;
  283. $data->allowLinks = $plan->type == 'kanban' ? false : true;
  284. if(!isset($executions[$task->execution])) $executions[$task->execution] = $this->dao->select('status')->from(TABLE_EXECUTION)->where('id')->eq($task->execution)->fetch('status');
  285. /* Determines if the object is delay. */
  286. $data->delay = $this->lang->programplan->delayList[0];
  287. $data->delayDays = 0;
  288. if(isset($task->delay) && $task->delay > 0)
  289. {
  290. $data->delay = $this->lang->programplan->delayList[1];
  291. $data->delayDays = $task->delay;
  292. }
  293. /* If multi task then show the teams. */
  294. if($task->mode == 'multi' && !empty($taskTeams[$task->id])) $data->owner_id = implode(',', array_map(function($assignedTo) use($users){return zget($users, $assignedTo);}, array_keys($taskTeams[$task->id])));
  295. if(strpos($selectCustom, 'task') !== false) $datas['data'][$data->id] = $data;
  296. if($task->isParent) continue;
  297. foreach($stageIndex as $index => $stage)
  298. {
  299. if($stage['planID'] != $task->execution) continue;
  300. if(!isset($stageIndex[$index])) continue;
  301. $stageIndex[$index]['totalEstimate'] += $task->estimate;
  302. $stageIndex[$index]['totalConsumed'] += $task->consumed;
  303. $stageIndex[$index]['totalReal'] += ((($task->status == 'closed' || $task->status == 'cancel') ? 0 : $task->left) + $task->consumed);
  304. foreach(explode(',', $stage['path']) as $planID)
  305. {
  306. if(!isset($stageIndex[$planID]) || $planID == $stage['planID']) continue;
  307. $stageIndex[$planID]['totalEstimate'] += $task->estimate;
  308. $stageIndex[$planID]['totalConsumed'] += $task->consumed;
  309. $stageIndex[$planID]['totalReal'] += ((($task->status == 'closed' || $task->status == 'cancel') ? 0 : $task->left) + $task->consumed);
  310. }
  311. }
  312. }
  313. return array('datas' => $datas, 'stageIndex' => $stageIndex);
  314. }
  315. /**
  316. * 获取子阶段数量。
  317. * Get stage count.
  318. *
  319. * @param int $planID
  320. * @param string $mode
  321. * @access protected
  322. * @return int
  323. */
  324. protected function getStageCount($planID, $mode = '')
  325. {
  326. return $this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)
  327. ->where('parent')->eq($planID)
  328. ->andWhere('type')->eq('stage')
  329. ->beginIF($mode == 'milestone')->andWhere('milestone')->eq(1)->fi()
  330. ->andWhere('deleted')->eq(0)
  331. ->fetch('count');
  332. }
  333. /**
  334. * 计算阶段在数据库中的位置序号。
  335. * Compute the new order for programplan. if orders is not set then begin with the order in zt_project table.
  336. *
  337. * @param array $orders
  338. * @param array $plans
  339. * @access protected
  340. * @return array
  341. */
  342. protected function computeOrders($orders, $plans)
  343. {
  344. if(empty($orders)) $orders = array();
  345. asort($orders);
  346. $planCount = count($plans);
  347. if(count($orders) < $planCount)
  348. {
  349. $orderIndex = empty($orders) ? 0 : count($orders);
  350. $lastID = $this->dao->select('id')->from(TABLE_EXECUTION)->orderBy('id_desc')->limit(1)->fetch('id');
  351. for($i = $orderIndex; $i < $planCount; $i ++)
  352. {
  353. $lastID ++;
  354. $orders[$i] = $lastID * 5;
  355. }
  356. }
  357. return $orders;
  358. }
  359. /**
  360. * 插入项目细节。
  361. * Insert project spec.
  362. * @param int $planID
  363. * @param object $plan
  364. * @access protected
  365. * @return bool
  366. */
  367. protected function insertProjectSpec($planID, $plan)
  368. {
  369. if(empty($planID)) return false;
  370. $spec = new stdclass();
  371. $spec->project = $planID;
  372. $spec->version = $plan->version;
  373. $spec->name = $plan->name;
  374. $spec->milestone = $plan->milestone;
  375. $spec->begin = $plan->begin;
  376. $spec->end = $plan->end;
  377. $this->dao->insert(TABLE_PROJECTSPEC)->data($spec)->exec();
  378. return !dao::isError();
  379. }
  380. /**
  381. * 获取阶段百分比。
  382. * Get total percent.
  383. *
  384. * @param object $stage
  385. * @param bool $parent
  386. * @access public
  387. * @return int|float
  388. */
  389. protected function getTotalPercent($stage, $parent = false)
  390. {
  391. /* When parent is equal to true, query the total workload of the subphase. */
  392. $executionID = $parent ? $stage->id : $stage->project;
  393. $plans = $this->getStageList((int)$executionID, (int)$stage->product, 'parent');
  394. $totalPercent = 0;
  395. $stageID = $stage->id;
  396. foreach($plans as $id => $stage)
  397. {
  398. if($id == $stageID) continue;
  399. $totalPercent += $stage->percent;
  400. }
  401. return $totalPercent;
  402. }
  403. /**
  404. * Get point end date.
  405. *
  406. * @param int $planID
  407. * @param object $point
  408. * @param array $reviewDeadline
  409. * @access protected
  410. * @return string
  411. */
  412. protected function getPointEndDate($planID, $point, $reviewDeadline)
  413. {
  414. if($point->end and !helper::isZeroDate($point->end)) return $point->end;
  415. $end = $reviewDeadline[$planID]['stageEnd'];
  416. $begin = $reviewDeadline[$planID]['stageBegin'];
  417. $pointType = $this->dao->select('type')->from(TABLE_DECISION)->where('id')->eq($point->id)->fetch('type');
  418. if($pointType == 'DCP') $end = $this->getReviewDeadline($end, 2);
  419. if($pointType == 'TR')
  420. {
  421. if(isset($reviewDeadline[$planID]['taskEnd']) and !helper::isZeroDate($reviewDeadline[$planID]['taskEnd'])) return $reviewDeadline[$planID]['taskEnd'];
  422. $end = $this->getReviewDeadline($end);
  423. }
  424. return $end <= $begin ? $begin : $end;
  425. }
  426. /**
  427. * 构建IPD版本的甘特图数据。
  428. * Build gantt's data for ipd edition.
  429. *
  430. * @param array $datas
  431. * @param int $projectID
  432. * @param int $productID
  433. * @param string $selectCustom
  434. * @param array $reviewDeadline
  435. * @access protected
  436. * @return array
  437. */
  438. protected function buildGanttData4IPD($datas, $projectID, $productID, $selectCustom, $reviewDeadline)
  439. {
  440. if($this->config->edition != 'ipd') return $datas;
  441. $this->loadModel('review');
  442. $reviewPoints = $this->dao->select('t1.*, t2.status, t2.lastReviewedDate,t2.id as reviewID')->from(TABLE_OBJECT)->alias('t1')
  443. ->leftJoin(TABLE_REVIEW)->alias('t2')->on('t1.id = t2.object')
  444. ->where('t1.deleted')->eq('0')
  445. ->andWhere('t1.project')->eq($projectID)
  446. ->andWhere('t1.enabled')->eq(1)
  447. ->fetchAll('id', false);
  448. foreach($datas['data'] as $plan)
  449. {
  450. if($plan->type != 'plan') continue;
  451. foreach($reviewPoints as $point)
  452. {
  453. if($point->execution != $plan->id) continue;
  454. if(!isset($point->status)) $point->status = '';
  455. $dataID = "{$plan->id}-point{$point->category}-{$point->id}";
  456. if($selectCustom && strpos($selectCustom, "point") !== false && !$plan->parent) $datas['data'][$dataID] = $this->buildPointDataForGantt($plan->id, $point, $reviewDeadline);
  457. }
  458. }
  459. return $datas;
  460. }
  461. /**
  462. * 获取创建时的关联产品。
  463. * Get linkProducts for create.
  464. *
  465. * @param int $projectID
  466. * @param int $productID
  467. * @access protected
  468. * @return array
  469. */
  470. protected function getLinkProductsForCreate($projectID, $productID)
  471. {
  472. $project = $this->fetchByID($projectID, 'project');
  473. $linkProducts = array();
  474. $linkBranches = array();
  475. $productList = $this->loadModel('product')->getProducts($projectID);
  476. if($project && $project->stageBy == 'product')
  477. {
  478. $linkProducts = array(0 => $productID);
  479. if(!empty($productList[$productID])) $linkBranches = array(0 => zget($productList[$productID], 'branches', array()));
  480. }
  481. else
  482. {
  483. $linkProducts = array_keys($productList);
  484. foreach($linkProducts as $index => $productID)
  485. {
  486. if(!empty($productList)) $linkBranches[$index] = $productList[$productID]->branches;
  487. }
  488. }
  489. return array('products' => $linkProducts, 'branch' => $linkBranches);
  490. }
  491. /**
  492. * 插入阶段数据。
  493. * Insert stage.
  494. * @param object $plan
  495. * @param int $projectID
  496. * @param int $productID
  497. * @param int $parentID
  498. * @access protected
  499. * @return int|false
  500. */
  501. protected function insertStage($plan, $projectID, $productID, $parentID)
  502. {
  503. $this->loadModel('execution');
  504. $project = $this->fetchById($projectID, 'project');
  505. $account = $this->app->user->account;
  506. unset($plan->id);
  507. $plan->status = 'wait';
  508. $plan->stageBy = empty($project) ? 'product' : $project->stageBy;
  509. $plan->version = 1;
  510. $plan->parentVersion = $plan->parent == 0 ? 0 : $this->dao->findByID($plan->parent)->from(TABLE_PROJECT)->fetch('version');
  511. $plan->team = mb_substr($plan->name, 0, 30);
  512. $plan->openedBy = $account;
  513. $plan->openedDate = helper::now();
  514. $plan->openedVersion = $this->config->version;
  515. if(!isset($plan->acl)) $plan->acl = $this->dao->findByID($plan->parent)->from(TABLE_PROJECT)->fetch('acl');
  516. $this->dao->insert(TABLE_PROJECT)->data($plan, 'point,defaultPoint')->autoCheck()->exec();
  517. if(dao::isError()) return false;
  518. $stageID = (int)$this->dao->lastInsertID();
  519. $this->insertProjectSpec($stageID, $plan);
  520. /* Ipd project create default review points. */
  521. if($project && $project->model == 'ipd' && $this->config->edition == 'ipd' && !$parentID) $this->loadModel('review')->createDefaultPoint($project->id, $stageID, $plan->defaultPoint);
  522. if($plan->type == 'kanban')
  523. {
  524. $execution = $this->execution->getByID($stageID);
  525. $this->loadModel('kanban')->createRDKanban($execution);
  526. }
  527. $this->loadModel('execution');
  528. if($projectID) $this->execution->createMainLib($projectID, $stageID);
  529. $this->execution->addExecutionMembers($stageID, array($account, $plan->PM));
  530. $this->setTreePath($stageID);
  531. $this->computeProgress($stageID, 'create');
  532. /* Add the execution team members to parent executions. */
  533. $stagePath = $this->dao->findByID($stageID)->from(TABLE_PROJECT)->fetch('path');
  534. foreach(explode(',', $stagePath) as $stageParentID)
  535. {
  536. if(empty($stageParentID) || $stageParentID == $projectID) continue;
  537. $this->execution->addExecutionMembers((int)$stageParentID, array($plan->PM));
  538. }
  539. return $stageID;
  540. }
  541. /**
  542. * 根据任务列表,构建任务分组。
  543. * Build task group by assignedTo.
  544. *
  545. * @param array $task
  546. * @access protected
  547. * @return array
  548. * @param mixed[] $tasks
  549. */
  550. protected function buildTaskGroup($tasks)
  551. {
  552. $taskGroup = array();
  553. $multiTasks = array();
  554. foreach($tasks as $taskID => $task)
  555. {
  556. $taskGroup[$task->assignedTo][$taskID] = $task;
  557. if($task->mode == 'multi') $multiTasks[$taskID] = $task->assignedTo;
  558. }
  559. if(empty($multiTasks)) return $taskGroup;
  560. $taskTeams = $this->dao->select('t1.*,t2.realname')->from(TABLE_TASKTEAM)->alias('t1')
  561. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  562. ->where('t1.task')->in(array_keys($multiTasks))
  563. ->orderBy('t1.order')
  564. ->fetchGroup('task', 'id');
  565. foreach($taskTeams as $taskID => $team)
  566. {
  567. $assignedTo = $multiTasks[$taskID];
  568. foreach($team as $member)
  569. {
  570. $account = $member->account;
  571. if($account == $assignedTo) continue;
  572. if(!isset($taskGroup[$account])) $taskGroup[$account] = array();
  573. $taskGroup[$account][$taskID] = clone $tasks[$taskID];
  574. $taskGroup[$account][$taskID]->id = $taskID . '_' . $account;
  575. $taskGroup[$account][$taskID]->realID = $taskID;
  576. $taskGroup[$account][$taskID]->assignedTo = $account;
  577. $taskGroup[$account][$taskID]->realname = $member->realname;
  578. }
  579. }
  580. return $taskGroup;
  581. }
  582. /**
  583. * Build plan data for gantt.
  584. *
  585. * @param object $plan
  586. * @access protected
  587. * @return object
  588. */
  589. protected function buildPlanDataForGantt($plan)
  590. {
  591. $start = helper::isZeroDate($plan->begin) ? '' : $plan->begin;
  592. $end = helper::isZeroDate($plan->end) ? '' : $plan->end;
  593. $realBegan = helper::isZeroDate($plan->realBegan) ? '' : $plan->realBegan;
  594. $realEnd = helper::isZeroDate($plan->realEnd) ? '' : $plan->realEnd;
  595. $isMilestone = "<icon class='icon icon-flag icon-sm red'></icon> ";
  596. $data = new stdclass();
  597. $data->id = $plan->id;
  598. $data->type = 'plan';
  599. $data->executionType = $plan->type;
  600. $data->text = empty($plan->milestone) ? $plan->name : $plan->name . $isMilestone;
  601. $data->name = $plan->name;
  602. $data->attribute = zget($this->lang->stage->typeList, $plan->attribute);
  603. $data->milestone = zget($this->lang->programplan->milestoneList, $plan->milestone);
  604. $data->milestonecode = $plan->milestone;
  605. $data->owner_id = $plan->PM;
  606. $data->status = $this->processStatus('execution', $plan);
  607. $data->begin = $start;
  608. $data->deadline = $end;
  609. $data->realBegan = $realBegan ? substr($realBegan, 0, 10) : '';
  610. $data->realEnd = $realEnd ? substr($realEnd, 0, 10) : '';
  611. $data->parent = $plan->grade == 1 ? 0 :$plan->parent;
  612. $data->isParent = $plan->isParent;
  613. $data->isTpl = $plan->isTpl;
  614. $data->open = true;
  615. $data->start_date = $start;
  616. $data->endDate = $end;
  617. $data->duration = 1;
  618. $data->color = $this->lang->execution->gantt->stage->color;
  619. $data->progressColor = $this->lang->execution->gantt->stage->progressColor;
  620. $data->textColor = $this->lang->execution->gantt->stage->textColor;
  621. $data->bar_height = $this->lang->execution->gantt->bar_height;
  622. /* Set default progress from database. */
  623. $data->progress = $plan->progress / 100;
  624. $data->taskProgress = $plan->progress . '%';
  625. $data->frozen = $plan->frozen;
  626. if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1;
  627. if(empty($data->start_date) || empty($data->endDate)) $data->duration = 1;
  628. if(!empty($this->config->setPercent)) $data->percent = $plan->percent;
  629. if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
  630. return $data;
  631. }
  632. /**
  633. * Guild point data for gantt.
  634. *
  635. * @param int $planID
  636. * @param object $point
  637. * @param array $reviewDeadline
  638. * @access protected
  639. * @return object
  640. */
  641. protected function buildPointDataForGantt($planID, $point, $reviewDeadline)
  642. {
  643. $statusList = array();
  644. if(isset($this->lang->review->statusList)) $statusList = $this->lang->review->statusList;
  645. $end = $this->getPointEndDate($planID, $point, $reviewDeadline);
  646. $data = new stdclass();
  647. $data->id = $planID . '-point' . $point->category . '-' . $point->id;
  648. $data->reviewID = $point->reviewID;
  649. $data->type = 'point';
  650. $data->text = "<i class='icon-seal'></i> " . $point->title;
  651. $data->name = $point->title;
  652. $data->attribute = '';
  653. $data->milestone = '';
  654. $data->owner_id = '';
  655. $data->rawStatus = $point->status;
  656. $data->status = $point->status ? zget($statusList, $point->status) : $this->lang->programplan->wait;
  657. $data->status = "<span class='status-{$point->status}'>" . $data->status . '</span>';
  658. $data->begin = $end;
  659. $data->deadline = $end;
  660. $data->realBegan = $point->createdDate;
  661. $data->realEnd = $point->lastReviewedDate;;
  662. $data->parent = $planID;
  663. $data->open = true;
  664. $data->start_date = $end;
  665. $data->endDate = $end;
  666. $data->duration = 1;
  667. $data->color = isset($this->lang->programplan->reviewColorList[$point->status]) ? $this->lang->programplan->reviewColorList[$point->status] : '#FC913F';
  668. $data->progressColor = $this->lang->execution->gantt->stage->progressColor;
  669. $data->textColor = $this->lang->execution->gantt->stage->textColor;
  670. $data->bar_height = $this->lang->execution->gantt->bar_height;
  671. if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
  672. return $data;
  673. }
  674. /**
  675. * 构建甘特图的分组数据。
  676. * Build group data for gantt.
  677. *
  678. * @param int $groupID
  679. * @param string $group
  680. * @param array $users
  681. * @access protected
  682. * @return object
  683. */
  684. protected function buildGroupDataForGantt($groupID, $group, $users)
  685. {
  686. $this->app->loadLang('task');
  687. $groupName = $group;
  688. $groupName = $group != '/' ? zget($users, $group) : $this->lang->task->noAssigned;
  689. $dataGroup = new stdclass();
  690. $dataGroup->id = $groupID;
  691. $dataGroup->type = 'group';
  692. $dataGroup->text = $groupName;
  693. $dataGroup->percent = '';
  694. $dataGroup->attribute = '';
  695. $dataGroup->milestone = '';
  696. $dataGroup->owner_id = $group;
  697. $dataGroup->status = '';
  698. $dataGroup->begin = '';
  699. $dataGroup->deadline = '';
  700. $dataGroup->realBegan = '';
  701. $dataGroup->realEnd = '';
  702. $dataGroup->parent = 0;
  703. $dataGroup->open = true;
  704. $dataGroup->progress = '';
  705. $dataGroup->taskProgress = '';
  706. $dataGroup->color = $this->lang->execution->gantt->stage->color;
  707. $dataGroup->progressColor = $this->lang->execution->gantt->stage->progressColor;
  708. $dataGroup->textColor = $this->lang->execution->gantt->stage->textColor;
  709. $dataGroup->bar_height = $this->lang->execution->gantt->bar_height;
  710. return $dataGroup;
  711. }
  712. /**
  713. * 构建甘特图的任务数据。
  714. * Build task data for gantt.
  715. *
  716. * @param int $groupID
  717. * @param object $task
  718. * @param array $dateLimit array('start' => $start, 'end' => $end, 'realBegan' => $realBegan, 'realEnd' => $realEnd);
  719. * @param array $tasksMap
  720. * @access protected
  721. * @return object
  722. */
  723. protected function buildTaskDataForGantt($task, $dateLimit, $groupID = 0, $tasksMap = array())
  724. {
  725. $taskPri = "<span class='pri-%s align-middle' title='%s'>%s</span> ";
  726. $pri = zget($this->lang->task->priList, $task->pri);
  727. $priIcon = sprintf($taskPri, $task->pri, $pri, $pri);
  728. $total = $task->left + $task->consumed;
  729. $progress = $total > 0 ? round($task->consumed / $total, 3) : 0;
  730. $data = new stdclass();
  731. $data->id = $task->id;
  732. $data->type = 'task';
  733. $data->text = $priIcon . "<span class='gantt_title'>{$task->name}</span>";
  734. $data->percent = '';
  735. $data->status = $task->status == 'changed' ? $this->lang->task->storyChange : $this->processStatus('task', $task);
  736. $data->owner_id = $task->assignedTo;
  737. $data->attribute = '';
  738. $data->milestone = '';
  739. $data->begin = substr($dateLimit['start'], 0, 10);
  740. $data->deadline = substr($dateLimit['end'], 0, 10);
  741. $data->realBegan = $dateLimit['realBegan'] ? substr($dateLimit['realBegan'], 0, 10) : '';
  742. $data->realEnd = $dateLimit['realEnd'] ? substr($dateLimit['realEnd'], 0, 10) : '';
  743. $data->pri = $task->pri;
  744. $data->parent = ($task->parent > 0 and $task->assignedTo != '' and !empty($tasksMap[$task->parent]->assignedTo)) ? $task->parent : $groupID;
  745. $data->open = true;
  746. $data->progress = $progress;
  747. $data->taskProgress = ($progress * 100) . '%';
  748. $data->start_date = $dateLimit['start'];
  749. $data->endDate = $dateLimit['end'];
  750. $data->duration = 1;
  751. $data->estimate = $task->estimate;
  752. $data->consumed = $task->consumed;
  753. $data->color = zget($this->lang->execution->gantt->color, $task->pri, $this->lang->execution->gantt->defaultColor);
  754. $data->progressColor = zget($this->lang->execution->gantt->progressColor, $task->pri, $this->lang->execution->gantt->defaultProgressColor);
  755. $data->textColor = zget($this->lang->execution->gantt->textColor, $task->pri, $this->lang->execution->gantt->defaultTextColor);
  756. $data->bar_height = $this->lang->execution->gantt->bar_height;
  757. if($data->endDate > $data->start_date) $data->duration = helper::diffDate($data->endDate, $data->start_date) + 1;
  758. if(empty($data->start_date) or empty($data->endDate)) $data->duration = 1;
  759. if($data->start_date) $data->start_date = date('d-m-Y', strtotime($data->start_date));
  760. return $data;
  761. }
  762. /**
  763. * 构建甘特图的关系链接
  764. * Build gantt links.
  765. *
  766. * @param int $projectID
  767. * @param array $tasks
  768. * @access protected
  769. * @return array
  770. */
  771. protected function buildGanttLinks($projectID, $tasks = array())
  772. {
  773. $this->app->loadConfig('execution');
  774. if($this->config->edition == 'open') return array();
  775. $links = array();
  776. $relations = $this->dao->select('*')->from(TABLE_RELATIONOFTASKS)->where('project')->eq($projectID)->orderBy('task,pretask')->fetchAll();
  777. foreach($relations as $relation)
  778. {
  779. $link = array();
  780. $link['id'] = $relation->id;
  781. $link['source'] = !empty($tasks[$relation->pretask]->id) ? $tasks[$relation->pretask]->id : $relation->pretask;
  782. $link['target'] = !empty($tasks[$relation->task]->id) ? $tasks[$relation->task]->id : $relation->task;
  783. $link['type'] = $this->config->execution->gantt->linkType[$relation->condition][$relation->action];
  784. $links[] = $link;
  785. }
  786. return $links;
  787. }
  788. /**
  789. * 设置阶段统计数据。
  790. * Set stage summary.
  791. *
  792. * @param array $ganttData
  793. * @param array $stages
  794. * @access protected
  795. * @return array
  796. */
  797. protected function setStageSummary($ganttData, $stages)
  798. {
  799. foreach($stages as $index => $stage)
  800. {
  801. if(!isset($ganttData['data'][$index])) continue;
  802. $progress = empty($stage['totalReal']) ? 0 : round($stage['totalConsumed'] / $stage['totalReal'], 3);
  803. $ganttData['data'][$index]->progress = $progress;
  804. $ganttData['data'][$index]->taskProgress = ($progress * 100) . '%';
  805. $ganttData['data'][$index]->estimate = $stage['totalEstimate'];
  806. $ganttData['data'][$index]->consumed = $stage['totalConsumed'];
  807. }
  808. return $ganttData;
  809. }
  810. /**
  811. * 获取任务任务限制。获取该任务在甘特图中的开始,结束时间。
  812. * Get task date limit.
  813. *
  814. * @param object $task
  815. * @param object|null $execution
  816. * @param object|null $parent
  817. * @access protected
  818. * @return array
  819. */
  820. protected function getTaskDateLimit($task, $execution = null, $parent = null)
  821. {
  822. $estStart = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted;
  823. $estEnd = helper::isZeroDate($task->deadline) ? '' : $task->deadline;
  824. $realBegan = helper::isZeroDate($task->realStarted) ? '' : $task->realStarted;
  825. $realEnd = (in_array($task->status, array('done', 'closed')) and !helper::isZeroDate($task->finishedDate)) ? $task->finishedDate : '';
  826. if($parent)
  827. {
  828. if(empty($estStart) && !helper::isZeroDate($parent->estStarted)) $estStart = $parent->estStarted;
  829. if(empty($estEnd) && !helper::isZeroDate($parent->deadline)) $estEnd = $parent->deadline;
  830. }
  831. $start = $estStart;
  832. $end = $estEnd;
  833. if(empty($start) and $execution) $start = $execution->begin;
  834. if(empty($end) and $execution) $end = $execution->end;
  835. if($start > $end) $end = $start;
  836. return array('start' => $start, 'end' => $end, 'realBegan' => $realBegan, 'realEnd' => $realEnd);
  837. }
  838. /**
  839. * Get five days ago.
  840. *
  841. * @param string $date
  842. * @param int $date
  843. * @access public
  844. * @return string
  845. * @param int $counter
  846. */
  847. public function getReviewDeadline($date, $counter = 5)
  848. {
  849. if(helper::isZeroDate($date)) return '';
  850. $this->loadModel('holiday');
  851. $weekendDays = array(6, 7);
  852. $timestamp = strtotime($date);
  853. $index = 0;
  854. while($index < $counter)
  855. {
  856. $timestamp -= 24 * 3600;
  857. $weekday = date('N', $timestamp);
  858. $currentDate = date('Y-m-d', $timestamp);
  859. if(!in_array($weekday, $weekendDays) and !$this->holiday->isHoliday($currentDate)) $index ++;
  860. }
  861. return date('Y-m-d', $timestamp);
  862. }
  863. /**
  864. * 获取基线数据。
  865. * Get baseline data.
  866. *
  867. * @param array $oldPlans
  868. * @param array $plans
  869. * @access protected
  870. * @return array
  871. */
  872. protected function setPlanBaseline($oldPlans, $plans)
  873. {
  874. foreach($oldPlans as $id => $oldPlan)
  875. {
  876. if(!isset($plans[$id])) continue;
  877. $plans[$id]->version = $oldPlan->version;
  878. $plans[$id]->name = $oldPlan->name;
  879. $plans[$id]->milestone = $oldPlan->milestone;
  880. $plans[$id]->begin = $oldPlan->begin;
  881. $plans[$id]->end = $oldPlan->end;
  882. }
  883. return $plans;
  884. }
  885. /**
  886. * 将父阶段数据转移到第一个子阶段中。
  887. * Sync parent data to first stage.
  888. *
  889. * @param int $executionID
  890. * @param int $parentID
  891. * @access protected
  892. * @return bool
  893. */
  894. protected function syncParentData($executionID, $parentID)
  895. {
  896. if(empty($executionID) || empty($parentID)) return false;
  897. $this->dao->update(TABLE_TASK)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  898. $this->dao->update(TABLE_EFFORT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  899. $this->dao->update(TABLE_BUG)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  900. $this->dao->update(TABLE_CASE)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  901. $this->dao->update(TABLE_BUILD)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  902. $this->dao->update(TABLE_TESTTASK)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  903. $this->dao->update(TABLE_TESTREPORT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('deleted')->eq('0')->exec();
  904. $this->dao->update(TABLE_ACTION)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('objectType')->ne('execution')->exec();
  905. $this->dao->update(TABLE_ACTIONRECENT)->set('execution')->eq($executionID)->where('execution')->eq($parentID)->andWhere('objectType')->ne('execution')->exec();
  906. $this->dao->update(TABLE_DOCLIB)->set('execution')->eq($executionID)->where('type')->eq('execution')->andWhere('execution')->eq($parentID)->andWhere('main')->eq('0')->andWhere('deleted')->eq('0')->exec();
  907. $storyIdSQL = $this->dao->select('id')->from(TABLE_STORY)->where('deleted')->eq('0')->get();
  908. $this->dao->update(TABLE_PROJECTSTORY)->set('project')->eq($executionID)->where('project')->eq($parentID)->andWhere('story')->subIn($storyIdSQL)->exec();
  909. $caseIdSQL = $this->dao->select('id')->from(TABLE_CASE)->where('deleted')->eq('0')->get();
  910. $this->dao->update(TABLE_PROJECTCASE)->set('project')->eq($executionID)->where('project')->eq($parentID)->andWhere('case')->subIn($caseIdSQL)->exec();
  911. /* Update doc in the main doc lib. */
  912. $parentLibID = $this->dao->select('id')->from(TABLE_DOCLIB)->where('type')->eq('execution')->andWhere('execution')->eq($parentID)->andWhere('main')->eq('1')->limit(1)->fetch('id');
  913. $libID = $this->dao->select('id')->from(TABLE_DOCLIB)->where('type')->eq('execution')->andWhere('execution')->eq($executionID)->andWhere('main')->eq('1')->limit(1)->fetch('id');
  914. if($parentLibID && $libID) $this->dao->update(TABLE_DOC)->set('execution')->eq($executionID)->set('lib')->eq($libID)->where('lib')->eq($parentLibID)->andWhere('deleted')->eq('0')->exec();
  915. /* Update task and doc module. */
  916. $this->dao->update(TABLE_MODULE)->set('root')->eq($executionID)->where('root')->eq($parentID)->andWhere('type')->eq('task')->andWhere('deleted')->eq('0')->exec();
  917. $this->dao->update(TABLE_MODULE)->set('root')->eq($libID)->where('root')->eq($parentLibID)->andWhere('type')->eq('doc')->andWhere('deleted')->eq('0')->exec();
  918. /* Update stage info. */
  919. $parent = $this->fetchByID($parentID);
  920. $data = new stdclass();
  921. $data->status = $parent->status;
  922. $data->progress = $parent->progress;
  923. $data->estimate = $parent->estimate;
  924. $data->left = $parent->left;
  925. $data->consumed = $parent->consumed;
  926. $this->dao->update(TABLE_EXECUTION)->data($data)->where('id')->eq($executionID)->exec();
  927. return !dao::isError();
  928. }
  929. }