tao.php 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203
  1. <?php
  2. /**
  3. * The tao file of project 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 sunguangming <sunguangming@easycorp.ltd>
  8. * @link https://www.zentao.net
  9. */
  10. class projectTao extends projectModel
  11. {
  12. /**
  13. * Update project table when start a project.
  14. *
  15. * @param int $projectID
  16. * @param object $project
  17. * @access protected
  18. * @return bool
  19. */
  20. protected function doStart($projectID, $project)
  21. {
  22. $this->dao->update(TABLE_PROJECT)->data($project, 'comment')
  23. ->autoCheck()
  24. ->check($this->config->project->start->requiredFields, 'notempty')
  25. ->checkIF($project->realBegan != '', 'realBegan', 'le', helper::today())
  26. ->checkFlow()
  27. ->where('id')->eq($projectID)
  28. ->exec();
  29. return !dao::isError();
  30. }
  31. /**
  32. * Update project table when suspend a project.
  33. *
  34. * @param int $projectID
  35. * @param object $project
  36. *
  37. * @access protected
  38. * @return bool
  39. */
  40. protected function doSuspend($projectID, $project)
  41. {
  42. $this->dao->update(TABLE_PROJECT)->data($project)
  43. ->autoCheck()
  44. ->checkFlow()
  45. ->where('id')->eq($projectID)
  46. ->exec();
  47. return !dao::isError();
  48. }
  49. /**
  50. * Update project table when close a project.
  51. *
  52. * @param int $projectID
  53. * @param object $project
  54. * @param object $oldProject
  55. *
  56. * @access protected
  57. * @return bool
  58. */
  59. protected function doClosed($projectID, $project, $oldProject)
  60. {
  61. $this->lang->error->ge = $this->lang->project->ge;
  62. $this->dao->update(TABLE_PROJECT)->data($project)
  63. ->autoCheck()
  64. ->check($this->config->project->close->requiredFields, 'notempty')
  65. ->checkIF($project->realEnd != '', 'realEnd', 'le', helper::today())
  66. ->checkIF($project->realEnd != '', 'realEnd', 'ge', $oldProject->realBegan)
  67. ->checkFlow()
  68. ->where('id')->eq($projectID)
  69. ->exec();
  70. return !dao::isError();
  71. }
  72. /**
  73. * 激活项目时更新项目的信息。
  74. * Update project table when activate a project.
  75. *
  76. * @param int $projectID
  77. * @param object $project
  78. * @access protected
  79. * @return bool
  80. */
  81. protected function doActivate($projectID ,$project)
  82. {
  83. $this->dao->update(TABLE_PROJECT)->data($project , 'readjustTime, readjustTask, comment')
  84. ->autoCheck()
  85. ->checkFlow()
  86. ->where('id')->eq((int)$projectID)
  87. ->exec();
  88. return !dao::isError();
  89. }
  90. /**
  91. * 更新项目主表。
  92. * Update project table when edit a project.
  93. *
  94. * @param int $projectID
  95. * @param object $project
  96. * @access protected
  97. * @return bool
  98. */
  99. public function doUpdate($projectID, $project)
  100. {
  101. $project->id = $projectID;
  102. $this->dao->update(TABLE_PROJECT)->data($project)
  103. ->autoCheck('begin,end')
  104. ->check('end', 'gt', $project->begin)
  105. ->checkIF(!empty($project->name), 'name', 'unique', "id != $projectID and `type` = 'project' and `parent` = '$project->parent' and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
  106. ->checkIF(!empty($project->code), 'code', 'unique', "id != $projectID and `type` = 'project' and `parent` = '$project->parent' and `deleted` = '0'")
  107. ->checkFlow()
  108. ->where('id')->eq($projectID)
  109. ->exec();
  110. return !dao::isError();
  111. }
  112. /**
  113. * 新增项目。
  114. * Insert a project to project table.
  115. *
  116. * @param object $project
  117. * @access protected
  118. * @return bool
  119. */
  120. protected function doCreate($project)
  121. {
  122. $this->lang->error->unique = $this->lang->error->repeat;
  123. $this->dao->insert(TABLE_PROJECT)->data($project)
  124. ->autoCheck()
  125. ->batchCheck($this->config->project->create->requiredFields, 'notempty')
  126. ->checkIF(!empty($project->name), 'name', 'unique', "`type`='project' and `parent` = " . $this->dao->sqlobj->quote($project->parent) . " and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
  127. ->checkIF(!empty($project->code), 'code', 'unique', "`type`='project' and `parent` = " . $this->dao->sqlobj->quote($project->parent) . " and `model` = " . $this->dao->sqlobj->quote($project->model) . " and `deleted` = '0'")
  128. ->checkIF($project->end != '', 'end', 'gt', $project->begin)
  129. ->checkFlow()
  130. ->exec();
  131. return !dao::isError();
  132. }
  133. /**
  134. * 获取未完成的任务。
  135. * Fetch undone tasks.
  136. *
  137. * @param int $projectID
  138. * @access protected
  139. * @return object[]
  140. */
  141. protected function fetchUndoneTasks($projectID)
  142. {
  143. return $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK)
  144. ->where('deadline')->notZeroDate()
  145. ->andWhere('status')->in('wait,doing')
  146. ->andWhere('project')->eq($projectID)
  147. ->fetchAll();
  148. }
  149. /**
  150. * 更新任务的起止日期。
  151. * Update start and end date of tasks.
  152. *
  153. * @param array $tasks
  154. * @param object $oldProject
  155. * @param object $project
  156. * @access protected
  157. * @return bool
  158. */
  159. protected function updateTasksStartAndEndDate($tasks, $oldProject, $project)
  160. {
  161. $beginTimeStamp = strtotime($project->begin);
  162. foreach($tasks as $task)
  163. {
  164. if($task->status == 'wait' && !helper::isZeroDate($task->estStarted))
  165. {
  166. $taskDays = helper::diffDate($task->deadline, $task->estStarted);
  167. $taskOffset = helper::diffDate($task->estStarted, $oldProject->begin);
  168. $estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600;
  169. $estStarted = date('Y-m-d', $estStartedTimeStamp);
  170. $deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600);
  171. if($estStarted > $project->end) $estStarted = $project->end;
  172. if($deadline > $project->end) $deadline = $project->end;
  173. $this->dao->update(TABLE_TASK)
  174. ->set('estStarted')->eq($estStarted)
  175. ->set('deadline')->eq($deadline)
  176. ->where('id')->eq($task->id)
  177. ->exec();
  178. if(dao::isError()) return false;
  179. }
  180. else
  181. {
  182. $taskOffset = helper::diffDate($task->deadline, $oldProject->begin);
  183. $deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600);
  184. if($deadline > $project->end) $deadline = $project->end;
  185. $this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec();
  186. if(dao::isError()) return false;
  187. }
  188. }
  189. return true;
  190. }
  191. /**
  192. * 插入项目团队成员。
  193. * Insert project members.
  194. *
  195. * @param array $members
  196. * @param int $projectID
  197. * @param array $oldJoin
  198. * @access protected
  199. * @return array
  200. */
  201. protected function insertMember($members, $projectID, $oldJoin)
  202. {
  203. $accounts = array();
  204. foreach($members as $member)
  205. {
  206. if(empty($member->account)) continue;
  207. $account = $member->account;
  208. $accounts[] = $account;
  209. $data = new stdclass();
  210. $data->role = $member->role;
  211. $data->days = $member->days;
  212. $data->hours = $member->hours;
  213. $data->limited = isset($member->limited) ? $member->limited : 'no';
  214. $data->root = $projectID;
  215. $data->account = $account;
  216. $data->join = isset($oldJoin[$account]) ? $oldJoin[$account] : helper::today();
  217. $data->type = 'project';
  218. $this->dao->insert(TABLE_TEAM)->data($data)->exec();
  219. }
  220. return $accounts;
  221. }
  222. /**
  223. * 更新项目团队成员的视图权限
  224. * Update member view when project member changed.
  225. *
  226. * @param int $projectID
  227. * @param array $accounts
  228. * @param array $oldJoin
  229. * @access protected
  230. * @return void
  231. */
  232. protected function updateMemberView($projectID, $accounts, $oldJoin)
  233. {
  234. /* Only changed account update userview. */
  235. $oldAccounts = array_keys($oldJoin);
  236. $removedAccounts = array_diff($oldAccounts, $accounts);
  237. $addedAccounts = array_diff($accounts, $oldAccounts);
  238. $changedAccounts = array_merge($removedAccounts, $addedAccounts);
  239. $changedAccounts = array_unique($changedAccounts);
  240. $childSprints = $this->dao->select('id')->from(TABLE_PROJECT)
  241. ->where('project')->eq($projectID)
  242. ->andWhere('type')->in('stage,sprint,kanban')
  243. ->andWhere('deleted')->eq('0')
  244. ->fetchPairs();
  245. $linkedProducts = $this->dao->select("t2.id")->from(TABLE_PROJECTPRODUCT)->alias('t1')
  246. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
  247. ->where('t2.deleted')->eq(0)
  248. ->andWhere('t1.project')->eq($projectID)
  249. ->andWhere('t2.vision')->eq($this->config->vision)
  250. ->fetchPairs();
  251. $this->loadModel('user')->updateUserView(array($projectID), 'project', $changedAccounts);
  252. if(!empty($childSprints)) $this->user->updateUserView($childSprints, 'sprint', $changedAccounts);
  253. if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccounts);
  254. if(empty($addedAccounts) && empty($removedAccounts)) return;
  255. /* Log history. */
  256. $actionID = $this->loadModel('action')->create('project', $projectID, 'ManagedTeam');
  257. $users = $this->loadModel('user')->getPairs('noletter');
  258. $addedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $addedAccounts);
  259. $removedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $removedAccounts);
  260. if(!empty($addedAccountList)) $changes[] = array('field' => 'addDiff', 'old' => '', 'new' => '', 'diff' => join(',', $addedAccountList));
  261. if(!empty($removedAccountList)) $changes[] = array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => join(',', $removedAccountList));
  262. if(!empty($changes)) $this->action->logHistory($actionID, $changes);
  263. /* Remove execution members. */
  264. if($this->post->removeExecution == 'yes' and !empty($childSprints) and !empty($removedAccountList))
  265. {
  266. $childTeams = $this->dao->select('root,account')->from(TABLE_TEAM)
  267. ->where('root')->in($childSprints)
  268. ->andWhere('type')->eq('execution')
  269. ->andWhere('account')->in($removedAccounts)
  270. ->fetchGroup('root', 'account');
  271. $this->dao->delete()->from(TABLE_TEAM)
  272. ->where('root')->in($childSprints)
  273. ->andWhere('type')->eq('execution')
  274. ->andWhere('account')->in($removedAccounts)
  275. ->exec();
  276. foreach($childTeams as $sprintID => $childTeam)
  277. {
  278. $childAccounts = array_keys($childTeam);
  279. $removedAccountList = array_intersect($childAccounts, $removedAccounts);
  280. if(empty($removedAccountList)) continue;
  281. $removedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $removedAccountList);
  282. $changes = array(array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => join(',', $removedAccountList)));
  283. $actionID = $this->action->create('execution', $sprintID, 'syncProjectTeam');
  284. $this->action->logHistory($actionID, $changes);
  285. }
  286. }
  287. }
  288. /**
  289. * 获取项目的详情,包含project表的所有内容。
  290. * Get project details, including all contents of the project.
  291. *
  292. * @param int $projectID
  293. * @param string $type
  294. * @access protected
  295. * @return object|false
  296. */
  297. protected function fetchProjectInfo($projectID, $type = '')
  298. {
  299. $project = $this->dao->select('*')
  300. ->from(TABLE_PROJECT)
  301. ->where('id')->eq($projectID)
  302. ->beginIF(!empty($type))->andWhere('`type`')->in($type)->fi()
  303. ->fetch();
  304. /* Filter the date is empty or 1970. */
  305. if($project && helper::isZeroDate($project->end)) $project->end = '';
  306. return $project;
  307. }
  308. /**
  309. * 创建项目后,创建默认的项目主库。
  310. * Create doclib after create a project.
  311. *
  312. * @param int $projectID
  313. * @param object $project
  314. * @param object $program
  315. * @access protected
  316. * @return bool
  317. */
  318. protected function createDocLib($projectID, $project, $program)
  319. {
  320. /* Create doc lib. */
  321. $this->app->loadLang('doc');
  322. $authorizedUsers = array();
  323. if($project->parent && $project->acl == 'program')
  324. {
  325. $stakeHolders = $this->loadModel('stakeholder')->getStakeHolderPairs($project->parent);
  326. $authorizedUsers = array_keys($stakeHolders);
  327. foreach(explode(',', $project->whitelist) as $user)
  328. {
  329. if(empty($user)) continue;
  330. $authorizedUsers[$user] = $user;
  331. }
  332. $authorizedUsers[$project->PM] = $project->PM;
  333. $authorizedUsers[$project->openedBy] = $project->openedBy;
  334. $authorizedUsers[$program->PM] = $program->PM;
  335. $authorizedUsers[$program->openedBy] = $program->openedBy;
  336. }
  337. $lib = new stdclass();
  338. $lib->project = $projectID;
  339. $lib->name = $this->lang->doclib->main['project'];
  340. $lib->type = 'project';
  341. $lib->main = '1';
  342. $lib->acl = 'default';
  343. $lib->users = ',' . implode(',', array_filter($authorizedUsers)) . ',';
  344. $lib->vision = zget($project, 'vision', 'rnd');
  345. $lib->addedBy = $this->app->user->account;
  346. $lib->addedDate = helper::now();
  347. $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
  348. return !dao::isError();
  349. }
  350. /**
  351. * 创建项目时,如果直接输入了产品名,则创建产品并与项目关联。
  352. * Create doclib after create a project.
  353. *
  354. * @param int $projectID
  355. * @param object $project
  356. * @param object $postData
  357. * @param object $program
  358. * @access protected
  359. * @return bool
  360. */
  361. protected function createProduct($projectID, $project, $postData, $program)
  362. {
  363. /* If parent not empty, link products or create products. */
  364. $product = new stdclass();
  365. $product->name = $project->hasProduct && !empty($postData->rawdata->productName) ? $postData->rawdata->productName : zget($project, 'name', '');
  366. $product->shadow = zget($project, 'vision', 'rnd') == 'rnd' ? (int)empty($project->hasProduct) : 1;
  367. $product->bind = $postData->rawdata->parent ? 0 : 1;
  368. $product->program = $project->parent ? current(array_filter(explode(',', zget($program, 'path', '')))) : 0;
  369. $product->acl = $project->acl == 'open' ? 'open' : 'private';
  370. $product->PO = $project->PM;
  371. $product->QD = '';
  372. $product->RD = '';
  373. $product->whitelist = '';
  374. $product->createdBy = $this->app->user->account;
  375. $product->createdDate = helper::now();
  376. $product->status = 'normal';
  377. $product->line = 0;
  378. $product->desc = '';
  379. $product->createdVersion = $this->config->version;
  380. $product->vision = zget($project, 'vision', 'rnd');
  381. if($this->config->edition != 'open') $product->workflowGroup = $this->dao->select('id')->from(TABLE_WORKFLOWGROUP)->where('code')->eq('productproject')->fetch('id');
  382. $this->app->loadLang('product');
  383. $this->dao->insert(TABLE_PRODUCT)->data($product)
  384. ->check('name', 'notempty')
  385. ->checkIF(!empty($product->name), 'name', 'unique', "`program` = {$product->program} and `deleted` = '0'")
  386. ->exec();
  387. if(dao::isError()) return false;
  388. $productID = $this->dao->lastInsertId();
  389. /* Update whitelist and user view. */
  390. if(!$project->hasProduct) $this->loadModel('personnel')->updateWhitelist(explode(',', $project->whitelist), 'product', $productID);
  391. $this->loadModel('action')->create('product', $productID, 'opened');
  392. $this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($productID * 5)->where('id')->eq($productID)->exec();
  393. if($product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
  394. $productSettingList = isset($this->config->global->productSettingList) ? json_decode($this->config->global->productSettingList, true) : array();
  395. if($this->config->edition != 'open' && $productSettingList)
  396. {
  397. $productSettingList[] = $productID;
  398. $this->loadModel('setting')->setItem('system.common.global.productSettingList', json_encode($productSettingList));
  399. }
  400. /* Link product. */
  401. $projectProduct = new stdclass();
  402. $projectProduct->project = $projectID;
  403. $projectProduct->product = $productID;
  404. $projectProduct->branch = 0;
  405. $projectProduct->plan = 0;
  406. $this->dao->insert(TABLE_PROJECTPRODUCT)->data($projectProduct)->exec();
  407. if(dao::isError()) return false;
  408. /* Create doc lib. */
  409. if($project->hasProduct) $this->createProductDocLib($productID);
  410. return !dao::isError();
  411. }
  412. /**
  413. * 创建产品后,创建默认的产品主库。
  414. * Create doclib after create a product.
  415. *
  416. * @param int $productID
  417. * @access protected
  418. * @return bool
  419. */
  420. protected function createProductDocLib($productID)
  421. {
  422. $this->app->loadLang('doc');
  423. $lib = new stdclass();
  424. $lib->product = $productID;
  425. $lib->name = $this->lang->doclib->main['product'];
  426. $lib->type = 'product';
  427. $lib->main = '1';
  428. $lib->acl = 'default';
  429. $lib->addedBy = $this->app->user->account;
  430. $lib->addedDate = helper::now();
  431. $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
  432. return !dao::isError();
  433. }
  434. /**
  435. * 获取创建项目时选择的产品数量。
  436. * Get products count from post.
  437. *
  438. * @param object $project
  439. * @param object $rawdata
  440. * @access protected
  441. * @return int
  442. */
  443. protected function getLinkedProductsCount($project, $rawdata)
  444. {
  445. if(!isset($project) || !empty($project->newProject)) return 0;
  446. $linkedProductsCount = 0;
  447. if($project->hasProduct && isset($rawdata->products))
  448. {
  449. foreach($rawdata->products as $product)
  450. {
  451. if(empty($product)) continue;
  452. $linkedProductsCount ++;
  453. }
  454. }
  455. return $linkedProductsCount;
  456. }
  457. /**
  458. * 创建项目后,将项目创建者加到项目管理员分组。
  459. * Create project admin after create a project.
  460. *
  461. * @param int $projectID
  462. * @access protected
  463. * @return bool
  464. */
  465. protected function addProjectAdmin($projectID)
  466. {
  467. $projectAdmin = $this->dao->select('*')->from(TABLE_PROJECTADMIN)->where('account')->eq($this->app->user->account)->fetch();
  468. if(!empty($projectAdmin) && $projectAdmin->projects != 'all')
  469. {
  470. $newProject = $projectAdmin->projects . ",$projectID";
  471. $this->dao->update(TABLE_PROJECTADMIN)->set('projects')->eq($newProject)->where('account')->eq($projectAdmin->account)->andWhere('`group`')->eq($projectAdmin->group)->exec();
  472. }
  473. else
  474. {
  475. $maxGroupID = $this->dao->select('max(`group`) as maxGroupID')->from(TABLE_PROJECTADMIN)->fetch('maxGroupID');
  476. $projectAdmin = new stdclass();
  477. $projectAdmin->account = $this->app->user->account;
  478. $projectAdmin->group = (int)$maxGroupID + 1;
  479. $projectAdmin->projects = $projectID;
  480. $this->dao->replace(TABLE_PROJECTADMIN)->data($projectAdmin)->exec();
  481. }
  482. return !dao::isError();
  483. }
  484. /**
  485. * 生成project模块的项目下拉框跳转链接.
  486. * Build project link in project page.
  487. *
  488. * @param string $method
  489. * @access protected
  490. * @return string
  491. */
  492. protected function buildLinkForProject($method)
  493. {
  494. if($method == 'execution')
  495. return helper::createLink($module, $method, "status=all&projectID=%s");
  496. if($method == 'managePriv')
  497. return helper::createLink($module, 'group', "projectID=%s");
  498. if($method == 'showerrornone')
  499. return helper::createLink('projectstory', 'story', "projectID=%s");
  500. $methods = ',bug,testcase,testtask,testreport,build,dynamic,view,manageproducts,team,managemembers,whitelist,addwhitelist,group,';
  501. if(strpos($methods, ',' . $method . ',') !== false)
  502. return helper::createLink($module, $method, "projectID=%s");
  503. }
  504. /**
  505. * 生成bug模块的项目下拉框跳转链接.
  506. * Build project link in bug page.
  507. *
  508. * @param string $method
  509. * @access protected
  510. * @return string
  511. */
  512. protected function buildLinkForBug($method)
  513. {
  514. if($method == 'create')
  515. return helper::createLink($module, $method, "productID=0&branch=0&extras=projectID=%s");
  516. if($method == 'edit')
  517. return helper::createLink('project', 'bug', "projectID=%s");
  518. }
  519. /**
  520. * 生成story模块的项目下拉框跳转链接.
  521. * Build project link in story page.
  522. *
  523. * @param string $method
  524. * @access protected
  525. * @return string
  526. */
  527. protected function buildLinkForStory($method)
  528. {
  529. if($method == 'change' || $method == 'create')
  530. return helper::createLink('projectstory', 'story', "projectID=%s");
  531. if($method == 'zerocase')
  532. return helper::createLink('project', 'testcase', "projectID=%s");
  533. }
  534. /**
  535. * 构造批量更新项目的数据。
  536. * Build bathc update project data.
  537. *
  538. * @param array $data
  539. * @param array $oldProjects
  540. * @access public
  541. * @return array
  542. */
  543. public function buildBatchUpdateProjects($data, $oldProjects)
  544. {
  545. if(empty($data)) return array();
  546. $extendFields = $this->getFlowExtendFields();
  547. $projects = array();
  548. foreach($data as $projectID => $project)
  549. {
  550. $projectID = (int)$projectID;
  551. $projects[$projectID] = new stdClass();
  552. $projects[$projectID]->id = $projectID;
  553. $projects[$projectID]->name = $project->name;
  554. $projects[$projectID]->model = $oldProjects[$projectID]->model;
  555. $projects[$projectID]->PM = $project->PM;
  556. $projects[$projectID]->begin = $project->begin;
  557. $projects[$projectID]->end = $project->end == $this->lang->project->longTime ? LONG_TIME : $project->end;
  558. $projects[$projectID]->acl = $project->acl;
  559. $projects[$projectID]->days = $project->days;
  560. $projects[$projectID]->lastEditedBy = $this->app->user->account;
  561. $projects[$projectID]->lastEditedDate = helper::now();
  562. if(isset($project->parent)) $projects[$projectID]->parent = $project->parent;
  563. if(isset($project->code)) $projects[$projectID]->code = $project->code;
  564. if($project->end == $this->lang->project->longTime) $projects[$projectID]->days = 0;
  565. foreach($extendFields as $extendField)
  566. {
  567. $projects[$projectID]->{$extendField->field} = $project->{$extendField->field};
  568. if(is_array($projects[$projectID]->{$extendField->field})) $projects[$projectID]->{$extendField->field} = implode(',', $projects[$projectID]->{$extendField->field});
  569. $projects[$projectID]->{$extendField->field} = htmlSpecialString($projects[$projectID]->{$extendField->field});
  570. }
  571. }
  572. return $projects;
  573. }
  574. /**
  575. * 删除项目团队成员。
  576. * Delete project team member.
  577. *
  578. * @param array|int $projectIdList
  579. * @param string $type
  580. * @param string $account
  581. * @param string $realname
  582. * @param array $changes
  583. * @access protected
  584. * @return bool
  585. */
  586. protected function unlinkTeamMember($projectIdList, $type, $account, $realname, $changes)
  587. {
  588. $accountProjects = $this->dao->select('root')->from(TABLE_TEAM)
  589. ->where('root')->in($projectIdList)
  590. ->andWhere('type')->eq($type)
  591. ->andWhere('account')->eq($account)
  592. ->fetchPairs();
  593. $this->dao->delete()->from(TABLE_TEAM)
  594. ->where('root')->in($projectIdList)
  595. ->andWhere('type')->eq($type)
  596. ->andWhere('account')->eq($account)
  597. ->exec();
  598. $this->loadModel('action');
  599. foreach($accountProjects as $projectID)
  600. {
  601. $actionID = $this->action->create($type, $projectID, $type == 'project' ? 'managedTeam' : 'syncProjectTeam');
  602. $this->action->logHistory($actionID, $changes);
  603. }
  604. return !dao::isError();
  605. }
  606. /**
  607. * 对项目按照我的、其它的和关闭的进行分类。
  608. * Classify projects according to my, other and closed.
  609. *
  610. * @param array $projects
  611. * @access protected
  612. * @return array
  613. */
  614. protected function classifyProjects($projects)
  615. {
  616. $classifiedProjects = array('myProjects' => array(), 'otherProjects' => array(), 'closedProjects' => array());
  617. foreach($projects as $project)
  618. {
  619. if(strpos('wait,doing,closed', $project->status) === false) continue;
  620. /* Convert predefined HTML entities to characters. */
  621. $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES);
  622. $projectPath = explode(',', trim($project->path, ','));
  623. $topProgram = !empty($project->parent) ? $projectPath[0] : $project->parent;
  624. if(!helper::hasFeature('program')) $topProgram = 0;
  625. if($project->PM == $this->app->user->account)
  626. {
  627. if($project->status != 'closed')
  628. {
  629. $classifiedProjects['myProjects'][$topProgram][$project->status][] = $project;
  630. }
  631. else
  632. {
  633. $classifiedProjects['closedProjects']['my'][$topProgram][$project->closedDate] = $project;
  634. }
  635. }
  636. else
  637. {
  638. if($project->status != 'closed')
  639. {
  640. $classifiedProjects['otherProjects'][$topProgram][$project->status][] = $project;
  641. }
  642. else
  643. {
  644. $classifiedProjects['closedProjects']['other'][$topProgram][$project->closedDate] = $project;
  645. }
  646. }
  647. }
  648. return $classifiedProjects;
  649. }
  650. /**
  651. * 对groups列表进行排序和缩减。
  652. * Sort and reduce projects.
  653. *
  654. * @param array $projectsStats
  655. * @param int $retainNum
  656. * @access protected
  657. * @return array
  658. */
  659. protected function sortAndReduceClosedProjects($projectsStats, $retainNum = 2)
  660. {
  661. $sortedAndReducedProjects = array('my' => $projectsStats['myProjects'], 'other' => $projectsStats['otherProjects']);
  662. $closedProjects = $projectsStats['closedProjects'];
  663. foreach($closedProjects as $group => $groupedProjects)
  664. {
  665. foreach($groupedProjects as $topProgram => $projects)
  666. {
  667. krsort($projects);
  668. if($retainNum > 0)
  669. {
  670. $sortedAndReducedProjects[$group][$topProgram]['closed'] = array_slice($projects, 0, $retainNum);
  671. }
  672. }
  673. }
  674. return $sortedAndReducedProjects;
  675. }
  676. /**
  677. * 根据项目集ID查询所有项目集的层级。
  678. * Get all program level of a program.
  679. *
  680. * @param int $program
  681. * @param string $path
  682. * @param int $grade
  683. * @access protected
  684. * @return string
  685. */
  686. protected function getParentProgram($path, $grade)
  687. {
  688. $programList = $this->dao->select('id,name')->from(TABLE_PROGRAM)
  689. ->where('id')->in(trim($path, ','))
  690. ->andWhere('grade')->lt($grade)
  691. ->orderBy('grade asc')
  692. ->fetchPairs();
  693. return implode('/', $programList);
  694. }
  695. /**
  696. * 根据状态和和我参与的查询项目列表。
  697. * Get project list by status and with my participation.
  698. *
  699. * @param string $status
  700. * @param string $orderBy
  701. * @param bool $involved
  702. * @param object $pager
  703. * @access protected
  704. * @return array
  705. */
  706. protected function fetchProjectList($status, $orderBy, $involved, $pager)
  707. {
  708. $query = '';
  709. if($status == 'bysearch' && $this->session->projectQuery !== false) $query = $this->session->projectQuery;
  710. $query = str_replace('`id`','t1.id', $query);
  711. return $this->dao->select('DISTINCT t1.*')->from(TABLE_PROJECT)->alias('t1')
  712. ->leftJoin(TABLE_TEAM)->alias('t2')->on('t1.id=t2.root')
  713. ->leftJoin(TABLE_STAKEHOLDER)->alias('t3')->on('t1.id=t3.objectID')
  714. ->where('t1.deleted')->eq('0')
  715. ->andWhere('t1.vision')->eq($this->config->vision)
  716. ->andWhere('t1.type')->eq('project')
  717. ->beginIF(!in_array($status, array('all', 'undone', 'review', 'unclosed', 'delayed', 'bysearch'), true))->andWhere('t1.status')->eq($status)->fi()
  718. ->beginIF(in_array($status, array('undone', 'unclosed')))->andWhere('t1.status')->in('wait,doing')->fi()
  719. ->beginIF($status == 'delayed')->andWhere('t1.status')->notIn('done,closed,suspend')->andWhere('t1.end')->lt(helper::today())->fi()
  720. ->beginIF($status == 'review')
  721. ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
  722. ->andWhere('t1.reviewStatus')->eq('doing')
  723. ->fi()
  724. ->beginIF($this->cookie->involved || $involved)
  725. ->andWhere('t2.type')->eq('project')
  726. ->andWhere('t1.openedBy', true)->eq($this->app->user->account)
  727. ->orWhere('t1.PM')->eq($this->app->user->account)
  728. ->orWhere('t2.account')->eq($this->app->user->account)
  729. ->orWhere('(t3.user')->eq($this->app->user->account)
  730. ->andWhere('t3.deleted')->eq(0)
  731. ->markRight(1)
  732. ->orWhere("CONCAT(',', t1.whitelist, ',')")->like("%,{$this->app->user->account},%")
  733. ->markRight(1)
  734. ->fi()
  735. ->beginIF($status == 'bysearch' && $query)->andWhere($query)->fi()
  736. ->orderBy($orderBy)
  737. ->page($pager, 't1.id')
  738. ->fetchAll('id');
  739. }
  740. /**
  741. * 通过条件查询和数量限制查询项目列表。
  742. * Get project list by query and with limit.
  743. *
  744. * @param string $status
  745. * @param int $projectID
  746. * @param string $orderBy
  747. * @param int $limit
  748. * @param string $excludedModel
  749. * @access protected
  750. * @return array
  751. */
  752. protected function fetchProjectListByQuery($status = 'all', $projectID = 0, $orderBy = 'order_asc', $limit = 0, $excludedModel = '')
  753. {
  754. return $this->dao->select('*')->from(TABLE_PROJECT)
  755. ->where('type')->eq('project')
  756. ->andWhere('vision')->eq($this->config->vision)
  757. ->andWhere('deleted')->eq(0)
  758. ->beginIF($projectID)->andWhere('hasProduct')->eq('1')->fi()
  759. ->beginIF($projectID)->andWhere('multiple')->eq('1')->fi()
  760. ->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi()
  761. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
  762. ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
  763. ->beginIF($status == 'unclosed')->andWhere('status')->ne('closed')->fi()
  764. ->beginIF($status && !in_array($status, array('all', 'undone', 'unclosed', 'involved')))->andWhere('status')->eq($status)->fi()
  765. ->beginIF($status == 'involved')
  766. ->andWhere('openedBy', true)->eq($this->app->user->account)
  767. ->orWhere('PM')->eq($this->app->user->account)
  768. ->orWhere("CONCAT(',', whitelist, ',')")->like("%,{$this->app->user->account},%")
  769. ->markRight(1)
  770. ->fi()
  771. ->beginIF($projectID)->orWhere('id')->eq($projectID)->fi()
  772. ->orderBy($orderBy)
  773. ->beginIF($limit)->limit($limit)->fi()
  774. ->fetchAll('id');
  775. }
  776. /**
  777. * 根据项目ID列表查询团队成员数量。
  778. * Get project team member count by project id list.
  779. *
  780. * @param array $projectIdList
  781. * @access protected
  782. * @return array
  783. */
  784. protected function fetchMemberCountByIdList($projectIdList)
  785. {
  786. return $this->dao->select('t1.root, count(t1.id) as count')->from(TABLE_TEAM)->alias('t1')
  787. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
  788. ->where('t1.root')->in($projectIdList)
  789. ->andWhere('t2.deleted')->eq(0)
  790. ->groupBy('t1.root')
  791. ->fetchPairs();
  792. }
  793. /**
  794. * 根据项目ID列表查询任务的总预计工时。
  795. * Get task all estimate by project id list.
  796. *
  797. * @param array $projectIdList
  798. * @param string $fields
  799. * @access protected
  800. * @return array
  801. */
  802. protected function fetchTaskEstimateByIdList($projectIdList, $fields = 'estimate')
  803. {
  804. $fields = explode(',', $fields);
  805. $selectFields = 't2.project';
  806. foreach($fields as $field) $selectFields .= ", ROUND(SUM(t1.{$field}), 1) AS {$field}";
  807. return $this->dao->select($selectFields)->from(TABLE_TASK)->alias('t1')
  808. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
  809. ->where('t1.parent')->lt(1)
  810. ->andWhere('t2.project')->in($projectIdList)
  811. ->andWhere('t1.deleted')->eq(0)
  812. ->andWhere('t2.deleted')->eq(0)
  813. ->groupBy('t2.project')
  814. ->fetchAll('project');
  815. }
  816. /**
  817. * 通过项目ID列表查询需求的数量。
  818. * Get the number of stories associated with the project.
  819. *
  820. * @param array $projectIdList
  821. * @access protected
  822. * @return array
  823. */
  824. protected function getTotalStoriesByProject($projectIdList)
  825. {
  826. return $this->dao->select("t1.project, count(t2.id) as `allStories`, count(if(t2.status = 'active' or t2.status = 'changing', 1, null)) as `leftStories`, count(if(t2.status = 'closed' and t2.`closedReason` = 'done', 1, null)) as `doneStories`")->from(TABLE_PROJECTSTORY)->alias('t1')
  827. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id')
  828. ->where('t1.project')->in($projectIdList)
  829. ->andWhere('deleted')->eq('0')
  830. ->groupBy('project')
  831. ->fetchAll('project');
  832. }
  833. /**
  834. * 删除团队成员。
  835. * Delete Members.
  836. *
  837. * @param int $projectID
  838. * @param string $openedBy
  839. * @param array $deleteMembers
  840. * @access protected
  841. * @return bool
  842. */
  843. protected function deleteMembers($projectID, $openedBy, $deleteMembers)
  844. {
  845. $this->dao->delete()->from(TABLE_TEAM)
  846. ->where('root')->eq($projectID)
  847. ->andWhere('type')->eq('project')
  848. ->andWhere('account')->in($deleteMembers)
  849. ->andWhere('account')->ne($openedBy)
  850. ->exec();
  851. return !dao::isError();
  852. }
  853. /**
  854. * 通过项目ID获取任务数量统计。
  855. * Get the number of tasks associated with the project.
  856. *
  857. * @param array $projectIdList
  858. * @access protected
  859. * @return array
  860. */
  861. protected function getTotalTaskByProject($projectIdList)
  862. {
  863. $executionIdList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->in($projectIdList)->andWhere('deleted')->eq(0)->fetchPairs();
  864. return $this->dao->select("project, count(id) as allTasks, count(if(status = 'wait', 1, null)) as waitTasks, count(if(status = 'doing', 1, null)) as doingTasks, count(if(status = 'done', 1, null)) as doneTasks, count(if(status = 'wait' or status = 'pause' or status = 'cancel', 1, null)) as leftTasks, count(if(status = 'done' or status = 'closed', 1, null)) as litedoneTasks")->from(TABLE_TASK)
  865. ->where('execution')->in($executionIdList)
  866. ->andWhere('deleted')->eq(0)
  867. ->groupBy('project')
  868. ->fetchAll('project');
  869. }
  870. /**
  871. * 返回请求被拒绝的跳转信息。
  872. * return accessDenied response.
  873. *
  874. * @access protected
  875. * @return string
  876. */
  877. protected function accessDenied()
  878. {
  879. $this->session->set('project', '0');
  880. return $this->app->control->sendError($this->lang->project->accessDenied, helper::createLink('project', 'browse'));
  881. }
  882. /**
  883. * 修改无迭代项目下执行的状态。
  884. * Modify the execution status when changing the status of no execution project.
  885. *
  886. * @param int $projectID
  887. * @param string $status
  888. * @access protected
  889. * @return array|false|int
  890. */
  891. protected function changeExecutionStatus($projectID, $status)
  892. {
  893. if(!in_array($status, array('start', 'suspend', 'activate', 'close'))) return false;
  894. $execution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('multiple')->eq('0')->fetch();
  895. if(!$execution) return false;
  896. $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
  897. $postData = new stdclass();
  898. $postData->status = $status;
  899. $postData->begin = $execution->begin;
  900. $postData->end = $execution->end;
  901. $postData->uid = '';
  902. $postData->comment = '';
  903. if($status == 'close') $postData->realEnd = $project->realEnd;
  904. return $this->loadModel('execution')->$status($execution->id, $postData);
  905. }
  906. /**
  907. * 通过项目ID获取Bug数量统计。
  908. * Get the number of bugs associated with the project.
  909. *
  910. * @param array $projectIdList
  911. * @access protected
  912. * @return array
  913. */
  914. protected function getTotalBugByProject($projectIdList)
  915. {
  916. return $this->dao->select("project, count(id) AS allBugs, count(if(status = 'active', 1, null)) AS leftBugs, count(if(status = 'resolved', 1, null)) AS doneBugs")->from(TABLE_BUG)
  917. ->where('project')->in($projectIdList)
  918. ->andWhere('deleted')->eq(0)
  919. ->groupBy('project')
  920. ->fetchAll('project');
  921. }
  922. /**
  923. * 根据项目模式设置菜单。
  924. * Set menu by project model.
  925. *
  926. * @param string $projectModel
  927. * @access protected
  928. * @return bool
  929. */
  930. protected function setMenuByModel($projectModel)
  931. {
  932. global $lang, $config, $app;
  933. $model = 'scrum';
  934. if(in_array($projectModel, $this->config->project->waterfallList))
  935. {
  936. $model = 'waterfall';
  937. $lang->project->createExecution = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->createExecution);
  938. $lang->project->lastIteration = str_replace($lang->executionCommon, $lang->project->stage, $lang->project->lastIteration);
  939. $this->loadModel('execution');
  940. $executionCommonLang = $lang->executionCommon;
  941. $lang->executionCommon = $lang->project->stage;
  942. include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
  943. $lang->execution->typeList['sprint'] = $executionCommonLang;
  944. if($app->rawModule == 'project' && $app->rawMethod == 'executionreport') $lang->waterfall->menu->execution['subModule'] .= ',project';
  945. }
  946. elseif($projectModel == 'kanban')
  947. {
  948. $model = 'kanbanProject';
  949. $lang->executionCommon = $lang->project->kanban;
  950. }
  951. if(isset($lang->$model))
  952. {
  953. $key = 'project-' . $model;
  954. if(isset($config->customMenu->$key))
  955. {
  956. $lang->{$model}->menuOrder = array();
  957. $lang->{$model}->dividerMenu = '';
  958. $items = json_decode($config->customMenu->$key);
  959. $prevItem = '';
  960. foreach($items as $item)
  961. {
  962. $lang->{$model}->menuOrder[$item->order] = $item->name;
  963. if($prevItem == 'divider') $lang->{$model}->dividerMenu .= $item->name . ',';
  964. $prevItem = $item->name;
  965. }
  966. if($lang->{$model}->dividerMenu) $lang->{$model}->dividerMenu = ',' . trim($lang->{$model}->dividerMenu, ',') . ',';
  967. }
  968. if($this->config->inCompose)
  969. {
  970. $repoServers = $this->loadModel('pipeline')->getPairs($this->config->pipeline->checkRepoServers);
  971. if(empty($repoServers)) unset($lang->{$model}->menu->devops);
  972. }
  973. $lang->project->menu = $lang->{$model}->menu;
  974. $lang->project->menuOrder = $lang->{$model}->menuOrder;
  975. $lang->project->dividerMenu = $lang->{$model}->dividerMenu;
  976. }
  977. return true;
  978. }
  979. /**
  980. * 根据是否产品型项目设置菜单。
  981. * Set menu by whether product project.
  982. *
  983. * @param int $projectID
  984. * @param int $hasProduct
  985. * @param string $model
  986. * @access protected
  987. * @return bool
  988. */
  989. protected function setMenuByProduct($projectID, $hasProduct, $model)
  990. {
  991. global $lang;
  992. if($hasProduct)
  993. {
  994. unset($lang->project->menu->projectplan);
  995. unset($lang->project->menu->settings['subMenu']->module);
  996. }
  997. $projectProduct = (int)$this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetch('product');
  998. if(isset($lang->project->menu->settings['subMenu']->module['link'])) $lang->project->menu->settings['subMenu']->module['link'] = sprintf($lang->project->menu->settings['subMenu']->module['link'], $projectProduct);
  999. if(!$hasProduct && in_array($model, $this->config->project->scrumList) && isset($lang->project->menu->projectplan)) $lang->project->menu->projectplan['link'] = sprintf($lang->project->menu->projectplan['link'], $projectProduct);
  1000. if(!$hasProduct) unset($lang->project->menu->settings['subMenu']->products);
  1001. if(!in_array($model, $this->config->project->scrumList)) unset($lang->project->menu->projectplan);
  1002. return true;
  1003. }
  1004. /**
  1005. * 根据访问模块的导航组设置菜单。
  1006. * Set menu lang for module.
  1007. *
  1008. * @param string $navGroup
  1009. * @param int $executionID
  1010. * @param object $project
  1011. * @access protected
  1012. * @return bool
  1013. */
  1014. protected function setNavGroupMenu($navGroup, $executionID, $project)
  1015. {
  1016. global $lang;
  1017. foreach($lang->$navGroup->menu as $label => $menu)
  1018. {
  1019. $objectID = 0;
  1020. if(strpos($this->config->project->multiple['project'], ",{$label},") !== false) $objectID = $project->id;
  1021. if(strpos($this->config->project->multiple['execution'], ",{$label},") !== false)
  1022. {
  1023. $objectID = $executionID;
  1024. $lang->$navGroup->menu->{$label}['subModule'] = 'project';
  1025. }
  1026. $lang->$navGroup->menu->$label = commonModel::setMenuVarsEx($menu, $objectID);
  1027. if(isset($menu['subMenu']))
  1028. {
  1029. foreach($menu['subMenu'] as $key1 => $subMenu) $lang->$navGroup->menu->{$label}['subMenu']->$key1 = common::setMenuVarsEx($subMenu, $objectID);
  1030. }
  1031. if(!isset($menu['dropMenu'])) continue;
  1032. foreach($menu['dropMenu'] as $key2 => $dropMenu)
  1033. {
  1034. if(strpos($this->config->project->multiple['project'], ",{$label},") !== false) $objectID = $project->id;
  1035. if(strpos($this->config->project->multiple['execution'], ",{$key2},") !== false)
  1036. {
  1037. $objectID = $executionID;
  1038. $lang->$navGroup->menu->{$label}['subModule'] = 'project';
  1039. }
  1040. $lang->$navGroup->menu->{$label}['dropMenu']->$key2 = common::setMenuVarsEx($dropMenu, $objectID);
  1041. if(!isset($dropMenu['subMenu'])) continue;
  1042. foreach($dropMenu['subMenu'] as $key3 => $subMenu) $lang->$navGroup->menu->{$label}['dropMenu']->$key3 = common::setMenuVarsEx($subMenu, $objectID);
  1043. }
  1044. }
  1045. return true;
  1046. }
  1047. /**
  1048. * 创建里程碑报告。
  1049. *
  1050. * @param int $projectID
  1051. * @access protected
  1052. * @return bool
  1053. */
  1054. protected function createMilestoneReport($projectID)
  1055. {
  1056. $reportID = $this->dao->select('id')->from(TABLE_DOC)->where('project')->eq($projectID)->andWhere('reportModule')->eq('milestone')->andWhere('type')->eq('projectReport')->fetch('id');
  1057. if(!empty($reportID)) return true;
  1058. $report = new stdclass();
  1059. $report->title = $this->lang->project->milestoneReport;
  1060. $report->project = $projectID;
  1061. $report->reportModule = 'milestone';
  1062. $report->templateType = 'projectReport';
  1063. $report->addedBy = 'system';
  1064. $report->addedDate = helper::now();
  1065. $this->dao->insert(TABLE_DOC)->data($report)->exec();
  1066. return !dao::isError();
  1067. }
  1068. }