tao.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. <?php
  2. /**
  3. * The model file of product 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 chen.tao<chentao@easycorp.ltd>
  8. * @package product
  9. * @link https://www.zentao.net
  10. */
  11. class productTao extends productModel
  12. {
  13. /**
  14. * 初始化类的时候,根据是否有产品,还有其他条件,判断是否需要跳转到创建产品页面
  15. * Check locate create product page whether or not, by exist products and other.
  16. *
  17. * @param array $products
  18. * @access protected
  19. * @return bool
  20. */
  21. protected function checkLocateCreate($products)
  22. {
  23. if(!empty($products)) return false;
  24. $methodName = $this->app->getMethodName();
  25. if(strpos($this->config->product->skipRedirectMethod, ",{$methodName},") !== false) return false;
  26. $viewType = $this->app->getViewType();
  27. if($viewType == 'mhtml') return false; //If client device is mobile, then not locate.
  28. if($viewType == 'json' or (defined('RUN_MODE') and RUN_MODE == 'api')) return false; //If request type is api, then not locate.
  29. return true;
  30. }
  31. /**
  32. * 从数据表获取符合条件的id=>name的键值对。
  33. * Fetch pairs like id=>name.
  34. *
  35. * @param string $mode all|noclosed
  36. * @param int $programID
  37. * @param string|array $append
  38. * @param string|int $shadow all|0|1
  39. * @access protected
  40. * @return int[]
  41. */
  42. protected function fetchPairs($mode = '', $programID = 0, $append = '', $shadow = 0)
  43. {
  44. $append = $this->formatAppendParam($append);
  45. return $this->dao->select("t1.*, IF(INSTR(' closed', t1.status) < 2, 0, 1) AS isClosed")->from(TABLE_PRODUCT)->alias('t1')
  46. ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')
  47. ->where('t1.name')->ne('')
  48. ->beginIF($this->config->vision == 'rnd')->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)")->fi()
  49. ->beginIF($this->app->tab == 'feedback')
  50. ->andWhere('t1.status')->eq('normal')
  51. ->andWhere('t1.vision')->ne('lite')
  52. ->fi()
  53. ->beginIF($shadow !== 'all')->andWhere('t1.shadow')->eq((int)$shadow)->fi()
  54. ->andWhere('(1=1')
  55. ->beginIF(strpos($mode, 'all') === false)->andWhere('t1.deleted')->eq(0)->fi()
  56. ->beginIF($programID)->andWhere('t1.program')->eq($programID)->fi()
  57. ->beginIF(strpos($mode, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi()
  58. ->beginIF(strpos("|$mode|", '|closed|') !== false)->andWhere('t1.status')->eq('closed')->fi()
  59. ->beginIF(!$this->app->user->admin && $this->config->vision != 'lite' && $this->app->rawModule != 'repo')->andWhere('t1.id')->in($this->app->user->view->products)->fi()
  60. ->markRight(1)
  61. ->beginIF($append)->orWhere('(t1.id')->in($append)->markRight(1)->fi()
  62. ->filterTpl('skip')
  63. ->orderBy("isClosed,t2.order_asc,t1.line_desc,t1.order_asc")
  64. ->fetchPairs('id', 'name');
  65. }
  66. /**
  67. * 获取该产品下,所有符合参数条件的关联项目。
  68. * Fetch all projects link this product.
  69. *
  70. * @param int $productID
  71. * @param string $browseType all|undone|wait|doing|done
  72. * @param string $branch
  73. * @param string $orderBy
  74. * @param object|null $pager
  75. * @access protected
  76. * @return array
  77. */
  78. protected function fetchAllProductProjects($productID, $browseType = 'all', $branch = 'all', $orderBy = 'order_desc', $pager = null)
  79. {
  80. return $this->dao->select('DISTINCT t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  81. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  82. ->where('t1.product')->eq($productID)
  83. ->andWhere('t2.type')->eq('project')
  84. ->beginIF($browseType == 'undone')->andWhere('t2.status')->in('wait,doing')->fi()
  85. ->beginIF(strpos(",all,undone,", ",$browseType,") === false)->andWhere('t2.status')->eq($browseType)->fi()
  86. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
  87. ->beginIF($branch !== 'all')->andWhere('t1.branch')->in($branch)->fi()
  88. ->andWhere('t2.deleted')->eq('0')
  89. ->orderBy($orderBy)
  90. ->page($pager, 't2.id')
  91. ->fetchAll('id');
  92. }
  93. /**
  94. * 只获取该产品下,我参与的,符合参数条件的关联项目。
  95. * Fetch involved projects link product.
  96. *
  97. * @param int $productID
  98. * @param string $browseType all|undone|wait|doing|done
  99. * @param string $branch
  100. * @param string $orderBy
  101. * @param object|null $pager
  102. * @access protected
  103. * @return array
  104. */
  105. protected function fetchInvolvedProductProjects($productID, $browseType = 'all', $branch = 'all', $orderBy = 'order_desc', $pager = null)
  106. {
  107. return $this->dao->select('DISTINCT t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  108. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  109. ->leftJoin(TABLE_TEAM)->alias('t3')->on('t2.id=t3.root')
  110. ->leftJoin(TABLE_STAKEHOLDER)->alias('t4')->on('t2.id=t4.objectID')
  111. ->where('t1.product')->eq($productID)
  112. ->andWhere('t2.type')->eq('project')
  113. ->andWhere('t3.type')->eq('project')
  114. ->beginIF($browseType == 'undone')->andWhere('t2.status')->in('wait,doing')->fi()
  115. ->beginIF(strpos(",all,undone,", ",$browseType,") === false)->andWhere('t2.status')->eq($browseType)->fi()
  116. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
  117. ->andWhere('t2.openedBy', true)->eq($this->app->user->account)
  118. ->orWhere('t2.PM')->eq($this->app->user->account)
  119. ->orWhere('t3.account')->eq($this->app->user->account)
  120. ->orWhere('(t4.user')->eq($this->app->user->account)
  121. ->andWhere('t4.deleted')->eq(0)
  122. ->markRight(1)
  123. ->orWhere("CONCAT(',', t2.whitelist, ',')")->like("%,{$this->app->user->account},%")
  124. ->markRight(1)
  125. ->beginIF($branch !== 'all')->andWhere('t1.branch')->in($branch)->fi()
  126. ->andWhere('t2.deleted')->eq('0')
  127. ->orderBy($orderBy)
  128. ->page($pager, 't2.id')
  129. ->fetchAll('id');
  130. }
  131. /**
  132. * 获取产品ID数组中带有项目集信息的产品分页列表。
  133. * Get products with program data that in the ID list.
  134. *
  135. * @param array $productIDs
  136. * @param object $pager
  137. * @access protected
  138. * @return array
  139. */
  140. protected function getPagerProductsWithProgramIn($productIDs, $pager)
  141. {
  142. return $this->dao->select('t1.*')->from(TABLE_PRODUCT)->alias('t1')
  143. ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')
  144. ->where('t1.id')->in($productIDs)
  145. ->filterTpl('skip')
  146. ->orderBy('t2.order_asc, t1.line_desc, t1.order_asc')
  147. ->page($pager)
  148. ->fetchAll('id');
  149. }
  150. /**
  151. * 获取产品ID数组中的产品排序分页列表。
  152. * Get products in the ID list.
  153. *
  154. * @param array $productIDs
  155. * @param object $pager
  156. * @param string $orderBy
  157. * @access protected
  158. * @return array
  159. */
  160. protected function getPagerProductsIn($productIDs, $pager, $orderBy)
  161. {
  162. return $this->dao->select('*')->from(TABLE_PRODUCT)
  163. ->where('id')->in($productIDs)
  164. ->orderBy($orderBy)
  165. ->page($pager)
  166. ->fetchAll('id');
  167. }
  168. /**
  169. * 获取在需求列表页面的搜索表单中,模块字段的下拉选项。包括产品、项目下的需求页面。
  170. * Get modules for search form, in product, project story page.
  171. *
  172. * @param int $productID
  173. * @param array $products
  174. * @param string $branch
  175. * @param int $projectID
  176. * @access protected
  177. * @return array
  178. */
  179. protected function getModulesForSearchForm($productID, $products, $branch = 'all', $projectID = 0)
  180. {
  181. $this->loadModel('tree');
  182. if($this->app->tab != 'project') return $this->tree->getOptionMenu($productID, 'story', 0, $branch);
  183. if($productID)
  184. {
  185. $modules = array();
  186. $branchList = $this->loadModel('branch')->getPairs($productID, '', $projectID);
  187. unset($branchList['all']);
  188. $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchList));
  189. foreach($branchModuleList as $branchModules) $modules += $branchModules;
  190. return $modules;
  191. }
  192. /* 在项目需求页面,获取项目关联产品的模块。 */
  193. $moduleList = array();
  194. $modules = array('/');
  195. $branchGroup = $this->loadModel('execution')->getBranchByProduct(array_keys($products), $projectID, '');
  196. foreach($products as $productID => $productName)
  197. {
  198. if(isset($branchGroup[$productID]))
  199. {
  200. $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchGroup[$productID]));
  201. foreach($branchModuleList as $branchModules)
  202. {
  203. if(is_array($branchModules)) $moduleList += $branchModules;
  204. }
  205. }
  206. else
  207. {
  208. $moduleList = $this->tree->getOptionMenu($productID, 'story', 0, $branch);
  209. }
  210. foreach($moduleList as $moduleID => $moduleName)
  211. {
  212. if(empty($moduleID)) continue;
  213. $modules[$moduleID] = $productName . $moduleName;
  214. }
  215. }
  216. return $modules;
  217. }
  218. /**
  219. * 根据产品ID列表和类型,获取统计数据。
  220. * Get statistic data by product ID list and type.
  221. *
  222. * @param array $productIdList
  223. * @param string $type
  224. * @access protected
  225. * @return array
  226. */
  227. protected function getStatisticByType($productIdList, $type)
  228. {
  229. $this->app->loadClass('date', true);
  230. $weekDate = date::getThisWeek();
  231. $fields = 'product,COUNT(1) AS count';
  232. $tableName = zget($this->config->objectTables, $type, TABLE_BUG);
  233. if($type == 'plans') $tableName = TABLE_PRODUCTPLAN;
  234. if($type == 'releases') $tableName = TABLE_RELEASE;
  235. if($type == 'latestReleases')
  236. {
  237. $fields = 'product, name, date';
  238. $tableName = TABLE_RELEASE;
  239. }
  240. $dao = $this->dao->select($fields)
  241. ->from($tableName)
  242. ->where('product')->in($productIdList)
  243. ->andWhere('deleted')->eq(0)
  244. ->beginIF($type == 'plans')->andWhere('end')->gt(helper::now())->fi()
  245. ->beginIF($type == 'assignToNull')->andWhere('assignedto')->eq('')->fi()
  246. ->beginIF($type == 'fixedBugs')->andWhere('resolution')->eq('fixed')->fi()
  247. ->beginIF($type == 'unResolved')->orWhere('resolution')->eq('postponed')->fi()
  248. ->beginIF($type == 'closedBugs' || $type == 'fixedBugs')->andWhere('status')->eq('closed')->fi()
  249. ->beginIF($type == 'unResolved' || $type == 'activeBugs')->andWhere('status')->eq('active')->fi()
  250. ->beginIF($type == 'thisWeekBugs')->andWhere('openedDate')->between($weekDate['begin'], $weekDate['end'])->fi()
  251. ->beginIF($type != 'latestReleases')->groupBy('product')->fi()
  252. ->beginIF($type == 'latestReleases')->orderBy('id_desc')->fi();
  253. return $type == 'latestReleases' ? $dao->fetchGroup('product') : $dao->fetchPairs();
  254. }
  255. /**
  256. * 计算在点击1.5级导航下拉选项后,跳转的模块名和方法名。
  257. * Compute locate for drop menu in PC.
  258. *
  259. * @access protected
  260. * @return array
  261. */
  262. protected function computeLocate4DropMenu()
  263. {
  264. $currentModule = (string)$this->app->rawModule;
  265. $currentMethod = (string)$this->app->rawMethod;
  266. /* Init currentModule and currentMethod for report and story. */
  267. if($currentModule == 'story')
  268. {
  269. if(strpos(",track,create,batchcreate,batchclose,", ",{$currentMethod},") === false) $currentModule = 'product';
  270. if($currentMethod == 'view' or $currentMethod == 'change' or $currentMethod == 'review') $currentMethod = 'browse';
  271. }
  272. if($currentModule == 'testcase' and strpos(',view,edit,', ",$currentMethod,") !== false) $currentMethod = 'browse';
  273. if($currentModule == 'bug' and $currentMethod == 'edit') $currentMethod = 'browse';
  274. if($currentMethod == 'report') $currentMethod = 'browse';
  275. return array($currentModule, $currentMethod);
  276. }
  277. /**
  278. * 格式化append参数,保证输出用逗号间隔的id列表。
  279. * Format append param.
  280. *
  281. * @param string|array append
  282. * @access protected
  283. * @return string
  284. */
  285. protected function formatAppendParam($append = '')
  286. {
  287. if(empty($append)) return '';
  288. if(is_string($append)) $append = explode(',', $append);
  289. $append = array_map(function($item){;return is_array($item) ? $item->branch : (int)$item;}, $append);
  290. $append = array_unique(array_filter($append));
  291. sort($append);
  292. return implode(',', $append);
  293. }
  294. /**
  295. * 创建产品线。
  296. * Create product line.
  297. *
  298. * @param int programID
  299. * @param string lineName
  300. * @access protected
  301. * @return int|false
  302. */
  303. protected function createLine($programID, $lineName)
  304. {
  305. if($programID < 0) return false;
  306. if(empty($lineName)) return false;
  307. $line = new stdClass();
  308. $line->type = 'line';
  309. $line->parent = 0;
  310. $line->grade = 1;
  311. $line->name = htmlSpecialString($lineName);
  312. $line->root = in_array($this->config->systemMode, array('ALM', 'PLM')) && helper::hasFeature('program') ? $programID : 0;
  313. $existedLineID = (int)$this->dao->select('id')->from(TABLE_MODULE)->where('type')->eq('line')->andWhere('root')->eq($line->root)->andWhere('name')->eq($line->name)->fetch('id');
  314. if($existedLineID) return $existedLineID;
  315. $this->dao->insert(TABLE_MODULE)->data($line)->exec();
  316. if(dao::isError()) return false;
  317. $lineID = $this->dao->lastInsertID();
  318. $path = ",$lineID,";
  319. $this->dao->update(TABLE_MODULE)->set('path')->eq($path)->set('`order`')->eq($lineID)->where('id')->eq($lineID)->exec();
  320. return $lineID;
  321. }
  322. /**
  323. * 关联创建产品主库
  324. * Create main lib for product
  325. *
  326. * @param int productID
  327. * @access protected
  328. * @return int|false
  329. */
  330. protected function createMainLib($productID)
  331. {
  332. if($productID <= 0) return false;
  333. $existedLibID = (int)$this->dao->select('id')->from(TABLE_DOCLIB)->where('product')->eq($productID)
  334. ->andWhere('type')->eq('product')
  335. ->andWhere('main')->eq('1')
  336. ->fetch('id');
  337. if($existedLibID) return $existedLibID;
  338. $this->app->loadLang('doc');
  339. $lib = new stdclass();
  340. $lib->product = $productID;
  341. $lib->name = $this->lang->doclib->main['product'];
  342. $lib->type = 'product';
  343. $lib->main = '1';
  344. $lib->acl = 'default';
  345. $lib->vision = $this->config->vision;
  346. $lib->addedBy = $this->app->user->account;
  347. $lib->addedDate = helper::now();
  348. $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
  349. $productSettingList = isset($this->config->global->productSettingList) ? json_decode($this->config->global->productSettingList, true) : array();
  350. if($productSettingList)
  351. {
  352. $productSettingList[] = $productID;
  353. $this->loadModel('setting')->setItem('system.common.global.productSettingList', json_encode($productSettingList));
  354. }
  355. if(dao::isError())return false;
  356. return $this->dao->lastInsertID();
  357. }
  358. /**
  359. * 执行SQL,更新到对应的产品信息。
  360. * Do update this product.
  361. *
  362. * @param object $product
  363. * @param int $productID
  364. * @param int $programID
  365. * @access protected
  366. * @return bool
  367. */
  368. protected function doUpdate($product, $productID, $programID)
  369. {
  370. if(empty($productID)) return false;
  371. if(count(get_object_vars($product)) == 0) return false;
  372. $this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck()
  373. ->checkIF(!empty($product->name), 'name', 'unique', "id != {$productID} and `program` = {$programID} and `deleted` = '0'")
  374. ->checkIF(!empty($product->code), 'code', 'unique', "id != {$productID} and `program` = {$programID} and `deleted` = '0'")
  375. ->batchCheck($this->config->product->edit->requiredFields, 'notempty')
  376. ->checkFlow()
  377. ->where('id')->eq($productID)
  378. ->exec();
  379. return !dao::isError();
  380. }
  381. /**
  382. * 获取需求列表关联的用例总数。
  383. * Get cases count of stories.
  384. *
  385. * @param array $storyIdList
  386. * @access protected
  387. * @return int
  388. */
  389. protected function getStoryCasesCount($storyIdList)
  390. {
  391. if(empty($storyIdList)) return 0;
  392. return $this->dao->select('COUNT(story) AS count')->from(TABLE_CASE)->where('story')->in($storyIdList)->andWhere('deleted')->eq('0')->fetch('count');
  393. }
  394. /**
  395. * 过滤无用例的需求。
  396. * Filter no cases story.
  397. *
  398. * @param array $storyIDList
  399. * @access protected
  400. * @return array
  401. */
  402. protected function filterNoCasesStory($storyIDList)
  403. {
  404. if(empty($storyIDList)) return array();
  405. return $this->dao->select('story')->from(TABLE_CASE)->where('story')->in($storyIDList)->andWhere('deleted')->eq(0)->fetchAll('story');
  406. }
  407. /**
  408. * 获取项目关联的产品。
  409. * Get products by project ID.
  410. *
  411. * @param int|array $projectID
  412. * @param string|array $append '1,2,3'
  413. * @param string $status
  414. * @param string $orderBy
  415. * @param bool $withDeleted
  416. * @param bool $noShadow
  417. * @access protected
  418. * @return int[]
  419. */
  420. protected function getProductsByProjectID($projectID, $append, $status, $orderBy, $withDeleted = false, $noShadow = false)
  421. {
  422. /* 处理要用的到变量信息。 */
  423. $append = $this->formatAppendParam($append);
  424. $orderBy = ($orderBy ? "{$orderBy}," : '') . 't2.order asc';
  425. return $this->dao->select("t1.branch, t1.plan, t2.*")->from(TABLE_PROJECTPRODUCT)->alias('t1')
  426. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
  427. ->where("(FIND_IN_SET('{$this->config->vision}', t2.vision)")
  428. ->beginIF(!$withDeleted)->andWhere('t2.deleted')->eq(0)->fi()
  429. ->beginIF($noShadow)->andWhere('t2.shadow')->eq(0)->fi()
  430. ->beginIF(!empty($projectID))->andWhere('t1.project')->in($projectID)->fi()
  431. ->beginIF(!$this->app->user->admin and $this->config->vision != 'lite')->andWhere('t2.id')->in($this->app->user->view->products)->fi()
  432. ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi()
  433. ->markRight(1)
  434. ->beginIF($append && empty($projectID))->orWhere('(t1.product')->in($append)->fi()
  435. ->beginIF($append && !empty($projectID))
  436. ->orWhere('(t1.product')->in($append)
  437. ->andWhere('t1.project')->in($projectID)
  438. ->markRight(1)
  439. ->fi()
  440. ->orderBy($orderBy)
  441. ->fetchAll();
  442. }
  443. /**
  444. * 过滤有效的产品计划, 并返回所有父级计划。
  445. * Filter valid product plans.
  446. *
  447. * @param array $planList
  448. * @access protected
  449. * @return array[]
  450. */
  451. protected function filterOrderedAndParentPlans($planList)
  452. {
  453. $parentPlans = array();
  454. $orderedPlans = array();
  455. foreach($planList as $planID => $plan)
  456. {
  457. /* Collect and remove parent plan. */
  458. if($plan->parent == '-1')
  459. {
  460. $parentPlans[$planID] = $plan->title;
  461. unset($planList[$planID]);
  462. continue;
  463. }
  464. /* Remove exceeded plan and long-time plan. */
  465. if((!helper::isZeroDate($plan->end) and $plan->end < date('Y-m-d')) or $plan->end == '2030-01-01') continue;
  466. $orderedPlans[$plan->end][] = $plan;
  467. }
  468. krsort($orderedPlans);
  469. return array($orderedPlans, $parentPlans);
  470. }
  471. /**
  472. * 通过产品ID查询产品关联的统计数据。
  473. * Get stat count by product ID.
  474. *
  475. * @param int $productID
  476. * @access protected
  477. * @return int|float
  478. * @param string $tableName
  479. */
  480. protected function getStatCountByID($tableName, $productID)
  481. {
  482. switch($tableName)
  483. {
  484. case TABLE_PRODUCTPLAN:
  485. /* Get unclosed plans count. */
  486. return $this->dao->select('COUNT(1) AS count')->from(TABLE_PRODUCTPLAN)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->andWhere('end')->gt(helper::now())->fetch('count');
  487. case TABLE_BUILD:
  488. /* Get builds count. */
  489. return $this->dao->select('COUNT(1) AS count')->from(TABLE_BUILD)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count');
  490. case TABLE_CASE:
  491. /* Get cases count. */
  492. return $this->dao->select('COUNT(1) AS count')->from(TABLE_CASE)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count');
  493. case TABLE_BUG:
  494. /* Get bugs count. */
  495. return $this->dao->select('COUNT(1) AS count')->from(TABLE_BUG)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count');
  496. case TABLE_DOC:
  497. /* Get docs count. */
  498. return $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->andWhere('type')->ne('chapter')->fetch('count');
  499. case TABLE_RELEASE:
  500. /* Get releases count. */
  501. return $this->dao->select('COUNT(1) AS count')->from(TABLE_RELEASE)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->fetch('count');
  502. case TABLE_PROJECTPRODUCT:
  503. return $this->dao->select('COUNT(DISTINCT(t1.project)) AS count')
  504. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  505. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  506. ->where('t2.deleted')->eq('0')
  507. ->andWhere('t1.product')->eq("$productID")
  508. ->andWhere('t2.type')->eq('project')
  509. ->fetch('count');
  510. case 'executions':
  511. return $this->dao->select('COUNT(DISTINCT(t1.project)) AS count')
  512. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  513. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  514. ->where('t2.deleted')->eq('0')
  515. ->andWhere('t1.product')->eq("$productID")
  516. ->andWhere('t2.type')->in('sprint,stage,kanban')
  517. ->fetch('count');
  518. case 'progress':
  519. $closedTotal = $this->dao->select('COUNT(id) AS count')->from(TABLE_STORY)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->andWhere('status')->eq('closed')->fetch('count');
  520. if(empty($closedTotal)) return 0;
  521. $allTotal = $this->dao->select('COUNT(id) AS count')->from(TABLE_STORY)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->fetch('count');
  522. return round($closedTotal / $allTotal * 100, 1);
  523. default:
  524. return 0;
  525. }
  526. }
  527. /**
  528. * 获取产品与项目关联的项目集数据列表。
  529. * Get the progam info of the releated project and product by product list.
  530. *
  531. * @param array $productList
  532. * @access protected
  533. * @return array
  534. */
  535. protected function getProjectProductList($productList)
  536. {
  537. $projectProductList = $this->dao->select('t1.product,t1.project,t2.parent,t2.path')
  538. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  539. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  540. ->where('t1.product')->in(array_keys($productList))
  541. ->andWhere('t1.project')->in($this->app->user->view->projects)
  542. ->andWhere('t2.type')->eq('project')
  543. ->andWhere('t2.status')->eq('doing')
  544. ->andWhere('t2.deleted')->eq('0')
  545. ->fetchGroup('product', 'project');
  546. if(!in_array($this->config->systemMode, array('ALM', 'PLM')) || $this->config->product->showAllProjects) return $projectProductList;
  547. /* ALM mode and don't show all projects. */
  548. foreach($projectProductList as $productID => $projects)
  549. {
  550. if(!isset($productList[$productID])) continue;
  551. $product = $productList[$productID];
  552. foreach($projects as $projectID => $project)
  553. {
  554. if($project->parent == $product->program || strpos($project->path, ",{$product->program},") === 0) continue;
  555. /* Filter invalid product. */
  556. unset($projectProductList[$productID][$projectID]);
  557. }
  558. }
  559. return $projectProductList;
  560. }
  561. /**
  562. * 获取产品相关计划列表。
  563. * Get plan list by product ID list.
  564. *
  565. * @param array $productIdList
  566. * @access protected
  567. * @return array
  568. */
  569. protected function getPlanList($productIdList)
  570. {
  571. $date = date('Y-m-d');
  572. return $this->dao->select('id,product,title,parent,begin,end')
  573. ->from(TABLE_PRODUCTPLAN)
  574. ->where('product')->in($productIdList)
  575. ->andWhere('deleted')->eq('0')
  576. ->andWhere('end')->ge($date)
  577. ->andWhere('parent')->ne(-1)
  578. ->orderBy('begin desc')
  579. ->fetchGroup('product', 'id');
  580. }
  581. /**
  582. * 获取产品相关执行列表。
  583. * Get execution list by project and product ID list.
  584. *
  585. * @param array $projectIdList
  586. * @param array $productIdList
  587. * @access protected
  588. * @return array
  589. */
  590. protected function getExecutionList($projectIdList, $productIdList)
  591. {
  592. return $this->dao->select('t1.product as productID,t2.*')
  593. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  594. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id')
  595. ->where('t2.type')->in('stage,sprint,kanban')
  596. ->andWhere('t2.project')->in($projectIdList)
  597. ->beginIF(!$this->app->user->admin)
  598. ->andWhere('t1.project')->in($this->app->user->view->sprints)
  599. ->fi()
  600. ->andWhere('t1.product')->in($productIdList)
  601. ->andWhere('t2.status')->eq('doing')
  602. ->andWhere('t2.multiple')->ne('0')
  603. ->andWhere('t2.deleted')->eq('0')
  604. ->orderBy('t2.id_desc')
  605. ->fetchGroup('project', 'id');
  606. }
  607. /**
  608. * 获取产品相关发布列表。
  609. * Get release list by product ID list.
  610. *
  611. * @param array $productIdList
  612. * @access protected
  613. * @return array
  614. */
  615. protected function getReleaseList($productIdList)
  616. {
  617. return $this->dao->select('id,product,name,marker')
  618. ->from(TABLE_RELEASE)
  619. ->where('deleted')->eq('0')
  620. ->andWhere('product')->in($productIdList)
  621. ->andWhere('status')->eq('normal')
  622. ->fetchGroup('product', 'id');
  623. }
  624. /**
  625. * 获取多个产品下进行中的执行数的键值对。
  626. * Get K-V pairs of product ID and doing executions count.
  627. *
  628. * @param array $productIdList
  629. * @access protected
  630. * @return array
  631. */
  632. protected function getExecutionCountPairs($productIdList)
  633. {
  634. return $this->dao->select('t1.product, COUNT(1) AS count')
  635. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  636. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  637. ->where('t2.deleted')->eq('0')
  638. ->andWhere('t1.product')->in($productIdList)
  639. ->andWhere('t2.type')->in('sprint,stage,kanban')
  640. ->andWhere('t2.status')->eq('doing')
  641. ->groupBy('t1.product')
  642. ->fetchPairs('product');
  643. }
  644. /**
  645. * 获取多个产品关联的项目数的键值对。
  646. * Retrieve key-value pairs of product IDs and their corresponding project counts.
  647. *
  648. * @param array $productIdList
  649. * @access protected
  650. * @return array
  651. */
  652. protected function getProjectCountPairs($productIdList)
  653. {
  654. return $this->dao->select('t1.product, COUNT(1) AS count')
  655. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  656. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  657. ->where('t2.deleted')->eq('0')
  658. ->andWhere('t1.product')->in($productIdList)
  659. ->andWhere('t2.type')->eq('project')
  660. ->andWhere('t2.status')->eq('doing')
  661. ->groupBy('t1.product')
  662. ->fetchPairs('product');
  663. }
  664. /**
  665. * 通过产品ID列表获取每个需求关联的用例数。
  666. * Get case count of each story in the story list.
  667. *
  668. * @param int[] $productIdList
  669. * @access protected
  670. * @return array
  671. */
  672. protected function getCaseCountByProductIdList($productIdList)
  673. {
  674. return $this->dao->select('story, COUNT(1) AS count')->from(TABLE_CASE)->alias('t1')
  675. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  676. ->where('t1.deleted')->eq('0')
  677. ->andWhere('t2.deleted')->eq('0')
  678. ->andWhere('t2.product')->in($productIdList)
  679. ->groupBy('story')
  680. ->fetchPairs('story');
  681. }
  682. /**
  683. * 构建执行键值对,主要处理子阶段的情况。
  684. * Build execution pairs.
  685. *
  686. * @param array $executions
  687. * @param string $mode stagefilter|hasparent
  688. * @param bool $withProjectName
  689. * @access protected
  690. * @return array
  691. */
  692. protected function buildExecutionPairs($executions, $mode = '', $withProjectName = false)
  693. {
  694. $projectIdList = array_column($executions, 'project');
  695. /* 现在只有阶段有子阶段,所以只查询阶段类型的执行。 */
  696. $stages = $this->dao->select('id,name,attribute,parent,path')->from(TABLE_EXECUTION)->where('type')->eq('stage')->andWhere('project')->in($projectIdList)->andWhere('deleted')->eq('0')->fetchAll('id');
  697. $stageFilter = strpos($mode, 'stagefilter') !== false;
  698. foreach($stages as $exec) $parents[$exec->parent] = true;
  699. /* 根据条件整理数据,将子阶段放到父阶段中。 */
  700. $executionPairs = array();
  701. foreach($executions as $id => $execution)
  702. {
  703. if(isset($parents[$id])) continue;
  704. if($stageFilter && in_array($execution->attribute, array('request', 'design', 'review'))) continue; // Some stages of waterfall not need.
  705. if(isset($execution->children))
  706. {
  707. $executionPairs = $executionPairs + $execution->children;
  708. continue;
  709. }
  710. /* Set execution name. */
  711. $executionName = '';
  712. if(isset($stages[$id]))
  713. {
  714. $paths = array_slice(explode(',', trim($execution->path, ',')), 1);
  715. foreach($paths as $path)
  716. {
  717. if(isset($stages[$path])) $executionName .= '/' . $stages[$path]->name;
  718. }
  719. }
  720. else
  721. {
  722. $executionName = $withProjectName ? '/' . $execution->name : $execution->name;
  723. }
  724. if($withProjectName) $executionName = $execution->projectName . $executionName;
  725. if(empty($execution->multiple)) $executionName = $execution->projectName . "({$this->lang->project->disableExecution})";
  726. $executionPairs[$id] = $executionName;
  727. }
  728. return $executionPairs;
  729. }
  730. /**
  731. * 统计项目集内的产品数据
  732. * Statistic product data.
  733. *
  734. * @param string $type line|program
  735. * @param array $programStructure
  736. * @param object $product
  737. * @access protected
  738. * @return array
  739. */
  740. protected function statisticProductData($type, $programStructure, $product)
  741. {
  742. if(empty($programStructure)) return $programStructure;
  743. /* Init vars. */
  744. $data = $type == 'program' ? $programStructure[$product->program] : $programStructure[$product->program][$product->line];
  745. foreach($this->config->product->statisticFields as $key => $fields)
  746. {
  747. foreach($fields as $field)
  748. {
  749. if(!isset($data[$field])) $data[$field] = 0;
  750. $data[$field] += $product->$field;
  751. }
  752. }
  753. return $data;
  754. }
  755. /**
  756. * 获取产品的统计数据。
  757. * Get summary of products to be refreshed.
  758. *
  759. * @param array $productIdList
  760. * @access protected
  761. * @return array
  762. */
  763. protected function getProductStats($productIdList)
  764. {
  765. $productStories = $this->getStoryStats($productIdList);
  766. $productBugs = $this->getBugStats($productIdList);
  767. $plans = $this->dao->select('product, count(id) AS c')
  768. ->from(TABLE_PRODUCTPLAN)
  769. ->where('deleted')->eq(0)
  770. ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi()
  771. ->andWhere('end')->gt(helper::now())
  772. ->groupBy('product')
  773. ->fetchPairs();
  774. $releases = $this->dao->select('product, count(id) AS c')
  775. ->from(TABLE_RELEASE)
  776. ->where('deleted')->eq(0)
  777. ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi()
  778. ->groupBy('product')
  779. ->fetchPairs();
  780. /* If products is empty, get all products. */
  781. if(empty($productIdList)) $productIdList = $this->dao->select('id')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->fetchPairs();
  782. $stats = array();
  783. foreach($productIdList as $productID)
  784. {
  785. $product = new stdclass();
  786. $product->draftEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['draft'] : 0;
  787. $product->activeEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['active'] : 0;
  788. $product->changingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['changing'] : 0;
  789. $product->reviewingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['reviewing'] : 0;
  790. $product->closedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['closed'] : 0;
  791. $product->finishedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['finished'] : 0;
  792. $product->totalEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['total'] : 0;
  793. $product->draftRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['draft'] : 0;
  794. $product->activeRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['active'] : 0;
  795. $product->changingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['changing'] : 0;
  796. $product->reviewingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['reviewing'] : 0;
  797. $product->closedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['closed'] : 0;
  798. $product->finishedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['finished'] : 0;
  799. $product->totalRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['total'] : 0;
  800. $product->draftStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['draft'] : 0;
  801. $product->activeStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['active'] : 0;
  802. $product->changingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['changing'] : 0;
  803. $product->reviewingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['reviewing'] : 0;
  804. $product->closedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['closed'] : 0;
  805. $product->finishedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['finished'] : 0;
  806. $product->totalStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['total'] : 0;
  807. $product->unresolvedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['unresolved'] : 0;
  808. $product->closedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['closed'] : 0;
  809. $product->fixedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['fixed'] : 0;
  810. $product->totalBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['total'] : 0;
  811. $product->plans = isset($plans[$productID]) ? $plans[$productID] : 0;
  812. $product->releases = isset($releases[$productID]) ? $releases[$productID] : 0;
  813. $stats[$productID] = $product;
  814. }
  815. return $stats;
  816. }
  817. /**
  818. * 获取产品下需求的统计数据。
  819. * Get story statistic data.
  820. *
  821. * @param array $productIdList
  822. * @access protected
  823. * @return array
  824. */
  825. protected function getStoryStats($productIdList)
  826. {
  827. $stories = $this->dao->select('product, status, `closedReason`, count(id) AS c, type')
  828. ->from(TABLE_STORY)
  829. ->where('deleted')->eq(0)
  830. ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi()
  831. ->groupBy('product, status, `closedReason`, type')
  832. ->fetchAll();
  833. $productStories = array();
  834. foreach($stories as $story)
  835. {
  836. if(!isset($productStories[$story->product][$story->type])) $productStories[$story->product][$story->type] = array('draft' => 0, 'active' => 0, 'changing' => 0, 'reviewing' => 0, 'finished' => 0, 'closed' => 0, 'total' => 0);
  837. if($story->status == 'draft') $productStories[$story->product][$story->type]['draft'] += $story->c;
  838. if($story->status == 'active') $productStories[$story->product][$story->type]['active'] += $story->c;
  839. if($story->status == 'changing') $productStories[$story->product][$story->type]['changing'] += $story->c;
  840. if($story->status == 'reviewing') $productStories[$story->product][$story->type]['reviewing'] += $story->c;
  841. if($story->status == 'closed') $productStories[$story->product][$story->type]['closed'] += $story->c;
  842. $productStories[$story->product][$story->type]['total'] += $story->c;
  843. if($story->status == 'closed' && $story->closedReason == 'done') $productStories[$story->product][$story->type]['finished'] += $story->c;
  844. }
  845. return $productStories;
  846. }
  847. /**
  848. * 获取产品下bug的统计数据。
  849. * Get bug statistic data.
  850. *
  851. * @param array $productIdList
  852. * @access protected
  853. * @return array
  854. */
  855. protected function getBugStats($productIdList)
  856. {
  857. $bugs = $this->dao->select('product,status,resolution, count(id) AS c')
  858. ->from(TABLE_BUG)
  859. ->where('deleted')->eq(0)
  860. ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi()
  861. ->groupBy('product, status, resolution')
  862. ->fetchAll();
  863. $productBugs = array();
  864. foreach($bugs as $bug)
  865. {
  866. if(!isset($productBugs[$bug->product])) $productBugs[$bug->product] = array('unresolved' => 0, 'fixed' => 0, 'closed' => 0, 'total' => 0);
  867. if($bug->status == 'active' || ($bug->status == 'closed' && $bug->resolution == 'postponed')) $productBugs[$bug->product]['unresolved'] += $bug->c;
  868. if($bug->status == 'closed' && $bug->resolution == 'fixed') $productBugs[$bug->product]['fixed'] += $bug->c;
  869. if($bug->status == 'closed') $productBugs[$bug->product]['closed'] += $bug->c;
  870. $productBugs[$bug->product]['total'] += $bug->c;
  871. }
  872. return $productBugs;
  873. }
  874. /**
  875. * 当产品线的项目集发生变化时,更新产品线下产品的项目集。
  876. * Sync program to product when line's program changed.
  877. *
  878. * @param int $programID
  879. * @param int $lineID
  880. * @access protected
  881. * @return void
  882. */
  883. protected function syncProgramToProduct($programID, $lineID)
  884. {
  885. $this->loadModel('action');
  886. $oldProducts = $this->dao->select('*')->from(TABLE_PRODUCT)->where('line')->eq($lineID)->fetchAll('id');
  887. $this->dao->update(TABLE_PRODUCT)->set('program')->eq($programID)->where('line')->eq($lineID)->exec();
  888. if(!dao::isError())
  889. {
  890. $products = $this->dao->select('*')->from(TABLE_PRODUCT)->where('line')->eq($lineID)->fetchAll('id');
  891. foreach($products as $productID => $product)
  892. {
  893. if($oldProducts[$productID]->program != $product->program)
  894. {
  895. $changes = common::createChanges($oldProducts[$productID], $product);
  896. $actionID = $this->action->create('product', $productID, 'ChangedProgram');
  897. $this->action->logHistory($actionID, $changes);
  898. }
  899. }
  900. }
  901. }
  902. }