model.php 120 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004
  1. <?php
  2. class projectModel extends model
  3. {
  4. /**
  5. * 根据对象类型获取访问控制列表。
  6. * Get access control list by object type.
  7. *
  8. * @param string $objectType
  9. * @param string $account
  10. * @access public
  11. * @return array
  12. */
  13. public function getAclListByObjectType($objectType, $account = '')
  14. {
  15. return $this->dao->select('id, account, objectType, objectID')->from(TABLE_ACL)->where('objectType')->in($objectType)
  16. ->beginIF($account)->andWhere('account')->eq($account)->fi()
  17. ->fetchAll('id');
  18. }
  19. /**
  20. * 根据权限控制范围获取项目。
  21. * Get projects by acl.
  22. *
  23. * @param string $acl
  24. * @param array $idList
  25. * @access public
  26. * @return array
  27. */
  28. public function getListByAcl($acl, $idList = [])
  29. {
  30. $projects = $this->getListByAclAndType($acl);
  31. if(!$projects) return [];
  32. if(!$idList) return $projects;
  33. return array_intersect_key($projects, array_flip($idList)) ?: [];
  34. }
  35. /**
  36. * 根据权限控制范围和类型获取项目。
  37. * Get projects by acl and type.
  38. *
  39. * @param string $acl
  40. * @param string $type
  41. * @access public
  42. * @return array
  43. */
  44. public function getListByAclAndType($acl, $type = '')
  45. {
  46. return $this->dao->select('id, project, type, parent, path, openedBy, PO, PM, QD, RD, acl')->from(TABLE_PROJECT)
  47. ->where('acl')->in($acl)
  48. ->beginIF($type)->andWhere('type')->in($type)->fi()
  49. ->filterTpl('skip')
  50. ->fetchAll('id');
  51. }
  52. /**
  53. * 根据类型获取团队。
  54. * Get teams by type.
  55. *
  56. * @param string $type
  57. * @param string $account
  58. * @access public
  59. * @return array
  60. */
  61. public function getTeamListByType($type, $account = '')
  62. {
  63. return $this->dao->select('id, root, type, account')->from(TABLE_TEAM)->where('type')->in($type)
  64. ->beginIF($account)->andWhere('account')->eq($account)->fi()
  65. ->fetchAll('id');
  66. }
  67. /**
  68. * 获取当前登录用户有权限查看的项目列表.
  69. * Get project list by current user.
  70. *
  71. * @param string $fields
  72. * @param string $filterTpl
  73. * @access public
  74. * @return array
  75. */
  76. public function getListByCurrentUser($fields = '*', $filterTpl = '')
  77. {
  78. if($filterTpl == 'skip') dao::$filterTpl = 'skip';
  79. return $this->dao->select($fields)->from(TABLE_PROJECT)
  80. ->where('type')->eq('project')
  81. ->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi()
  82. ->andWhere('deleted')->eq(0)
  83. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
  84. ->beginIF($filterTpl == 'onlyTpl')->andWhere('isTpl')->eq('1')->fi()
  85. ->orderBy('order_asc,id_desc')
  86. ->fetchAll('id');
  87. }
  88. /**
  89. * 获取当前登录用户参与的项目列表.
  90. * Get project list by current user.
  91. *
  92. * @param string $fields
  93. * @access public
  94. * @return array
  95. */
  96. public function getInvolvedListByCurrentUser($fields = 't1.*')
  97. {
  98. $stmt = $this->dao->select($fields)->from(TABLE_PROJECT)->alias('t1');
  99. $stmt = $this->leftJoinInvolvedTable($stmt);
  100. $stmt->where('t1.type')->eq('project')
  101. ->beginIF($this->config->vision)->andWhere('t1.vision')->eq($this->config->vision)->fi()
  102. ->andWhere('t1.deleted')->eq(0)
  103. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->projects)->fi();
  104. $stmt = $this->appendInvolvedCondition($stmt);
  105. return $stmt->orderBy('order_asc,id_desc')
  106. ->fetchAll('id');
  107. }
  108. /**
  109. * 左链接 "我参与的" 条件需要的表,传入的$stmt t1应为 TABLE_PROJECT 表
  110. * LeftJoin "I participate" condition required table.
  111. *
  112. * @param object $stmt
  113. * @access public
  114. * @return object
  115. */
  116. public function leftJoinInvolvedTable($stmt)
  117. {
  118. return $stmt->leftJoin(TABLE_TEAM)->alias('t2')->on('t1.id = t2.root')
  119. ->leftJoin(TABLE_STAKEHOLDER)->alias('t3')->on('t1.id=t3.objectID');
  120. }
  121. /**
  122. * 添加 "我参与的" 条件所需的查询条件,项目的PM、项目的成员、项目的干系人、项目的白名单
  123. * Add the query condition for the "I Participate".
  124. *
  125. * @param object $stmt
  126. * @access public
  127. * @return object
  128. */
  129. public function appendInvolvedCondition($stmt)
  130. {
  131. return $stmt->andWhere('t2.type')->eq('project')
  132. ->andWhere('t1.openedBy', true)->eq($this->app->user->account)
  133. ->orWhere('t1.PM')->eq($this->app->user->account)
  134. ->orWhere('t2.account')->eq($this->app->user->account)
  135. ->orWhere('(t3.user')->eq($this->app->user->account)
  136. ->andWhere('t3.deleted')->eq(0)
  137. ->markRight(1)
  138. ->orWhere("CONCAT(',', t1.whitelist, ',')")->like("%,{$this->app->user->account},%")
  139. ->markRight(1);
  140. }
  141. /**
  142. * 查找项目执行下关联的产品
  143. * Get linked products with execution under the project.
  144. *
  145. * @param array $executionIDs
  146. *
  147. * @access protected
  148. * @return array
  149. */
  150. public function getExecutionProductGroup($executionIDs)
  151. {
  152. return $this->dao->select('project,product')->from(TABLE_PROJECTPRODUCT)
  153. ->where('project')->in($executionIDs)
  154. ->fetchGroup('project', 'product');
  155. }
  156. /**
  157. * 检查用户是否有查看项目的权限。
  158. * Check the privilege.
  159. *
  160. * @param int $projectID
  161. * @access public
  162. * @return bool
  163. */
  164. public function checkPriv($projectID)
  165. {
  166. return !empty($projectID) && ($this->app->user->admin || (strpos(",{$this->app->user->view->projects},", ",{$projectID},") !== false));
  167. }
  168. /**
  169. * 判断项目操作的权限。
  170. * Judge an action is clickable or not.
  171. *
  172. * @param object $project
  173. * @param string $action
  174. * @access public
  175. * @return bool
  176. */
  177. public static function isClickable($project, $action)
  178. {
  179. global $config;
  180. $action = strtolower($action);
  181. if($action == 'publishtemplate') return $project->status == 'wait' || $project->status == 'closed';
  182. if($action == 'disabletemplate') return $project->status == 'doing';
  183. if($action == 'deletedeliverable') return !$project->review && empty($project->systemList) && $project->status == 'draft';
  184. if($action == 'close') return $project->status != 'closed';
  185. if($action == 'group') return $project->model != 'kanban';
  186. if($action == 'start') return $project->status == 'wait' || $project->status == 'suspended';
  187. if($action == 'finish') return $project->status == 'wait' || $project->status == 'doing';
  188. if($action == 'suspend') return $project->status == 'wait' || $project->status == 'doing';
  189. if($action == 'activate') return $project->status == 'done' || $project->status == 'closed';
  190. if($action == 'whitelist') return $project->acl != 'open';
  191. if($action == 'link') return $project->hasProduct;
  192. return true;
  193. }
  194. /**
  195. * 检查用户是否可以访问当前项目。
  196. * Check whether access to the current project is allowed or not.
  197. *
  198. * @param int $projectID
  199. * @param array $projects
  200. * @access public
  201. * @return int|false
  202. */
  203. public function checkAccess($projectID = 0, $projects = array())
  204. {
  205. if(commonModel::isTutorialMode()) return $projectID;
  206. if(!$projectID)
  207. {
  208. if($this->cookie->lastProject) $projectID = $this->cookie->lastProject;
  209. if(!$projectID) $projectID = $this->session->project ? $this->session->project : (int)key($projects);
  210. }
  211. /* 项目模板不校验访问权限。 */
  212. $isTpl = $this->dao->select('isTpl')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('isTpl');
  213. if(empty($isTpl) && !isset($projects[$projectID]))
  214. {
  215. if($projectID && strpos(",{$this->app->user->view->projects},", ",{$projectID},") === false && !empty($projects))
  216. {
  217. /* Redirect old project to new project. */
  218. $projectID = $this->dao->select('project')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('project');
  219. if(!$projectID || strpos(",{$this->app->user->view->projects},", ",{$projectID},") === false) return false;
  220. }
  221. else
  222. {
  223. $projectID = key($projects);
  224. }
  225. }
  226. $this->session->set('project', (int)$projectID, $this->app->tab);
  227. if($projectID) helper::setcookie('lastProject', (string)$projectID);
  228. return (int)$projectID;
  229. }
  230. /**
  231. * 获取项目的预算单位列表。
  232. * Get budget unit list.
  233. *
  234. * @access public
  235. * @return array
  236. */
  237. public function getBudgetUnitList()
  238. {
  239. $budgetUnitList = array();
  240. if($this->config->vision != 'lite')
  241. {
  242. foreach(explode(',', $this->config->project->unitList) as $unit) $budgetUnitList[$unit] = zget($this->lang->project->currencySymbol, $unit, '') . ' ' . zget($this->lang->project->unitList, $unit, '');
  243. }
  244. return $budgetUnitList;
  245. }
  246. /**
  247. * 获取有迭代项目关联的产品列表。
  248. * Get Multiple linked products for project.
  249. *
  250. * @param int $projectID
  251. * @access public
  252. * @return array
  253. */
  254. public function getMultiLinkedProducts($projectID)
  255. {
  256. $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs();
  257. return $this->dao->select('t3.id,t3.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  258. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  259. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id')
  260. ->where('t1.product')->in($linkedProducts)
  261. ->andWhere('t1.project')->ne($projectID)
  262. ->andWhere('t2.type')->eq('project')
  263. ->andWhere('t2.deleted')->eq('0')
  264. ->andWhere('t3.deleted')->eq('0')
  265. ->fetchPairs('id', 'name');
  266. }
  267. /**
  268. * 根据项目ID获取项目信息。
  269. * Get a project by id.
  270. *
  271. * @param int $projectID
  272. * @param string $type
  273. * @access public
  274. * @return object|false
  275. */
  276. public function getByID($projectID, $type = '')
  277. {
  278. /* Using demo data during tutorials. */
  279. if(commonModel::isTutorialMode()) return empty($projectID) ? false : $this->loadModel('tutorial')->getProject();
  280. /* Get project info. */
  281. $project = $this->projectTao->fetchProjectInfo($projectID, $type);
  282. if(!$project) return false;
  283. /* Replace image url. */
  284. $project = $this->loadModel('file')->replaceImgURL($project, 'desc');
  285. return $project;
  286. }
  287. /**
  288. * 通过影子产品ID获取一条项目记录。
  289. * Get a project by its shadow product.
  290. *
  291. * @param int $productID
  292. * @access public
  293. * @return object|false
  294. */
  295. public function getByShadowProduct($productID)
  296. {
  297. return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  298. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  299. ->where('t1.product')->eq($productID)
  300. ->andWhere('t2.type')->eq('project')
  301. ->limit(1)
  302. ->fetch();
  303. }
  304. /**
  305. * 根据状态和和我参与的查询项目列表。
  306. * Get project list by status and with my participation.
  307. *
  308. * @param string $status
  309. * @param string $orderBy
  310. * @param bool $involved
  311. * @param object|null $pager
  312. * @access public
  313. * @return array
  314. */
  315. public function getList($status = 'undone', $orderBy = 'order_desc', $involved = false, $pager = null)
  316. {
  317. /* Get project list by status. */
  318. $projects = $this->projectTao->fetchProjectList($status, $orderBy, $involved, $pager);
  319. if(empty($projects)) return array();
  320. /* Get team members and estimates under the project. */
  321. $projectIdList = array_keys($projects);
  322. $teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList);
  323. $estimates = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'estimate');
  324. /* Set project attribute. */
  325. $this->app->loadClass('pager', true);
  326. foreach($projects as $projectID => $project)
  327. {
  328. $orderBy = in_array($project->model, array('waterfall', 'ipd')) ? 'id_asc' : 'id_desc';
  329. $pager = in_array($project->model, array('waterfall', 'ipd')) ? null : new pager(0, 1, 1);
  330. $project->executions = $this->loadModel('execution')->getStatData($projectID, 'undone', 0, 0, false, '', $orderBy, $pager);
  331. $project->teamCount = isset($teamCount[$projectID]) ? $teamCount[$projectID] : 0;
  332. $project->estimate = isset($estimates[$projectID]) ? round((float)$estimates[$projectID]->estimate, 2) : 0;
  333. $project->parentName = $project->parent ? $this->projectTao->getParentProgram($project->path, $project->grade) : '';
  334. }
  335. return $projects;
  336. }
  337. /**
  338. * 获取项目列表区块的数据。
  339. * Get project list for block.
  340. *
  341. * @param string $status
  342. * @param int $projectID
  343. * @param string $orderBy
  344. * @param int $limit
  345. * @param string $excludedModel
  346. * @access public
  347. * @return object[]
  348. */
  349. public function getOverviewList($status = '', $projectID = 0, $orderBy = 'id_desc', $limit = 10, $excludedModel = '')
  350. {
  351. /* Get project list by query. */
  352. $projects = $this->projectTao->fetchProjectListByQuery($status, $projectID, $orderBy, $limit, $excludedModel);
  353. if(empty($projects)) return array();
  354. if($projectID)
  355. {
  356. if(empty($projects[$projectID])) return array();
  357. $projects = array($projectID => $projects[$projectID]);
  358. }
  359. /* Get bug, task and story summary under the project. */
  360. $projectIdList = array_keys($projects);
  361. $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList);
  362. $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList);
  363. $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList);
  364. /* Set project attribute. */
  365. $today = helper::today();
  366. foreach($projects as $projectID => $project)
  367. {
  368. $project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0;
  369. $project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0;
  370. $project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0;
  371. $project->allStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->allStories : 0;
  372. $project->doneStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->doneStories : 0;
  373. $project->leftStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->leftStories : 0;
  374. $project->leftTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->leftTasks : 0;
  375. $project->allTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->allTasks : 0;
  376. $project->waitTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->waitTasks : 0;
  377. $project->doingTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doingTasks : 0;
  378. $project->rndDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doneTasks : 0;
  379. $project->liteDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->litedoneTasks : 0;
  380. /* Judge whether the project is delayed. */
  381. if($project->status != 'done' && $project->status != 'closed' && $project->status != 'suspended')
  382. {
  383. $delay = helper::diffDate($today, $project->end);
  384. if($delay > 0) $project->delay = $delay;
  385. }
  386. }
  387. return $projects;
  388. }
  389. /**
  390. * 获取瀑布项目的进度。
  391. * Get waterfall project progress.
  392. *
  393. * @param array $projectIdList
  394. * @param string $mode waterfall|research
  395. * @access public
  396. * @return array
  397. */
  398. public function getWaterfallProgress($projectIdList, $mode = 'waterfall')
  399. {
  400. /* Get stage list. */
  401. $stageGroup = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1')
  402. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  403. ->where('t1.type')->in('stage')
  404. ->andWhere('t1.deleted')->eq('0')
  405. ->andWhere('t1.vision')->eq($this->config->vision)
  406. ->andWhere('t1.project')->in($projectIdList)
  407. ->andWhere('t2.model')->eq($mode)
  408. ->fetchGroup('project', 'id');
  409. /* Get hours information. */
  410. $totalHour = $this->dao->select("t1.project, t1.execution, ROUND(SUM(if(t1.status !='closed' && t1.status !='cancel', t1.`left`, 0)), 2) AS totalLeft, ROUND(SUM(t1.`consumed`), 1) AS totalConsumed")->from(TABLE_TASK)->alias('t1')
  411. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
  412. ->where('t2.project')->in(array_keys($stageGroup))
  413. ->andWhere('t1.deleted')->eq(0)
  414. ->andWhere('t1.parent')->lt(1)
  415. ->groupBy('t1.project,t1.execution')
  416. ->fetchGroup('project', 'execution');
  417. /* Compute waterfall project progress. */
  418. $progressList = array();
  419. foreach($stageGroup as $projectID => $stageList)
  420. {
  421. $projectConsumed = 0;
  422. $projectLeft = 0;
  423. foreach($stageList as $stageID => $stage)
  424. {
  425. if($stage->project != $projectID) continue;
  426. $projectConsumed += isset($totalHour[$projectID][$stageID]) ? (float)$totalHour[$projectID][$stageID]->totalConsumed : 0;
  427. $projectLeft += isset($totalHour[$projectID][$stageID]) ? round((float)$totalHour[$projectID][$stageID]->totalLeft, 1) : 0;
  428. }
  429. $progressList[$projectID] = ($projectConsumed + $projectLeft) == 0 ? 0 : floor($projectConsumed / ($projectConsumed + $projectLeft) * 1000) / 1000 * 100;
  430. }
  431. return $progressList;
  432. }
  433. /**
  434. * 获取瀑布项目的计划值、挣值和实际成本。
  435. * Get waterfall general PV, EV and AC.
  436. *
  437. * @param int $projectID
  438. * @access public
  439. * @return array
  440. */
  441. public function getWaterfallPVEVAC($projectID)
  442. {
  443. $executions = $this->dao->select('id,begin,end,realEnd,status')->from(TABLE_EXECUTION)->where('deleted')->eq(0)->andWhere('vision')->eq($this->config->vision)->andWhere('project')->eq($projectID)->fetchAll('id');
  444. $stmt = $this->dao->select('id,status,estimate,consumed,`left`,closedReason')->from(TABLE_TASK)->where('execution')->in(array_keys($executions))->andWhere("isParent")->eq(0)->andWhere("deleted")->eq(0)->andWhere('status')->ne('cancel')->query();
  445. $PV = 0;
  446. $EV = 0;
  447. $left = 0;
  448. while($task = $stmt->fetch())
  449. {
  450. $PV += $task->estimate;
  451. $left += $task->left;
  452. if($task->status == 'done' or $task->closedReason == 'done')
  453. {
  454. $EV += $task->estimate;
  455. }
  456. else
  457. {
  458. $task->progress = 0;
  459. if(($task->consumed + $task->left) > 0) $task->progress = round($task->consumed / ($task->consumed + $task->left) * 100, 2);
  460. $EV += round($task->estimate * $task->progress / 100, 2);
  461. }
  462. }
  463. $AC = $this->dao->select('SUM(consumed) as consumed')->from(TABLE_EFFORT)
  464. ->where('deleted')->eq(0)
  465. ->andWhere('project')->eq($projectID)
  466. ->fetch('consumed');
  467. if(is_null($AC)) $AC = 0;
  468. return array('PV' => sprintf("%.2f", $PV), 'EV' => sprintf("%.2f", $EV), 'AC' => sprintf("%.2f", $AC), 'left' => sprintf("%.2f", $left));
  469. }
  470. /**
  471. * 获取项目的工时信息。
  472. * Get project workhour info.
  473. *
  474. * @param int $projectID
  475. * @access public
  476. * @return object
  477. */
  478. public function getWorkhour($projectID)
  479. {
  480. $totalEstimate = $this->dao->select('ROUND(SUM(t1.estimate), 1) AS totalEstimate')->from(TABLE_TASK)->alias('t1')
  481. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
  482. ->where('t2.project')->in($projectID)
  483. ->andWhere('t2.deleted')->eq(0)
  484. ->andWhere('t1.deleted')->eq(0)
  485. ->andWhere('t1.parent')->lt(1)
  486. ->fetch('totalEstimate');
  487. $totalConsumed = $this->dao->select('ROUND(SUM(t1.consumed), 1) AS totalConsumed')->from(TABLE_TASK)->alias('t1')
  488. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
  489. ->where('t2.project')->in($projectID)
  490. ->andWhere('t2.deleted')->eq(0)
  491. ->andWhere('t1.deleted')->eq(0)
  492. ->andWhere('t1.parent')->lt(1)
  493. ->fetch('totalConsumed');
  494. $totalLeft = $this->dao->select('ROUND(SUM(t1.`left`), 1) AS totalLeft')->from(TABLE_TASK)->alias('t1')
  495. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
  496. ->where('t2.project')->in($projectID)
  497. ->andWhere('t2.deleted')->eq(0)
  498. ->andWhere('t1.deleted')->eq(0)
  499. ->andWhere('t1.parent')->lt(1)
  500. ->andWhere('t1.status')->ne('closed,cancel')
  501. ->fetch('totalLeft');
  502. $workhour = new stdclass();
  503. $workhour->totalHours = $this->dao->select('sum(t1.days * t1.hours) AS totalHours')->from(TABLE_TEAM)->alias('t1')
  504. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.root=t2.id')
  505. ->leftJoin(TABLE_USER)->alias('t3')->on('t1.account=t3.account')
  506. ->where('t2.id')->in($projectID)
  507. ->andWhere('t2.deleted')->eq(0)
  508. ->andWhere('t1.type')->eq('project')
  509. ->andWhere('t3.deleted')->eq(0)
  510. ->fetch('totalHours');
  511. $workhour->totalHours = empty($workhour->totalHours) ? 0 : $workhour->totalHours;
  512. $workhour->totalEstimate = empty($totalEstimate) ? 0 : $totalEstimate;
  513. $workhour->totalConsumed = empty($totalConsumed) ? 0 : $totalConsumed;
  514. $workhour->totalLeft = empty($totalLeft) ? 0 : $totalLeft;
  515. return $workhour;
  516. }
  517. /**
  518. * 获取给定项目的总计消耗工时。
  519. * Get projects consumed info.
  520. *
  521. * @param array $projectID
  522. * @param string $time
  523. * @access public
  524. * @return object[]
  525. * @param mixed[] $projectIdList
  526. */
  527. public function getProjectsConsumed($projectIdList, $time = '')
  528. {
  529. $totalConsumeds = $this->dao->select('t2.project,ROUND(SUM(t1.consumed), 1) AS totalConsumed')->from(TABLE_EFFORT)->alias('t1')
  530. ->leftJoin(TABLE_TASK)->alias('t2')->on("t1.objectID=t2.id and t1.objectType = 'task'")
  531. ->where('t2.project')->in($projectIdList)
  532. ->andWhere('t2.deleted')->eq(0)
  533. ->andWhere('t2.parent')->lt(1)
  534. ->beginIF($time == 'THIS_YEAR')->andWhere('LEFT(t1.`date`, 4)')->eq(date('Y'))->fi()
  535. ->groupBy('t2.project')
  536. ->fetchAll('project');
  537. $projects = array();
  538. foreach($projectIdList as $projectID)
  539. {
  540. $project = new stdClass();
  541. $project->totalConsumed = isset($totalConsumeds[$projectID]->totalConsumed) ? $totalConsumeds[$projectID]->totalConsumed : 0;
  542. $projects[$projectID] = $project;
  543. }
  544. return $projects;
  545. }
  546. /**
  547. * 生成项目下拉框跳转链接
  548. * Create the link from module,method.
  549. *
  550. * @param string $module
  551. * @param string $method
  552. * @param int $projectID
  553. * @access public
  554. * @return string
  555. * @param string $extra
  556. */
  557. public function getProjectLink($module, $method, $projectID, $extra = '')
  558. {
  559. $link = helper::createLink('project', 'index', "projectID=%s");
  560. $project = $this->projectTao->fetchProjectInfo($projectID);
  561. if(empty($project->multiple)) return $link;
  562. if($module == 'build' && $method !== 'create')
  563. {
  564. $fromModule = $this->app->tab == 'project' ? 'projectbuild' : 'project';
  565. $fromMethod = $this->app->tab == 'project' ? 'browse' : 'build';
  566. return helper::createLink($fromModule, $fromMethod, "projectID=%s");
  567. }
  568. if(!empty($this->config->project->linkMap->$module[$method]))
  569. {
  570. $linkParams = $this->config->project->linkMap->$module[$method];
  571. if(!$linkParams[0]) $linkParams[0] = $module;
  572. if(!$linkParams[1]) $linkParams[1] = $method;
  573. if(strpos($linkParams[2], '{extra}') !== false) $linkParams[2] = str_replace('{extra}', $extra, $linkParams[2]);
  574. return helper::createLink($linkParams[0], $linkParams[1], $linkParams[2]) . $linkParams[3];
  575. }
  576. if(!empty($this->config->project->linkMap->$module['']))
  577. {
  578. $linkParams = $this->config->project->linkMap->$module[''];
  579. if(!$linkParams[0]) $linkParams[0] = $module;
  580. return helper::createLink($linkParams[0], $linkParams[1], $linkParams[2]) . $linkParams[3];
  581. }
  582. if($this->config->edition != 'open')
  583. {
  584. $flow = $this->loadModel('workflow')->getByModule($module);
  585. if(!empty($flow->app) && in_array($flow->app, array('scrum', 'waterfall', 'kanbanProject'))) $flow->app = 'project';
  586. if(!empty($flow) && $flow->buildin == '0') return helper::createLink('flow', 'ajaxSwitchBelong', "objectID=%s&moduleName=$module") . "#app=$flow->app";
  587. }
  588. if(in_array($module, $this->config->waterfallModules)) return helper::createLink($module, 'browse', "projectID=%s");
  589. return $link;
  590. }
  591. /**
  592. * Get project stat data .
  593. *
  594. * @param int $projectID
  595. * @access public
  596. * @return object
  597. */
  598. public function getStatData($projectID)
  599. {
  600. $executions = $this->loadModel('execution')->getPairs($projectID);
  601. $storyCount = $this->dao->select('count(t2.story) as storyCount')->from(TABLE_STORY)->alias('t1')
  602. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story')
  603. ->where('t2.project')->eq($projectID)
  604. ->andWhere('t1.deleted')->eq(0)
  605. ->fetch('storyCount');
  606. $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG)
  607. ->where('project')->in($projectID)
  608. ->andWhere('deleted')->eq(0)
  609. ->fetch('bugCount');
  610. $taskCount = $this->dao->select("COUNT(1) AS `count`,
  611. SUM(CASE WHEN `status` = 'wait' THEN 1 ELSE 0 END) AS `waitCount`,
  612. SUM(CASE WHEN `status` = 'doing' THEN 1 ELSE 0 END) AS `doingCount`,
  613. SUM(CASE WHEN `finishedBy` != '' THEN 1 ELSE 0 END) AS `finishedCount`")
  614. ->from(TABLE_TASK)
  615. ->where('execution')->in(array_keys($executions))
  616. ->andWhere('deleted')->eq('0')
  617. ->fetch();
  618. $delayedCount = $this->dao->select('COUNT(id) AS `count`')->from(TABLE_TASK)
  619. ->where('execution')->in(array_keys($executions))
  620. ->andWhere('deadline')->notZeroDate()
  621. ->andWhere('deadline')->lt(helper::today())
  622. ->andWhere('status')->in('wait,doing')
  623. ->andWhere('deleted')->eq(0)
  624. ->fetch('count');
  625. $statData = new stdclass();
  626. $statData->storyCount = $storyCount;
  627. $statData->bugCount = $bugCount;
  628. $statData->taskCount = $taskCount->count;
  629. $statData->waitCount = $taskCount->waitCount;
  630. $statData->doingCount = $taskCount->doingCount;
  631. $statData->finishedCount = $taskCount->finishedCount;
  632. $statData->delayedCount = $delayedCount;
  633. return $statData;
  634. }
  635. /**
  636. * Get project pairs.
  637. *
  638. * @param bool $ignoreVision
  639. * @param string $params
  640. * @access public
  641. * @return object
  642. */
  643. public function getPairs($ignoreVision = false, $params = '')
  644. {
  645. return $this->dao->select('id, name')->from(TABLE_PROJECT)
  646. ->where('type')->eq('project')
  647. ->andWhere('deleted')->eq(0)
  648. ->beginIF(!$ignoreVision)->andWhere('vision')->eq($this->config->vision)->fi()
  649. ->beginIF(strpos($params, 'noproduct') !== false)->andWhere('hasProduct')->eq(0)->fi()
  650. ->beginIF(strpos($params, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi()
  651. ->beginIF(strpos($params, 'nosprint') !== false)->andWhere('multiple')->eq('0')->fi()
  652. ->beginIF(strpos($params, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi()
  653. ->beginIF(strpos($params, 'nokanban') !== false)->andWhere('model')->ne('kanban')->fi()
  654. ->beginIF(!$this->app->user->admin && strpos($params, 'haspriv') !== false)->andWhere('id')->in($this->app->user->view->projects)->fi()
  655. ->fetchPairs();
  656. }
  657. /**
  658. * Get project pairs by programID.
  659. *
  660. * @param int $programID
  661. * @param string $status all|wait|doing|suspended|closed|noclosed|noprogram
  662. * @param bool $isQueryAll
  663. * @param string $orderBy
  664. * @param string $excludedModel
  665. * @param string|array $model
  666. * @param string $param multiple|product
  667. * @access public
  668. * @return array
  669. */
  670. public function getPairsByProgram($programID = 0, $status = 'all', $isQueryAll = false, $orderBy = 'order_asc', $excludedModel = '', $model = '', $param = '')
  671. {
  672. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProjectPairs();
  673. return $this->dao->select('id, name')->from(TABLE_PROJECT)
  674. ->where('type')->eq('project')
  675. ->andWhere('deleted')->eq(0)
  676. ->andWhere('vision')->eq($this->config->vision)
  677. ->beginIF(!empty($programID))->andWhere('path')->like("%,$programID,%")->fi()
  678. ->beginIF($programID === 0 && $status == 'noprogram')->andWhere('parent')->eq(0)->fi()
  679. ->beginIF($status != 'all' && $status != 'noclosed')->andWhere('status')->eq($status)->fi()
  680. ->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi()
  681. ->beginIF($model)->andWhere('model')->in($model)->fi()
  682. ->beginIF(strpos($param, 'multiple') !== false)->andWhere('multiple')->eq(1)->fi()
  683. ->beginIF(strpos($param, 'product') !== false)->andWhere('hasProduct')->eq(1)->fi()
  684. ->beginIF($status == 'noclosed')->andWhere('status')->ne('closed')->fi()
  685. ->beginIF(!$this->app->user->admin && !$isQueryAll)->andWhere('id')->in($this->app->user->view->projects)->fi()
  686. ->orderBy($orderBy)
  687. ->fetchPairs();
  688. }
  689. /**
  690. * 获取关联产品的项目列表信息。
  691. * Get product list information for linked products.
  692. *
  693. * @param array $productIdList
  694. * @param string $status
  695. * @access public
  696. * @return array
  697. */
  698. public function getGroupByProduct($productIdList = array(), $status = '')
  699. {
  700. return $this->dao->select('t1.product,t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  701. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  702. ->where('t2.deleted')->eq(0)
  703. ->andWhere('t2.type')->eq('project')
  704. ->beginIF(!empty($productIdList))->andWhere('t1.product')->in($productIdList)->fi()
  705. ->beginIF(!empty($status))->andWhere('t2.status')->in($status)->fi()
  706. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
  707. ->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi()
  708. ->fetchGroup('product');
  709. }
  710. /**
  711. * Get all the projects under the program set to which an project belongs.
  712. *
  713. * @param object $project
  714. * @access public
  715. * @return void
  716. */
  717. public function getBrotherProjects($project)
  718. {
  719. if($project->parent == 0) return array($project->id => $project->id);
  720. $projectIds = array_filter(explode(',', $project->path));
  721. $parentProgram = $this->dao->select('*')->from(TABLE_PROGRAM)
  722. ->where('id')->in($projectIds)
  723. ->andWhere('`type`')->eq('program')
  724. ->orderBy('grade desc')
  725. ->fetch();
  726. return $this->dao->select('id')->from(TABLE_PROJECT)
  727. ->where('type')->eq('project')
  728. ->andWhere('deleted')->eq(0)
  729. ->andWhere("CONCAT(',', path, ',')")->like("%,{$parentProgram->id},%")
  730. ->fetchPairs('id');
  731. }
  732. /**
  733. * Get project by id list.
  734. *
  735. * @param array $projectIdList
  736. * @param string $mode all
  737. * @access public
  738. * @return object
  739. */
  740. public function getByIdList($projectIdList = array(), $mode = '')
  741. {
  742. return $this->dao->select('*')->from(TABLE_PROJECT)
  743. ->where('type')->eq('project')
  744. ->andWhere('id')->in($projectIdList)
  745. ->beginIF(!$this->app->user->admin and $mode != 'all')->andWhere('id')->in($this->app->user->view->projects)->fi()
  746. ->fetchAll('id');
  747. }
  748. /**
  749. * Get project pairs by id list.
  750. *
  751. * @param array $projectIdList
  752. * @param string $model
  753. * @param string $param
  754. * @access public
  755. * @return array
  756. */
  757. public function getPairsByIdList($projectIdList = array(), $model = '', $param = '')
  758. {
  759. return $this->dao->select('id, name')->from(TABLE_PROJECT)
  760. ->where('type')->eq('project')
  761. ->andWhere('deleted')->eq(0)
  762. ->beginIF($projectIdList)->andWhere('id')->in($projectIdList)->fi()
  763. ->beginIF(!$this->app->user->admin and $model != 'all')->andWhere('id')->in($this->app->user->view->projects)->fi()
  764. ->beginIF($model != 'all' and !empty($model))->andWhere('model')->in($model)->fi()
  765. ->beginIF(strpos($param, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi()
  766. ->fetchPairs('id', 'name');
  767. }
  768. /**
  769. * 根据项目ID获取关联产品及分支
  770. * Get branches by project id.
  771. *
  772. * @param int $projectID
  773. *
  774. * @access public
  775. * @return array
  776. */
  777. public function getBranchesByProject($projectID)
  778. {
  779. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBranchesByProject();
  780. return $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)
  781. ->where('project')->eq($projectID)
  782. ->fetchGroup('product', 'branch');
  783. }
  784. /**
  785. * 根据项目ID获取分支分组。
  786. * Get branch groups.
  787. *
  788. * @param int $projectID
  789. * @param array $productIdList
  790. * @access public
  791. * @return array
  792. */
  793. public function getBranchGroup($projectID, $productIdList)
  794. {
  795. return $this->dao->select('t1.product as productID, t1.branch as branchID, t2.*')
  796. ->from(TABLE_PROJECTPRODUCT)->alias('t1')
  797. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  798. ->where('t1.product')->in($productIdList)
  799. ->andWhere('t1.project')->eq($projectID)
  800. ->andWhere('t2.deleted')->eq(0)
  801. ->fetchGroup('productID', 'branchID');
  802. }
  803. /**
  804. * Get No product project|execution List.
  805. *
  806. * @access public
  807. * @return array
  808. */
  809. public function getNoProductList()
  810. {
  811. return $this->dao->select('t1.product, t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  812. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  813. ->where('t2.hasProduct')->eq(0)
  814. ->andWhere('t2.deleted')->eq(0)
  815. ->fetchAll('id');
  816. }
  817. /**
  818. * 获取项目与执行的键值对
  819. * Get project and execution pairs.
  820. *
  821. * @param string $multiple
  822. * @param string $status
  823. * @access public
  824. * @return array
  825. */
  826. public function getProjectExecutionPairs($multiple = '0', $status = 'all')
  827. {
  828. return $this->dao->select('project, id')->from(TABLE_PROJECT)
  829. ->where('deleted')->eq('0')
  830. ->andWhere('multiple')->eq($multiple)
  831. ->andWhere('type')->in('sprint,stage,kanban')
  832. ->beginIF(!in_array($status, array('all', 'undone')))->andWhere('status')->eq($status)->fi()
  833. ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
  834. ->beginIF($this->config->vision)->andWhere("CONCAT(',', vision, ',')")->like("%,{$this->config->vision},%")->fi()
  835. ->fetchPairs();
  836. }
  837. /**
  838. * 根据状态和项目模型获取项目列表。
  839. * Get project list by query.
  840. *
  841. * @param string $status
  842. * @param string $order
  843. * @param int $limit
  844. * @param string $excludedModel
  845. * @access public
  846. * @return void
  847. */
  848. public function getProjectList($status, $order, $limit, $excludedModel)
  849. {
  850. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getProjectStats();
  851. return $this->projectTao->fetchProjectListByQuery($status, 0, $order, $limit, $excludedModel);
  852. }
  853. /**
  854. * 获取项目下的执行数据。
  855. * Get execution data under the project.
  856. *
  857. * @param array $projectIdList
  858. * @access public
  859. * @return object[]
  860. */
  861. public function getExecutionList($projectIdList = array())
  862. {
  863. return $this->dao->select('*')->from(TABLE_EXECUTION)
  864. ->where('deleted')->eq(0)
  865. ->andWhere('project')->in($projectIdList)
  866. ->andWhere('type')->in('sprint,stage,kanban')
  867. ->fetchAll('id');
  868. }
  869. /**
  870. * 根据项目类型生成权限数据。
  871. * Get project priv data according by the project type.
  872. *
  873. * @param string $model scrum|waterfall|noSprint|agileplus|waterfallplus
  874. * @param int $projectID
  875. * @param int $hasProduct
  876. * @access public
  877. * @return object|false
  878. */
  879. public function getPrivsByModel($model = 'waterfall', $projectID = 0, $hasProduct = 0)
  880. {
  881. if(!isset($this->config->programPriv->$model)) return false;
  882. if($projectID) $project = $this->fetchByID($projectID);
  883. if($model == 'noSprint') $this->config->project->includedPriv = $this->config->project->noSprintPriv;
  884. if(!$hasProduct) $this->config->project->includedPriv = array_merge($this->config->project->includedPriv, $this->config->project->noProductPriv);
  885. if(isset($project->coverExecutionPriv) && empty($project->coverExecutionPriv)) $this->config->project->includedPriv = $this->config->project->projectPriv;
  886. $hasBaseline = true;
  887. $hasAuditplan = true;
  888. $hasProcess = true;
  889. $hasDeliverable = true;
  890. $hasChange = true;
  891. if($this->config->edition != 'open' && $projectID)
  892. {
  893. $hasBaseline = $this->loadModel('workflowgroup')->hasFeature((int)$project->workflowGroup, 'cm');
  894. $hasDeliverable = $this->workflowgroup->hasFeature((int)$project->workflowGroup, 'deliverable');
  895. $hasChange = $this->workflowgroup->hasFeature((int)$project->workflowGroup, 'change');
  896. $hasAuditplan = $this->workflowgroup->hasFeature((int)$project->workflowGroup, 'auditplan');
  897. $hasProcess = $this->workflowgroup->hasFeature((int)$project->workflowGroup, 'process');
  898. }
  899. $this->app->loadLang('group');
  900. $privs = new stdclass();
  901. foreach($this->lang->resource as $module => $methods)
  902. {
  903. if(empty($methods)) continue;
  904. if(!in_array($module, $this->config->programPriv->$model)) continue;
  905. if($module == 'cm' && !$hasBaseline) continue;
  906. if($module == 'auditplan' && !$hasAuditplan) continue;
  907. if($module == 'pssp' && !$hasProcess) continue;
  908. if($module == 'projectchange' && !$hasChange) continue;
  909. if($module == 'review' && !$hasDeliverable) continue;
  910. if($module == 'reviewissue' && !$hasDeliverable) continue;
  911. if($module == 'review')
  912. {
  913. if(!$hasDeliverable) unset($methods->submitDeliverable);
  914. if(!$hasChange) unset($methods->submitProjectchange);
  915. if(!$hasBaseline) unset($methods->submitBaseline);
  916. if(!empty($project) && $project->model == 'scrum') unset($methods->submitIpd, $methods->submitProjectchange, $methods->submitBaseline);
  917. }
  918. if($module == 'project' && !$hasDeliverable) unset($methods->deliverable);
  919. foreach($methods as $method => $label)
  920. {
  921. if($method == 'submitIpd' && $model != 'ipd') continue;
  922. if(isset($this->config->project->includedPriv[$module]) and !in_array($method, $this->config->project->includedPriv[$module])) continue;
  923. if(!isset($privs->$module)) $privs->$module = new stdclass();
  924. $privs->$module->$method = $label;
  925. }
  926. }
  927. return $privs;
  928. }
  929. /*
  930. * 构造项目搜索表单配置项。
  931. * Build search form.
  932. *
  933. * @param int $queryID
  934. * @param string $actionURL
  935. *
  936. * @return void
  937. * */
  938. /**
  939. * @param int $queryID
  940. * @param string $actionURL
  941. */
  942. public function buildSearchForm($queryID, $actionURL)
  943. {
  944. $this->config->project->search['queryID'] = $queryID;
  945. $this->config->project->search['actionURL'] = $actionURL;
  946. $statusList = $this->lang->project->statusList;
  947. unset($statusList['delay']);
  948. $this->config->project->search['params']['status']['values'] = $statusList;
  949. $programPairs = array(0 => '');
  950. $programPairs += $this->loadModel('program')->getPairs();
  951. $this->config->project->search['params']['parent']['values'] = $programPairs;
  952. if($this->config->edition != 'open') $this->config->project->search['params']['workflowGroup']['values'] = $this->loadModel('workflowGroup')->getPairs('project', 'all');
  953. if(!isset($this->config->setCode) or $this->config->setCode == 0) unset($this->config->project->search['fields']['code'], $this->config->project->search['params']['code']);
  954. if($this->config->systemMode == 'light') unset($this->config->project->search['fields']['parent'], $this->config->project->search['params']['parent']);
  955. $this->loadModel('search')->setSearchParams($this->config->project->search);
  956. }
  957. /**
  958. * 构造项目版本的搜索表单配置。
  959. * Build project build search form.
  960. *
  961. * @param array $products
  962. * @param int $queryID
  963. * @param int $projectID
  964. * @param int $productID
  965. * @param string $type project|execution
  966. * @access public
  967. * @return bool
  968. */
  969. public function buildProjectBuildSearchForm($products, $queryID, $projectID, $productID, $type = 'project')
  970. {
  971. /* Set search param. */
  972. $project = $this->projectTao->fetchProjectInfo($projectID);
  973. if(!$project) return false;
  974. $this->loadModel('build');
  975. if(!$project->hasProduct) unset($this->config->build->search['fields']['product']);
  976. $product = $productID ? $this->loadModel('product')->getByID($productID) : '';
  977. if($product && $product->type != 'normal')
  978. {
  979. $this->loadModel('branch');
  980. $branches = array(BRANCH_MAIN => $this->lang->branch->main) + $this->branch->getPairs($product->id, '', $projectID);
  981. $this->config->build->search['fields']['branch'] = sprintf($this->lang->build->branchName, $this->lang->product->branchName[$product->type]);
  982. $this->config->build->search['params']['branch'] = array('operator' => '=', 'control' => 'select', 'values' => $branches);
  983. }
  984. /* If there is an execution, set the execution filter item. */
  985. if($type == 'project' && $project->multiple)
  986. {
  987. $executionPairs = $this->loadModel('execution')->getByProject($project->id, 'all', 0, true, $project->model == 'waterfall');
  988. $this->config->build->search['fields']['execution'] = zget($this->lang->project->executionList, $project->model);
  989. $this->config->build->search['params']['execution'] = array('operator' => '=', 'control' => 'select', 'values' => $executionPairs);
  990. }
  991. $this->config->build->search['module'] = $type == 'project' ? 'projectBuild' : 'executionBuild';
  992. $this->config->build->search['actionURL'] = helper::createLink($this->app->rawModule, $this->app->rawMethod, "projectID=$projectID&type=bysearch&queryID=myQueryID");
  993. $this->config->build->search['queryID'] = (int)$queryID;
  994. $this->config->build->search['params']['product']['values'] = $products;
  995. $this->config->build->search['params']['system']['values'] = $this->loadModel('system')->getPairs($queryID ? 0 : (int)$productID, '0');
  996. $this->loadModel('search')->setSearchParams($this->config->build->search);
  997. return true;
  998. }
  999. /**
  1000. * 根据项目集和模型获取项目列表(列表索引为项目编号)。
  1001. * Get project pairs by model and project.
  1002. *
  1003. * @param string|array $model all|scrum|waterfall|kanban
  1004. * @param string $param noclosed
  1005. * @param int $projectID
  1006. * @param bool $pairs
  1007. * @access public
  1008. * @return array array(projectID => projectName, ...)
  1009. */
  1010. public function getPairsByModel($model = 'all', $param = '', $projectID = 0, $pairs = true)
  1011. {
  1012. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProjectPairs();
  1013. /* Get project list. */
  1014. $projects = $this->projectTao->fetchProjectListByQuery(strpos($param, 'noclosed') !== false ? 'unclosed' : 'all', $projectID);
  1015. if(is_string($model) && $model == 'agileplus') $model = array('scrum', 'agileplus');
  1016. if(is_string($model) && $model == 'waterfallplus') $model = array('waterfall', 'waterfallplus');
  1017. /* Set first program to the project attribute. */
  1018. $model = is_string($model) && $model == 'all' ? array() : (array)$model;
  1019. $multiple = strpos($param, 'multiple') !== false;
  1020. foreach($projects as $projectID => $project)
  1021. {
  1022. if(($model && !in_array($project->model, $model)) || ($multiple && !$project->multiple))
  1023. {
  1024. unset($projects[$projectID]);
  1025. continue;
  1026. }
  1027. list($programID) = explode(',', trim($project->path, ','));
  1028. $projects[$projectID]->program = $programID;
  1029. }
  1030. $programs = $this->loadModel('program')->getPairsByList(toIntArray(array_column($projects, 'program')));
  1031. /* Sort by project order in the program list. */
  1032. $allProjects = array();
  1033. foreach($programs as $programID => $program) $allProjects[$programID] = array();
  1034. $hasProgram = helper::hasFeature('program');
  1035. foreach($projects as $project)
  1036. {
  1037. $programID = zget($project, 'program', '');
  1038. $projectName = $project->name;
  1039. if($this->config->systemMode == 'ALM' && $hasProgram && $programID != $project->id) $projectName = zget($programs, $programID, '') . ' / ' . $projectName;
  1040. $project->name = $projectName;
  1041. $allProjects[$programID][] = $project;
  1042. }
  1043. $projectPairs = array();
  1044. foreach($allProjects as $programID => $projects)
  1045. {
  1046. foreach($projects as $project) $projectPairs[$project->id] = $pairs ? $project->name : $project;
  1047. }
  1048. return $projectPairs;
  1049. }
  1050. /**
  1051. * 根据项目ID获取需求信息。
  1052. * Get stories by project id.
  1053. *
  1054. * @param int $projectID
  1055. * @access public
  1056. * @return array
  1057. */
  1058. public function getStoriesByProject($projectID = 0)
  1059. {
  1060. return $this->dao->select("t2.product, t2.branch, GROUP_CONCAT(t2.story) as storyIDList")->from(TABLE_STORY)->alias('t1')
  1061. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story')
  1062. ->where('t1.deleted')->eq(0)
  1063. ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi()
  1064. ->groupBy('t2.product, t2.branch')
  1065. ->fetchGroup('product', 'branch');
  1066. }
  1067. /**
  1068. * 获取项目集列表。
  1069. * Get the program tree of project.
  1070. *
  1071. * @param string $browseType
  1072. * @access public
  1073. * @return array
  1074. */
  1075. public function getProgramTree($browseType)
  1076. {
  1077. /* Get program list. */
  1078. $programsList = $this->dao->select('id,name,parent')->from(TABLE_PROGRAM)
  1079. ->where('deleted')->eq('0')
  1080. ->andWhere('type')->eq('program')
  1081. ->andWhere('status')->ne('closed')
  1082. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->programs)->fi()
  1083. ->orderBy('grade desc, `order`')
  1084. ->fetchAll();
  1085. /* Init tree data. */
  1086. $programs = array();
  1087. foreach($programsList as $index => $program)
  1088. {
  1089. $programs[$index] = new stdClass();
  1090. $programs[$index]->id = $program->id;
  1091. $programs[$index]->name = $program->name;
  1092. $programs[$index]->parent = $program->parent;
  1093. $programs[$index]->url = helper::createLink('project', 'browse', "programID={$program->id}&browseType={$browseType}");
  1094. }
  1095. return $programs;
  1096. }
  1097. /**
  1098. * 创建项目后,新增团队成员.
  1099. * Add team members after create a project.
  1100. *
  1101. * @param int $projectID
  1102. * @param object $project
  1103. * @param array $members
  1104. * @access protected
  1105. * @return bool
  1106. */
  1107. protected function addTeamMembers($projectID, $project, $members)
  1108. {
  1109. /* Manage Team of project. */
  1110. array_push($members, $project->PM, $project->openedBy);
  1111. $members = array_unique($members);
  1112. $roles = $this->loadModel('user')->getUserRoles(array_values($members));
  1113. $teamMembers = array();
  1114. $this->loadModel('execution');
  1115. foreach($members as $account)
  1116. {
  1117. if(empty($account)) continue;
  1118. $member = new stdClass();
  1119. $member->root = $projectID;
  1120. $member->type = 'project';
  1121. $member->join = helper::now();
  1122. $member->days = zget($project, 'days', 0);
  1123. $member->hours = $this->config->execution->defaultWorkhours;
  1124. $member->account = $account;
  1125. $member->role = zget($roles, $account, '');
  1126. $teamMembers[$account] = $member;
  1127. }
  1128. $this->execution->addProjectMembers($projectID, $teamMembers);
  1129. return !dao::isError();
  1130. }
  1131. /**
  1132. * 创建一个项目。
  1133. * Create a project.
  1134. *
  1135. * @param object $project
  1136. * @param object $postData
  1137. * @access public
  1138. * @return int|bool
  1139. */
  1140. public function create($project, $postData)
  1141. {
  1142. $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->create['id'], $this->post->uid);
  1143. $this->projectTao->doCreate($project);
  1144. if(dao::isError()) return false;
  1145. $projectID = $this->dao->lastInsertId();
  1146. /* Add project whitelist. */
  1147. $whitelist = explode(',', $project->whitelist);
  1148. $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID);
  1149. $program = $project->parent ? $this->getByID((int)$project->parent) : new stdclass();
  1150. $this->projectTao->createDocLib($projectID, $project, $program);
  1151. $this->addTeamMembers($projectID, $project, array($project->openedBy));
  1152. if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->projectTao->createMilestoneReport($projectID);
  1153. if($project->hasProduct && empty($postData->rawdata->newProduct))
  1154. {
  1155. $this->updateProducts($projectID);
  1156. /* If $_POST has product name, create it. */
  1157. $linkedProductsCount = $this->projectTao->getLinkedProductsCount($project, $postData->rawdata);
  1158. }
  1159. $needCreateProduct = (!$project->hasProduct || isset($postData->rawdata->newProduct) || empty($linkedProductsCount)) && empty($project->isTpl);
  1160. if($needCreateProduct && !$this->projectTao->createProduct($projectID, $project, $postData, $program)) return false;
  1161. /* Save order. */
  1162. $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($projectID * 5)->where('id')->eq($projectID)->exec();
  1163. $this->file->updateObjectID((string)$this->post->uid, $projectID, 'project');
  1164. $this->loadModel('program')->setTreePath($projectID);
  1165. /* Add project admin. */
  1166. if(empty($project->isTpl)) $this->projectTao->addProjectAdmin($projectID);
  1167. if($project->acl != 'open') $this->loadModel('user')->updateUserView(array($projectID), 'project');
  1168. if(empty($project->multiple) and $project->model != 'waterfall' and $project->model != 'waterfallplus') $this->loadModel('execution')->createDefaultSprint($projectID);
  1169. return $projectID;
  1170. }
  1171. /**
  1172. * 检查输入的$product和$branch变量是否合规。
  1173. * Check branch and product valid by project.
  1174. *
  1175. * @param int $parent
  1176. * @param array $products
  1177. * @param array $branch
  1178. * @access public
  1179. * @return bool
  1180. */
  1181. public function checkBranchAndProduct($parent, $products, $branch)
  1182. {
  1183. $topProgramID = $this->loadModel('program')->getTopByID($parent);
  1184. $multipleProducts = $this->loadModel('product')->getMultiBranchPairs((int)$topProgramID);
  1185. foreach($products as $index => $productID)
  1186. {
  1187. if(isset($multipleProducts[$productID]))
  1188. {
  1189. foreach($branch[$index] as $branchID)
  1190. {
  1191. if($branchID == '')
  1192. {
  1193. dao::$errors["branch[{$index}][]"] = $this->lang->project->error->emptyBranch;
  1194. }
  1195. }
  1196. }
  1197. }
  1198. return !dao::isError();
  1199. }
  1200. /**
  1201. * 检查执行的起止日期是否小于项目的起止日期。
  1202. * Check if execution's start and end dates in project's start and end dates.
  1203. *
  1204. * @param int $projectID
  1205. * @param object $project
  1206. * @access public
  1207. * @return bool
  1208. */
  1209. public function checkDates($projectID, $project)
  1210. {
  1211. $executionsCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)
  1212. ->where('project') ->eq($projectID)
  1213. ->andWhere('deleted') ->eq('0')
  1214. ->fetch('count');
  1215. if(empty($executionsCount))return true;
  1216. $maxExecutionEnd = $this->dao->select('`end` as maxEnd')->from(TABLE_PROJECT)
  1217. ->where('project')->eq($projectID)
  1218. ->andWhere('deleted')->eq('0')
  1219. ->orderBy('end_desc')
  1220. ->fetch();
  1221. $minExecutionBegin = $this->dao->select('`begin` as minBegin')->from(TABLE_PROJECT)
  1222. ->where('project')->eq($projectID)
  1223. ->andWhere('deleted')->eq('0')
  1224. ->orderBy('begin_asc')
  1225. ->fetch();
  1226. if($maxExecutionEnd && $project->end < $maxExecutionEnd->maxEnd) dao::$errors['end'] = sprintf($this->lang->project->endGreatEqualExecution, $maxExecutionEnd->maxEnd);
  1227. if($minExecutionBegin && $project->begin > $minExecutionBegin->minBegin) dao::$errors['begin'] = sprintf($this->lang->project->beginLessEqualExecution, $minExecutionBegin->minBegin);
  1228. return !dao::isError();
  1229. }
  1230. /**
  1231. * 更新项目关联的团队成员列表。
  1232. * update teammembers while update project.
  1233. *
  1234. * @param object $project
  1235. * @param object $oldProject
  1236. * @param array $newMembers
  1237. * @access public
  1238. * @return bool
  1239. */
  1240. public function updateTeamMembers($project, $oldProject, $newMembers = array())
  1241. {
  1242. /* Get old project's team and roles. */
  1243. $projectID = (int)$oldProject->id;
  1244. $oldMembers = $this->loadModel('user')->getTeamMemberPairs($projectID, 'project');
  1245. if(empty($newMembers)) $newMembers = array_keys($oldMembers);
  1246. array_push($newMembers, $project->PM);
  1247. $newMembers = array_unique($newMembers);
  1248. $roles = $this->loadModel('user')->getUserRoles(array_values($newMembers));
  1249. /* Delete members while old model is kanban. */
  1250. if($oldProject->model == 'kanban')
  1251. {
  1252. $deleteMembers = array_diff(array_keys($oldMembers), array_values($newMembers));
  1253. $this->projectTao->deleteMembers($projectID, $oldProject->openedBy, $deleteMembers);
  1254. }
  1255. /* Init member default for update members. */
  1256. $member = new stdclass();
  1257. $member->type = 'project';
  1258. $member->root = $projectID;
  1259. $member->join = helper::today();
  1260. $member->days = zget($project, 'days', 0);
  1261. $member->hours = $this->config->execution->defaultWorkhours;
  1262. /* Prepare $addMembers for addProjectMembers(). */
  1263. $addMembers = array();
  1264. foreach($newMembers as $account)
  1265. {
  1266. if(empty($account) or isset($oldMembers[$account])) continue;
  1267. $member->account = $account;
  1268. $member->role = zget($roles, $account, '');
  1269. $addMembers[$account] = $member;
  1270. }
  1271. /* Add members. */
  1272. if(!empty(count($addMembers))) $this->loadModel('execution')->addProjectMembers($projectID, $addMembers);
  1273. if(!isset($_POST['teamMembers'])) $_POST['teamMembers'] = $newMembers;
  1274. return !dao::isError();
  1275. }
  1276. /**
  1277. * 更新此项目下或影子产品下的白名单列表。
  1278. * Update whitelist by project.
  1279. *
  1280. * @param object $project
  1281. * @param object $oldProject
  1282. * @access public
  1283. * @return bool
  1284. */
  1285. public function updateWhitelist($project, $oldProject)
  1286. {
  1287. /* 对比新旧白名单检查是否需要更新白名单。*/
  1288. /* Check if whitelist shoud update .*/
  1289. $projectID = $oldProject->id;
  1290. $whitelist = array_filter(explode(',', (string)$project->whitelist));
  1291. $oldWhitelist = array_filter(explode(',', (string)$oldProject->whitelist));
  1292. if(count($oldWhitelist) != count($whitelist) || !empty(array_diff($oldWhitelist, $whitelist)))
  1293. {
  1294. if(!$oldProject->hasProduct)
  1295. {
  1296. $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)
  1297. ->where('project')->eq($projectID)
  1298. ->fetchPairs();
  1299. $this->loadModel('personnel')->updateWhitelist($whitelist, 'product', current($linkedProducts));
  1300. }
  1301. $this->loadModel('personnel')->updateWhitelist($whitelist, 'project', $projectID);
  1302. }
  1303. return !dao::isError();
  1304. }
  1305. /**
  1306. * 更新用户视图。
  1307. * Update user view.
  1308. *
  1309. * @param int $projectID
  1310. * @param string $acl
  1311. * @access public
  1312. * @return bool
  1313. */
  1314. public function updateUserView($projectID, $acl)
  1315. {
  1316. if($acl == 'open') return true;
  1317. $this->loadModel('user')->updateUserView(array($projectID), 'project');
  1318. $executions = $this->dao->select('id')->from(TABLE_EXECUTION)
  1319. ->where('project')->eq($projectID)
  1320. ->fetchPairs();
  1321. if($executions) $this->user->updateUserView($executions, 'sprint');
  1322. return !dao::isError();
  1323. }
  1324. /**
  1325. * 更新项目下的所有产品的阶段。
  1326. * Update product stage by project.
  1327. *
  1328. * @param int $projectID
  1329. * @param object $postProductData
  1330. * @access public
  1331. * @return bool
  1332. */
  1333. public function updateProductStage($projectID, $postProductData = null)
  1334. {
  1335. if(empty($postProductData)) return false;
  1336. $stageParis = $this->loadModel('execution')->getPairs($projectID);
  1337. $stageProducts = $this->dao->select('project, product')->from(TABLE_PROJECTPRODUCT)
  1338. ->where('project')->in(array_keys($stageParis))
  1339. ->fetchPairs('project', 'product');
  1340. $products = zget($postProductData, 'products', array());
  1341. $branches = zget($postProductData, 'branch', array(0));
  1342. $plans = zget($postProductData, 'plans', array());
  1343. $productBranches = array();
  1344. foreach($products as $i => $productID) $productBranches[$productID] = zget($branches, $i, array(0));
  1345. foreach($stageProducts as $stageID => $productID)
  1346. {
  1347. foreach($productBranches[$productID] as $branchID)
  1348. {
  1349. $data = new stdclass();
  1350. $data->project = (int)$stageID;
  1351. $data->product = (int)$productID;
  1352. $data->branch = (int)$branchID;
  1353. $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : '';
  1354. $data->plan = trim($data->plan, ',');
  1355. $data->plan = empty($data->plan) ? 0 : ",$data->plan,";
  1356. $this->dao->replace(TABLE_PROJECTPRODUCT)->data($data)->exec();
  1357. }
  1358. }
  1359. return !dao::isError();
  1360. }
  1361. /**
  1362. * 更新项目。
  1363. * Update project.
  1364. *
  1365. * @param object $project
  1366. * @param object $oldProject
  1367. * @param object $postProductData
  1368. * @access public
  1369. * @return array|false
  1370. */
  1371. public function update($project, $oldProject, $postProductData = null)
  1372. {
  1373. /* 通过主键查老项目信息, 处理父节点和图片字段。*/
  1374. /* Fetch old project's info and dispose parent and file info. */
  1375. $projectID = $oldProject->id;
  1376. if(!isset($project->parent)) $project->parent = $oldProject->parent;
  1377. $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->edit['id'], $this->post->uid);
  1378. /* 若此项目为多迭代项目, 检查起止日期不得小于迭代的起止日期。*/
  1379. /* If this project has multiple stage, check if execution's start and end dates in project's start and end dates. */
  1380. if($oldProject->multiple && !$this->checkDates($projectID, $project)) return false;
  1381. /* 如果没有传入项目管理方式,则用之前的管理方式。*/
  1382. /* If no project management method is passed, the project management method is used. */
  1383. if(empty($project->model)) $project->model = $oldProject->model;
  1384. if($project->model != $oldProject->model && !$this->checkCanChangeModel($projectID, $oldProject->model)) $project->model = $oldProject->model;
  1385. /* 更新项目表。*/
  1386. /* Update project table. */
  1387. if(!$this->projectTao->doUpdate($projectID, $project)) return false;
  1388. /* 更新项目的关联信息。*/
  1389. /* Update relation info of this project. */
  1390. $this->updateUserView($projectID, $project->acl); // 更新用户视图。
  1391. $this->updateShadowProduct($project, $oldProject); // 更新影子产品关联信息。
  1392. $this->updateWhitelist($project, $oldProject); // 更新关联的白名单列表。
  1393. $this->updatePlans($projectID, (array)$this->post->plans); // 更新关联的计划列表。
  1394. if($oldProject->hasProduct > 0) $this->updateProducts($projectID, (array)$this->post->products, $postProductData); // 更新关联的产品列表。
  1395. $this->updateTeamMembers($project, $oldProject, zget($_POST, 'teamMembers', array())); // 更新关联的用户信息。
  1396. if($oldProject->stageBy == 'product' && in_array($oldProject->model, array('waterfall', 'waterfallplus'))) $this->updateProductStage($projectID, $postProductData); // 更新多套阶段
  1397. $this->file->updateObjectID((string)$this->post->uid, $projectID, 'project'); // 通过uid更新文件id。
  1398. if($oldProject->parent != $project->parent) $this->loadModel('program')->processNode($projectID, (int)$project->parent, $oldProject->path, $oldProject->grade); // 更新项目从属路径。
  1399. if($oldProject->storyType != $project->storyType)
  1400. {
  1401. /* 编辑项目时如果取消关联需求类型,则把对应类型的需求移除。 */
  1402. $unlinkType = array_diff(explode(',', $oldProject->storyType), explode(',', $project->storyType));
  1403. if($unlinkType) $this->unlinkStoryByType($projectID, $unlinkType);
  1404. }
  1405. if(empty($oldProject->multiple) and !in_array($oldProject->model, array('waterfall', 'waterfallplus'))) $this->loadModel('execution')->syncNoMultipleSprint($projectID); // 无迭代的非瀑布项目需要更新。
  1406. if(in_array($this->config->edition, array('max', 'ipd')) && $oldProject->workflowGroup != $project->workflowGroup) $this->modifyWorkflowGroup($projectID, $oldProject->workflowGroup, $project->workflowGroup); // 更新项目流程。
  1407. if($oldProject->model != $project->model && !in_array($oldProject->model, array('waterfall', 'waterfallplus', 'ipd')) && in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')))
  1408. {
  1409. $this->projectTao->createMilestoneReport($projectID);
  1410. }
  1411. if(dao::isError()) return false;
  1412. return common::createChanges($oldProject, $project);
  1413. }
  1414. /**
  1415. * 批量更新项目。
  1416. * Batch update projects.
  1417. *
  1418. * @param array $data
  1419. * @access public
  1420. * @return array|false
  1421. */
  1422. public function batchUpdate($data)
  1423. {
  1424. $projects = array();
  1425. $allChanges = array();
  1426. $oldProjects = $this->getByIdList(array_keys($data));
  1427. $projects = $this->projectTao->buildBatchUpdateProjects($data, $oldProjects);
  1428. $this->loadModel('execution');
  1429. $this->lang->error->unique = $this->lang->error->repeat;
  1430. foreach($projects as $projectID => $project)
  1431. {
  1432. $oldProject = $oldProjects[$projectID];
  1433. $this->projectTao->doUpdate($projectID, $project);
  1434. if(dao::isError())
  1435. {
  1436. $errors = dao::getError();
  1437. foreach($errors as $key => $error) dao::$errors[$key][0] = 'ID' . $projectID . $error[0];
  1438. return false;
  1439. }
  1440. if(!dao::isError())
  1441. {
  1442. /* 无产品项目信息变更后更新影子产品的相关字段. */
  1443. if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent or $oldProject->acl != $project->acl)) $this->updateShadowProduct($project, $oldProject);
  1444. if(isset($project->parent))
  1445. {
  1446. $linkedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs();
  1447. $this->updateProductProgram($oldProject->parent, $project->parent, $linkedProducts);
  1448. if($oldProject->parent != $project->parent) $this->loadModel('program')->processNode($projectID, $project->parent, $oldProject->path, $oldProject->grade);
  1449. }
  1450. /* When acl is open, white list set empty. When acl is private,update user view. */
  1451. if($project->acl == 'open') $this->loadModel('personnel')->updateWhitelist(array(), 'project', $projectID);
  1452. if($project->acl != 'open') $this->loadModel('user')->updateUserView(array($projectID), 'project');
  1453. $this->executeHooks($projectID);
  1454. if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->execution->syncNoMultipleSprint($projectID);
  1455. }
  1456. $allChanges[$projectID] = common::createChanges($oldProject, $project);
  1457. }
  1458. return $allChanges;
  1459. }
  1460. /**
  1461. * 开始项目并更改其状态.
  1462. * start and update a project.
  1463. *
  1464. * @param int $projectID
  1465. * @param object $postData
  1466. * @access public
  1467. * @return array|false
  1468. */
  1469. public function start($projectID, $postData)
  1470. {
  1471. $oldProject = $this->getById($projectID);
  1472. $project = $this->loadModel('file')->processImgURL($postData, $this->config->project->editor->start['id'], $this->post->uid);
  1473. $this->projectTao->doStart($projectID, $project);
  1474. /* When it has multiple errors, only the first one is prompted */
  1475. if(dao::isError())
  1476. {
  1477. if(count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0];
  1478. return false;
  1479. }
  1480. $this->recordFirstEnd($projectID);
  1481. if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'start');
  1482. return common::createChanges($oldProject, $project);
  1483. }
  1484. /**
  1485. * Suspend project and update status.
  1486. * 暂停项目并更改其状态
  1487. *
  1488. * @param int $projectID
  1489. * @param object $project
  1490. * @param string $type
  1491. *
  1492. * @access public
  1493. * @return array|flase
  1494. */
  1495. public function suspend($projectID, $project, $type = 'project')
  1496. {
  1497. $editorIdList = $this->config->project->editor->suspend['id'];
  1498. $oldProject = $this->getById($projectID, $type);
  1499. $project = $this->loadModel('file')->processImgURL($project, $editorIdList, $this->post->uid);
  1500. $this->projectTao->doSuspend($projectID, $project);
  1501. if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'suspend');
  1502. return common::createChanges($oldProject, $project);
  1503. }
  1504. /**
  1505. * Activate project.
  1506. *
  1507. * @param int $projectID
  1508. * @param object $project
  1509. * @access public
  1510. * @return array $changes|false
  1511. */
  1512. public function activate($projectID, $project)
  1513. {
  1514. if($project->begin > $project->end)
  1515. {
  1516. dao::$errors['end'] = $this->lang->project->error->endLessBegin;
  1517. return false;
  1518. }
  1519. $oldProject = $this->projectTao->fetchProjectInfo($projectID);
  1520. $daoSuccess = $this->projectTao->doActivate($projectID, $project);
  1521. if(!$daoSuccess) return false;
  1522. if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID);
  1523. /* Update start and end date of tasks in this project. */
  1524. if($project->readjustTask)
  1525. {
  1526. $tasks = $this->projectTao->fetchUndoneTasks($projectID);
  1527. $this->projectTao->updateTasksStartAndEndDate($tasks, $oldProject, $project);
  1528. }
  1529. /* Activate the shadow product of the project. (only change product status) */
  1530. if(!$oldProject->hasProduct)
  1531. {
  1532. $productID = $this->loadModel('product')->getProductIDByProject($projectID);
  1533. $product = new stdclass();
  1534. $product->status = $this->config->vision == 'or' ? 'wait' : 'normal';
  1535. $this->product->activate($productID, $product);
  1536. }
  1537. return common::createChanges($oldProject, $project);
  1538. }
  1539. /**
  1540. * 关闭项目并更改其状态
  1541. * Close project and update status.
  1542. *
  1543. * @param int $projectID
  1544. * @param object $project
  1545. *
  1546. * @access public
  1547. * @return array|false
  1548. */
  1549. public function close($projectID, $project)
  1550. {
  1551. $oldProject = $this->getByID($projectID);
  1552. $editorIdList = $this->config->project->editor->close['id'];
  1553. $project = $this->loadModel('file')->processImgURL($project, $editorIdList, $this->post->uid);
  1554. $this->projectTao->doClosed($projectID, $project, $oldProject);
  1555. /* When it has multiple errors, only the first one is prompted */
  1556. if(dao::isError())
  1557. {
  1558. if(count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0];
  1559. return false;
  1560. }
  1561. if(!$oldProject->multiple) $this->projectTao->changeExecutionStatus($projectID, 'close');
  1562. /* Close the shadow product of the project. */
  1563. if(!$oldProject->hasProduct)
  1564. {
  1565. $productID = $this->loadModel('product')->getProductIDByProject($projectID);
  1566. $product = new stdclass();
  1567. $product->status = 'closed';
  1568. $this->product->close($productID, $product);
  1569. }
  1570. $this->loadModel('score')->create('project', 'close', $oldProject);
  1571. return common::createChanges($oldProject, $project);
  1572. }
  1573. /**
  1574. * 如果是无产品项目,更新影子产品信息。
  1575. * Update shadow product.
  1576. *
  1577. * @param object $project
  1578. * @param object $oldProject
  1579. * @access public
  1580. * @return bool
  1581. */
  1582. public function updateShadowProduct($project, $oldProject)
  1583. {
  1584. /* If this is a project without product, update shadow product's info. */
  1585. if($oldProject->hasProduct) return true;
  1586. /* If oldProject has no product and name or parent or acl has changed, update shadow product. */
  1587. if($oldProject->name != $project->name || $oldProject->parent != $project->parent || $oldProject->acl != $project->acl)
  1588. {
  1589. $product = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($oldProject->id)->fetch('product');
  1590. $topProgram = !empty($project->parent) ? $this->loadModel('program')->getTopByID((int)$project->parent) : 0;
  1591. /* Convert program acl to custom for product table */
  1592. $productAcl = $project->acl;
  1593. if($productAcl == 'program') $productAcl = 'private';
  1594. $this->dao->update(TABLE_PRODUCT)
  1595. ->set('name')->eq($project->name)
  1596. ->set('program')->eq($topProgram)
  1597. ->set('acl')->eq($productAcl)
  1598. ->where('id')->eq($product)
  1599. ->exec();
  1600. }
  1601. /* Update shadow product's status if need .*/
  1602. if(isset($project->status) && $oldProject->status != $project->status && strpos('doing,closed', $project->status) !== false)
  1603. {
  1604. $productID = $this->loadModel('product')->getProductIDByProject($oldProject->id);
  1605. if($project->status == 'doing') $this->product->activate($productID);
  1606. if($project->status == 'closed') $this->product->close($productID);
  1607. }
  1608. return !dao::isError();
  1609. }
  1610. /**
  1611. * 删除项目下关联执行与产品与文档库
  1612. * Deletes related items under project.
  1613. *
  1614. * @param string $table product|execution|doclib
  1615. * @param int|array $idList
  1616. *
  1617. * @access public
  1618. * @return bool
  1619. */
  1620. public function deleteByTableName($table, $idList)
  1621. {
  1622. if(strpos($table, 'doclib') !== false)
  1623. {
  1624. $this->dao->update($table)->set('deleted')->eq(1)->where('execution')->eq($idList)->exec();
  1625. }
  1626. else
  1627. {
  1628. $this->dao->update($table)->set('deleted')->eq(1)->where('id')->in($idList)->exec();
  1629. }
  1630. return !dao::isError();
  1631. }
  1632. /**
  1633. * 更新产品的项目集。
  1634. * Update the program of the product.
  1635. *
  1636. * @param int $oldProgram
  1637. * @param int $newProgram
  1638. * @param array $products
  1639. * @access public
  1640. * @return bool
  1641. */
  1642. public function updateProductProgram($oldProgram, $newProgram, $products)
  1643. {
  1644. /* Product belonging project set processing. */
  1645. $oldTopProgram = $this->loadModel('program')->getTopByID($oldProgram);
  1646. $newTopProgram = $this->program->getTopByID($newProgram);
  1647. if($oldTopProgram != $newTopProgram)
  1648. {
  1649. $productList = $this->loadModel('product')->getByIdList($products);
  1650. foreach($products as $productID)
  1651. {
  1652. $product = zget($productList, $productID, array());
  1653. if(!$product) continue;
  1654. unset($product->id);
  1655. unset($product->code);
  1656. if(empty($product->closedDate)) unset($product->closedDate);
  1657. $product->program = $newTopProgram;
  1658. $this->product->update($productID, $product);
  1659. }
  1660. }
  1661. return !dao::isError();
  1662. }
  1663. /**
  1664. * 删除项目和迭代下对应需求概念的关联关系。
  1665. * Unlink the relationship between the project and the story concept under the iteration and the project.
  1666. *
  1667. * @param int $projectID
  1668. * @param string $storyType
  1669. * @access public
  1670. * @return void
  1671. */
  1672. public function unlinkStoryByType($projectID = 0, $storyType = '')
  1673. {
  1674. $idList = $this->dao->select('id')->from(TABLE_PROJECT)
  1675. ->where('deleted')->eq(0)
  1676. ->andWhere('project')->eq($projectID)
  1677. ->fetchPairs();
  1678. $idList[$projectID] = $projectID;
  1679. $storyIdList = $this->dao->select('t1.story')->from(TABLE_PROJECTSTORY)->alias('t1')
  1680. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  1681. ->where('t2.type')->in($storyType)
  1682. ->beginIF($projectID > 0)->andWhere('t1.project')->in($idList)->fi()
  1683. ->fetchPairs();
  1684. if($storyIdList) $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('story')->in($storyIdList)->beginIF($projectID > 0)->andWhere('project')->in($idList)->fi()->exec();
  1685. }
  1686. /**
  1687. * 移除项目成员。
  1688. * Unlink a member.
  1689. *
  1690. * @param int $projectID
  1691. * @param string $account
  1692. * @param bool $removeExecution
  1693. * @access public
  1694. * @return bool
  1695. */
  1696. public function unlinkMember($projectID, $account, $removeExecution = false)
  1697. {
  1698. $user = $this->loadModel('user')->getById($account);
  1699. $changes = array(array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => $user->realname));
  1700. $this->projectTao->unlinkTeamMember($projectID, 'project', $account, $user->realname, $changes);
  1701. $this->user->updateUserView(array($projectID), 'project', array($account));
  1702. if($removeExecution)
  1703. {
  1704. $executions = $this->loadModel('execution')->getByProject($projectID, 'undone', 0, true);
  1705. $this->projectTao->unlinkTeamMember(array_keys($executions), 'execution', $account, $user->realname, $changes);
  1706. $this->user->updateUserView(array_keys($executions), 'sprint', array($account));
  1707. }
  1708. $linkedProducts = $this->loadModel('product')->getProductPairsByProject($projectID);
  1709. if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', array($account));
  1710. return !dao::isError();
  1711. }
  1712. /**
  1713. * 维护项目团队成员。
  1714. * Manage team members.
  1715. *
  1716. * @param int $projectID
  1717. * @param array $members
  1718. * @access public
  1719. * @return bool
  1720. */
  1721. public function manageMembers($projectID, $members)
  1722. {
  1723. $project = $this->projectTao->fetchProjectInfo($projectID);
  1724. $oldMembers = $this->dao->select('`account`, `join`, `days`')->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq('project')->fetchAll();
  1725. $oldJoin = array_column($oldMembers, 'join', 'account');
  1726. $oldDays = array_column($oldMembers, 'days', 'account');
  1727. /* Check fields. */
  1728. foreach($members as $key => $member)
  1729. {
  1730. if(empty($member->account)) continue;
  1731. if((float)$member->hours > 24) dao::$errors = $this->lang->project->errorHours;
  1732. if(!empty($project->days))
  1733. {
  1734. if(isset($oldDays[$member->account]) && $oldDays[$member->account] == $member->days) continue;
  1735. if((int)$member->days > $project->days) dao::$errors = sprintf($this->lang->project->daysGreaterProject, $project->days);
  1736. }
  1737. }
  1738. if(dao::isError()) return false;
  1739. $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq('project')->exec();
  1740. $accounts = $this->projectTao->insertMember($members, $projectID, $oldJoin);
  1741. $this->projectTao->updateMemberView($projectID, $accounts, $oldJoin);
  1742. if(empty($project->multiple) and $project->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID);
  1743. return !dao::isError();
  1744. }
  1745. /**
  1746. * 将数字转换成带单位的数字。
  1747. * Convert budget unit.
  1748. *
  1749. * @param float|string $budget
  1750. * @access public
  1751. * @return float|string $projectBudget
  1752. */
  1753. public function getBudgetWithUnit($budget)
  1754. {
  1755. $budget = (float)$budget;
  1756. $rawBudget = $budget;
  1757. if($budget < $this->config->project->budget->tenThousand)
  1758. {
  1759. $budget = round($budget, $this->config->project->budget->precision);
  1760. $unit = '';
  1761. }
  1762. elseif($budget < $this->config->project->budget->oneHundredMillion && $budget >= $this->config->project->budget->tenThousand)
  1763. {
  1764. $budget = round($budget/$this->config->project->budget->tenThousand, $this->config->project->budget->precision);
  1765. $unit = $this->lang->project->tenThousand;
  1766. }
  1767. else
  1768. {
  1769. $budget = round($budget/$this->config->project->budget->oneHundredMillion, $this->config->project->budget->precision);
  1770. $unit = $this->lang->project->hundredMillion;
  1771. }
  1772. return !commonModel::checkNotCN() ? $budget . $unit : round($rawBudget, $this->config->project->budget->precision);
  1773. }
  1774. /**
  1775. * 更新项目关联的产品信息。
  1776. * Update products of a project.
  1777. *
  1778. * @param int $projectID
  1779. * @param array $products
  1780. * @param object|array|null $postProductData
  1781. * @access public
  1782. * @return bool
  1783. */
  1784. public function updateProducts($projectID, $products = array(), $postProductData = array())
  1785. {
  1786. if(!empty($_POST['newProduct'])) return array();
  1787. $this->loadModel('user');
  1788. $teams = array_keys($this->getTeamMembers($projectID));
  1789. $stakeholders = array_keys($this->loadModel('stakeholder')->getStakeHolderPairs($projectID));
  1790. $members = array_merge($teams, $stakeholders);
  1791. /* Link products of other programs. */
  1792. if(!empty($_POST['otherProducts'])) return $this->linkOtherProducts($projectID, $members);
  1793. /* Link products of current program of the project. */
  1794. $products = isset($_POST['products']) ? (array)$_POST['products'] : $products;
  1795. $oldProjectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchGroup('product', 'branch');
  1796. $this->linkProducts($projectID, $products, $oldProjectProducts, $members);
  1797. /* Delete the execution linked products that is not linked with the execution. */
  1798. if($projectID > 0)
  1799. {
  1800. $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->fetchPairs('id');
  1801. $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->in($executions)->andWhere('product')->notin(toIntArray($products))->exec();
  1802. if(isset($_POST['stageBy']) and $_POST['stageBy'] == 'product')
  1803. {
  1804. $this->dao->update(TABLE_PROJECT)->set('stageBy')->eq('product')->where('id')->eq($projectID)->orWhere('project')->eq($projectID)->exec();
  1805. }
  1806. $project = $this->projectTao->fetchProjectInfo($projectID);
  1807. if(!empty($project) && !empty($executions) && $project->stageBy == 'project' && in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')))
  1808. {
  1809. $this->loadModel('execution');
  1810. unset($postProductData->plans);
  1811. foreach($executions as $executionID) $this->execution->updateProducts($executionID, $postProductData);
  1812. }
  1813. }
  1814. /* Update the user product view. */
  1815. $oldProductIdList = array_keys($oldProjectProducts);
  1816. $needUpdate = array_merge(array_diff($oldProductIdList, $products), array_diff($products, $oldProductIdList));
  1817. if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members);
  1818. /* Create actions. */
  1819. $this->loadModel('action');
  1820. if(!empty($needUpdate)) $this->action->create('project', $projectID, 'managed', '', implode(',', $products));
  1821. /* 如果有取消关联的产品,且项目有迭代且是非瀑布项目,记录关联产品执行到action表。*/
  1822. /* If there are unlinkedProducts and it is multiple project and it isn't waterfall project, record to table action. */
  1823. $unlinkedProducts = array_diff($oldProductIdList, $products);
  1824. if(!empty($unlinkedProducts) && !empty($project) && $project->multiple && $project->model != 'waterfall' && $project->model != 'waterfallplus')
  1825. {
  1826. $products = $this->dao->select('name')->from(TABLE_PRODUCT)
  1827. ->where('id')->in($unlinkedProducts)
  1828. ->fetchPairs();
  1829. $this->action->create('project', $projectID, 'unlinkproduct', '', implode(',', $products));
  1830. }
  1831. return !dao::isError();
  1832. }
  1833. /**
  1834. * 关联项目所属项目集下的产品。
  1835. * Link products of current program of the project.
  1836. *
  1837. * @param int $projectID
  1838. * @param array $products
  1839. * @param array $oldProjectProducts
  1840. * @param array $members
  1841. * @access public
  1842. * @return bool
  1843. */
  1844. public function linkProducts($projectID, $products, $oldProjectProducts, $members)
  1845. {
  1846. $this->loadModel('user');
  1847. /* Delete the linked data. */
  1848. $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->exec();
  1849. /* Update the user product view. */
  1850. if(empty($products))
  1851. {
  1852. $this->user->updateUserView(array_keys($oldProjectProducts), 'product', $members);
  1853. return true;
  1854. }
  1855. /* Set the product information linked with the project. */
  1856. $branches = isset($_POST['branch']) ? $_POST['branch'] : array();
  1857. $plans = isset($_POST['plans']) ? $_POST['plans'] : array();
  1858. $charter = isset($_POST['charter']) ? $_POST['charter'] : 0;
  1859. $roadmaps = isset($_POST['roadmaps']) ? $_POST['roadmaps'] : array();
  1860. $existedProducts = array();
  1861. foreach($products as $index => $productID)
  1862. {
  1863. if(empty($productID)) continue;
  1864. if(!isset($existedProducts[$productID])) $existedProducts[$productID] = array();
  1865. $oldPlan = 0;
  1866. $oldRoadmap = 0;
  1867. $branch = isset($branches[$index]) ? $branches[$index] : 0;
  1868. $branch = !is_array($branch) ? array($branch => $branch) : $branch;
  1869. foreach($branch as $branchID)
  1870. {
  1871. if(isset($existedProducts[$productID][$branchID])) continue;
  1872. if(isset($oldProjectProducts[$productID][$branchID]))
  1873. {
  1874. $oldProjectProduct = $oldProjectProducts[$productID][$branchID];
  1875. if($this->app->rawMethod != 'edit') $oldPlan = $oldProjectProduct->plan;
  1876. }
  1877. $data = new stdclass();
  1878. $data->project = $projectID;
  1879. $data->product = $productID;
  1880. $data->branch = (int)$branchID;
  1881. $data->plan = (isset($plans[$productID]) && !empty($plans[$productID])) ? implode(',', $plans[$productID]) : $oldPlan;
  1882. $data->plan = trim((string)$data->plan, ',');
  1883. $data->plan = empty($data->plan) ? 0 : ",$data->plan,";
  1884. if($charter)
  1885. {
  1886. $data->roadmap = (isset($roadmaps[$productID]) && !empty($roadmaps[$productID])) ? implode(',', $roadmaps[$productID]) : $oldRoadmap;
  1887. $data->roadmap = trim((string)$data->roadmap, ',');
  1888. $data->roadmap = empty($data->roadmap) ? 0 : ",$data->roadmap,";
  1889. }
  1890. $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec();
  1891. $existedProducts[$productID][$branchID] = true;
  1892. }
  1893. }
  1894. return true;
  1895. }
  1896. /**
  1897. * 关联其他项目集下的产品。
  1898. * Link products of other programs.
  1899. *
  1900. * @access public
  1901. * @return bool
  1902. * @param int $projectID
  1903. * @param mixed[] $members
  1904. */
  1905. public function linkOtherProducts($projectID, $members)
  1906. {
  1907. $this->loadModel('user');
  1908. $productIdList = array();
  1909. $otherProducts = $_POST['otherProducts'];
  1910. foreach($otherProducts as $otherProduct)
  1911. {
  1912. if(!$otherProduct) continue;
  1913. $data = new stdclass();
  1914. $data->project = $projectID;
  1915. $data->plan = 0;
  1916. if(strpos($otherProduct, '_') !== false)
  1917. {
  1918. $params = explode('_', $otherProduct);
  1919. $data->product = $params[0];
  1920. $data->branch = $params[1];
  1921. }
  1922. else
  1923. {
  1924. $data->product = $otherProduct;
  1925. $data->branch = 0;
  1926. }
  1927. $this->dao->replace(TABLE_PROJECTPRODUCT)->data($data)->exec();
  1928. $productIdList[] = $data->product;
  1929. }
  1930. $this->user->updateUserView($productIdList, 'product', $members);
  1931. if($projectID > 0 and isset($_POST['stageBy']) and $_POST['stageBy'] == 'product')
  1932. {
  1933. $this->dao->update(TABLE_PROJECT)->set('stageBy')->eq('product')->where('id')->eq($projectID)->orWhere('project')->eq($projectID)->exec();
  1934. }
  1935. return !dao::isError();
  1936. }
  1937. /**
  1938. * 更新关联的产品和执行的用户视图。
  1939. * Update userview for involved product and execution.
  1940. *
  1941. * @param int $projectID
  1942. * @param array $users
  1943. * @access public
  1944. * @return bool
  1945. */
  1946. public function updateInvolvedUserView($projectID, $users = array())
  1947. {
  1948. $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchPairs('product', 'product');
  1949. $this->loadModel('user')->updateUserView($products, 'product', $users);
  1950. $executions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->fetchPairs('id', 'id');
  1951. if($executions) $this->user->updateUserView($executions, 'sprint', $users);
  1952. return true;
  1953. }
  1954. /**
  1955. * 获取项目团队成员。
  1956. * Get team members.
  1957. *
  1958. * @param int $projectID
  1959. * @access public
  1960. * @return array
  1961. */
  1962. public function getTeamMembers($projectID)
  1963. {
  1964. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getTeamMembers();
  1965. $project = $this->projectTao->fetchProjectInfo($projectID);
  1966. if(empty($project)) return array();
  1967. return $this->dao->select("t1.*, t1.hours * t1.days AS totalHours, t2.id as userID, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1')
  1968. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  1969. ->where('t1.root')->eq((int)$projectID)
  1970. ->andWhere('t1.type')->eq($project->type)
  1971. ->andWhere('t2.deleted')->eq('0')
  1972. ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi()
  1973. ->fetchAll('account');
  1974. }
  1975. /**
  1976. * Get team member pairs by projectID.
  1977. *
  1978. * @param int $projectID
  1979. * @access public
  1980. * @return array
  1981. */
  1982. public function getTeamMemberPairs($projectID)
  1983. {
  1984. $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
  1985. if(empty($project)) return array();
  1986. $type = $project->type == 'project' ? 'project' : 'execution';
  1987. $members = $this->dao->select("t1.account, t2.realname")->from(TABLE_TEAM)->alias('t1')
  1988. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  1989. ->where('t1.root')->eq((int)$projectID)
  1990. ->andWhere('t1.type')->eq($type)
  1991. ->andWhere('t2.deleted')->eq('0')
  1992. ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi()
  1993. ->fetchPairs('account', 'realname');
  1994. return $members;
  1995. }
  1996. /**
  1997. * Get team member group.
  1998. *
  1999. * @param mixed $projectIdList
  2000. * @access public
  2001. * @return array
  2002. */
  2003. public function getTeamMemberGroup($projectIdList)
  2004. {
  2005. if(empty($projectIdList)) return array();
  2006. return $this->dao->select("t1.account, t2.realname, t1.root as project")->from(TABLE_TEAM)->alias('t1')
  2007. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  2008. ->where('t1.root')->in($projectIdList)
  2009. ->andWhere('t1.type')->eq('project')
  2010. ->andWhere('t2.deleted')->eq('0')
  2011. ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi()
  2012. ->fetchGroup('project', 'account');
  2013. }
  2014. /**
  2015. * Get members of a project who can be imported.
  2016. *
  2017. * @param int $projectID
  2018. * @param array $currentMembers
  2019. * @access public
  2020. * @return array
  2021. */
  2022. public function getMembers2Import($projectID, $currentMembers)
  2023. {
  2024. if($projectID == 0) return array();
  2025. return $this->dao->select('account, role, hours')
  2026. ->from(TABLE_TEAM)
  2027. ->where('root')->eq($projectID)
  2028. ->andWhere('type')->eq('project')
  2029. ->andWhere('account')->notIN($currentMembers)
  2030. ->fetchAll('account');
  2031. }
  2032. /**
  2033. * Get stats for project kanban.
  2034. *
  2035. * @access public
  2036. * @return array
  2037. */
  2038. public function getStats4Kanban()
  2039. {
  2040. /* Get execution of the status is doing. */
  2041. $executions = $this->loadModel('execution')->getStatData(0, 'doing', 0, 0, false, 'hasParentName|skipParent');
  2042. $projectExecutions = array();
  2043. foreach($executions as $execution)
  2044. {
  2045. if(!empty($execution->projectName)) $execution->projectName = htmlspecialchars_decode($execution->projectName);
  2046. $projectExecutions[$execution->project][$execution->id] = $execution;
  2047. }
  2048. /* The execution is sorted in reverse order by execution ID. */
  2049. $ongoingExecutions = array();
  2050. foreach($projectExecutions as $projectID => $executions)
  2051. {
  2052. krsort($projectExecutions[$projectID]);
  2053. $ongoingExecutions[$projectID] = current($projectExecutions[$projectID]);
  2054. }
  2055. $projectsStats = $this->loadModel('program')->getProjectStats(0, 'all', 0, 'order_asc');
  2056. $projectsStats = $this->projectTao->classifyProjects($projectsStats);
  2057. /* Only display recent two closed projects. */
  2058. $projectsStats = $this->projectTao->sortAndReduceClosedProjects($projectsStats, 2);
  2059. return array($projectsStats, $ongoingExecutions);
  2060. }
  2061. /**
  2062. * 设置项目的导航菜单。
  2063. * Set menu of project module.
  2064. *
  2065. * @param int $projectID
  2066. * @access public
  2067. * @return int|false
  2068. */
  2069. public function setMenu($projectID)
  2070. {
  2071. $moduleName = $this->app->rawModule;
  2072. $methodName = $this->app->rawMethod;
  2073. if(!$this->loadModel('common')->isOpenMethod($moduleName, $methodName) and !commonModel::hasPriv($moduleName, $methodName)) $this->common->deny($moduleName, $methodName, false);
  2074. $projectID = (int)$this->checkAccess($projectID, $this->getPairsByProgram());
  2075. $project = commonModel::isTutorialMode() ? $this->loadModel('tutorial')->getProject() : $this->projectTao->fetchProjectInfo($projectID);
  2076. if(!$project) return false;
  2077. if(!commonModel::isTutorialMode() && $projectID != 0 && !$this->checkPriv($projectID))
  2078. {
  2079. $this->projectTao->accessDenied();
  2080. return true;
  2081. }
  2082. /* Reset project priv. */
  2083. $this->common->resetProjectPriv($projectID);
  2084. /* Set secondary menu. */
  2085. $this->projectTao->setMenuByModel($project->model);
  2086. $this->projectTao->setMenuByProduct($projectID, $project->hasProduct, $project->model);
  2087. $this->lang->switcherMenu = $this->getSwitcher($projectID, $moduleName, $methodName);
  2088. /* Replace url params. */
  2089. common::setMenuVars('project', $projectID);
  2090. $this->setNoMultipleMenu($projectID);
  2091. if($project->acl == 'open') unset($this->lang->project->menu->settings['subMenu']->whitelist);
  2092. if($this->app->getModuleName() == 'repo' || $this->app->getModuleName() == 'mr') $this->loadModel('repo')->setHideMenu($projectID);
  2093. if(!empty($project->isTpl)) dao::$filterTpl = 'never';
  2094. return $projectID;
  2095. }
  2096. /**
  2097. * 设置未启用迭代的菜单。
  2098. * Set multi-scrum menu.
  2099. *
  2100. * @param int $projectID
  2101. * @access public
  2102. * @return bool
  2103. */
  2104. public function setNoMultipleMenu($projectID)
  2105. {
  2106. $this->session->set('multiple', true);
  2107. $project = common::isTutorialMode() ? $this->loadModel('tutorial')->getProject() : $this->projectTao->fetchProjectInfo($projectID);
  2108. if(empty($project) || $project->multiple) return false;
  2109. if(!in_array($project->type, array('project', 'sprint', 'kanban'))) return false;
  2110. if($project->type == 'project')
  2111. {
  2112. $model = $project->model;
  2113. $executionID = $this->loadModel('execution')->getNoMultipleID($projectID);
  2114. }
  2115. else
  2116. {
  2117. $model = $project->type == 'kanban' ? 'kanban' : 'scrum';
  2118. $executionID = $project->id;
  2119. $projectID = $project->project;
  2120. $project = $this->projectTao->fetchProjectInfo($projectID);
  2121. }
  2122. if(empty($projectID) || empty($executionID)) return false;
  2123. $this->session->set('project', $projectID, 'project');
  2124. $this->session->set('multiple', false);
  2125. global $lang;
  2126. $navGroup = zget($lang->navGroup, $this->app->rawModule);
  2127. $lang->$navGroup->menu = $lang->project->noMultiple->{$model}->menu;
  2128. $lang->$navGroup->menuOrder = $lang->project->noMultiple->{$model}->menuOrder;
  2129. $lang->$navGroup->dividerMenu = $lang->project->noMultiple->{$model}->dividerMenu;
  2130. $this->projectTao->setNavGroupMenu($navGroup, $executionID, $project);
  2131. $lang->project->menu = $lang->$navGroup->menu;
  2132. $lang->project->menuOrder = $lang->$navGroup->menuOrder;
  2133. $lang->project->dividerMenu = $lang->$navGroup->dividerMenu;
  2134. $this->lang->switcherMenu = $this->getSwitcher($projectID, $this->app->rawModule, $this->app->rawMethod);
  2135. /* 无迭代项目不开启过程删除导航。 */
  2136. if(in_array($this->config->edition, array('max', 'ipd')))
  2137. {
  2138. $this->loadModel('workflowgroup');
  2139. if(!$this->workflowgroup->hasFeature((int)$project->workflowGroup, 'process')) unset($lang->project->menu->other['dropMenu']->pssp);
  2140. if(!$this->workflowgroup->hasFeature((int)$project->workflowGroup, 'deliverable'))
  2141. {
  2142. unset($lang->project->menu->deliverable);
  2143. unset($lang->project->menu->auditplan['subMenu']->deliverable);
  2144. }
  2145. if(!$this->workflowgroup->hasFeature((int)$project->workflowGroup, 'auditplan')) unset($lang->project->menu->other['dropMenu']->auditplan);
  2146. }
  2147. /* If projectID is set, cannot use homeMenu. */
  2148. unset($lang->project->homeMenu);
  2149. if(empty($project->hasProduct))
  2150. {
  2151. unset($lang->project->menu->settings['subMenu']->products);
  2152. }
  2153. else
  2154. {
  2155. unset($lang->project->menu->settings['subMenu']->module);
  2156. unset($lang->project->menu->projectplan);
  2157. }
  2158. $this->loadModel('common')->resetProjectPriv($projectID);
  2159. return true;
  2160. }
  2161. /**
  2162. * 检查是否可以修改model字段
  2163. * Check if the project model can be changed.
  2164. *
  2165. * @param int $projectID
  2166. * @param string $model
  2167. * @access public
  2168. * @return bool
  2169. */
  2170. public function checkCanChangeModel($projectID, $model)
  2171. {
  2172. if(empty($model)) return false;
  2173. $checkList = $this->config->project->checkList->$model;
  2174. if(in_array($this->config->edition, array('max', 'ipd'))) $checkList = $this->config->project->maxCheckList->$model;
  2175. foreach($checkList as $module)
  2176. {
  2177. if($module == '') continue;
  2178. $type = '';
  2179. $table = constant('TABLE_'. strtoupper($module));
  2180. if($module == 'execution')
  2181. {
  2182. $this->app->loadConfig('execution');
  2183. $type = zget($this->config->execution->modelList, $model, '');
  2184. }
  2185. /* 检查对应类型的数据是否已经存在。 */
  2186. /* Check if the data of the type already exists. */
  2187. $object = $this->getDataByProject($table, $projectID, $type);
  2188. if(!empty($object)) return false;
  2189. }
  2190. return true;
  2191. }
  2192. /**
  2193. * 根据项目ID获取项目关联的对象。
  2194. * Get the objects under the project.
  2195. *
  2196. * @param string $table
  2197. * @param int $projectID
  2198. * @param string $type
  2199. * @access public
  2200. * @return object|bool
  2201. */
  2202. public function getDataByProject($table, $projectID, $type = '')
  2203. {
  2204. return $this->dao->select('id')->from($table)
  2205. ->where('project')->eq($projectID)
  2206. ->beginIF(!empty($type))->andWhere('type')->eq($type)->fi()
  2207. ->fetch();
  2208. }
  2209. /**
  2210. * Add plans.
  2211. *
  2212. * @param int $projectID
  2213. * @param array $plans
  2214. * @access public
  2215. * @return bool
  2216. */
  2217. public function addPlans($projectID, $plans)
  2218. {
  2219. $planIdList = array();
  2220. foreach($plans as $planList)
  2221. {
  2222. if(!$planList) continue;
  2223. foreach($planList as $planID)
  2224. {
  2225. $planIdList[$planID] = $planID;
  2226. }
  2227. }
  2228. $planIdList = toIntArray($planIdList);
  2229. if(empty($planIdList)) return true;
  2230. $planStoryGroup = $this->loadModel('story')->getStoriesByPlanIdList($planIdList);
  2231. foreach($planIdList as $planID)
  2232. {
  2233. $planStories = $planProducts = array();
  2234. $planStory = isset($planStoryGroup[$planID]) ? $planStoryGroup[$planID] : array();
  2235. if(!empty($planStory))
  2236. {
  2237. foreach($planStory as $id => $story)
  2238. {
  2239. if($story->status != 'active')
  2240. {
  2241. unset($planStory[$id]);
  2242. continue;
  2243. }
  2244. $planProducts[$story->id] = $story->product;
  2245. }
  2246. $planStories = array_keys($planStory);
  2247. $this->loadModel('execution')->linkStory($projectID, $planStories);
  2248. }
  2249. }
  2250. return !dao::isError();
  2251. }
  2252. /**
  2253. * 更新项目关联的计划。
  2254. * Update project's plans.
  2255. *
  2256. * @param int $projectID
  2257. * @param array $plans
  2258. * @access public
  2259. * @return bool
  2260. */
  2261. public function updatePlans($projectID, $plans)
  2262. {
  2263. /* Transfer multi dimensional array to one dimensional array. */
  2264. $newPlans = array();
  2265. if(isset($plans))
  2266. {
  2267. foreach($plans as $planList)
  2268. {
  2269. if(is_array($planList))
  2270. foreach($planList as $planID) $newPlans[$planID] = $planID;
  2271. }
  2272. }
  2273. if(empty($newPlans)) return true;
  2274. /* Fetch old plan list. */
  2275. $oldPlanList = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT)
  2276. ->where('project')->eq($projectID)
  2277. ->andWhere('plan')->ne(0)
  2278. ->fetchPairs();
  2279. $oldPlans = array();
  2280. foreach($oldPlanList as $oldPlanIDList)
  2281. {
  2282. if(is_numeric($oldPlanIDList)) $oldPlans[$oldPlanIDList] = $oldPlanIDList;
  2283. if(!is_numeric($oldPlanIDList))
  2284. {
  2285. $oldPlanIDList = explode(',', $oldPlanIDList);
  2286. foreach($oldPlanIDList as $oldPlanID) $oldPlans[$oldPlanID] = $oldPlanID;
  2287. }
  2288. }
  2289. if(count($newPlans) != count($oldPlans) || !empty(array_diff($newPlans, $oldPlans))) $this->loadModel('productplan')->linkProject($projectID, $newPlans);
  2290. return !dao::isError();
  2291. }
  2292. /**
  2293. * 更新项目排序
  2294. * update project order
  2295. *
  2296. * @param array $idList
  2297. * @param string $orderBy
  2298. *
  2299. * @access public
  2300. * @return bool
  2301. */
  2302. public function updateOrder($idList, $orderBy)
  2303. {
  2304. $projects = $this->dao->select('id,`order`')->from(TABLE_PROJECT)
  2305. ->where('id')->in($idList)
  2306. ->orderBy($orderBy)
  2307. ->fetchPairs('order', 'id');
  2308. foreach($projects as $order => $id)
  2309. {
  2310. $newID = array_shift($idList);
  2311. if($id == $newID) continue;
  2312. $this->dao->update(TABLE_PROJECT)
  2313. ->set('`order`')->eq($order)
  2314. ->set('lastEditedBy')->eq($this->app->user->account)
  2315. ->set('lastEditedDate')->eq(helper::now())
  2316. ->where('id')->eq($newID)
  2317. ->exec();
  2318. }
  2319. return !dao::isError();
  2320. }
  2321. /**
  2322. * 获取项目集的最小开始时间
  2323. * Get program min begin
  2324. *
  2325. * @param int $objectID
  2326. *
  2327. * @access public
  2328. * @return string
  2329. */
  2330. public function getProgramMinBegin($objectID)
  2331. {
  2332. return $this->dao->select('`begin` as minBegin')->from(TABLE_PROGRAM)
  2333. ->where('id')->ne($objectID)
  2334. ->andWhere('deleted')->eq(0)
  2335. ->andWhere('path')->like("%,{$objectID},%")
  2336. ->orderBy('begin_asc')
  2337. ->fetch('minBegin');
  2338. }
  2339. /**
  2340. * 获取项目集的最大结束时间
  2341. * get program max end
  2342. *
  2343. * @param int $objectID
  2344. *
  2345. * @access public
  2346. * @return string
  2347. */
  2348. public function getProgramMaxEnd($objectID)
  2349. {
  2350. return $this->dao->select('`end` as maxEnd')->from(TABLE_PROGRAM)
  2351. ->where('id')->ne($objectID)
  2352. ->andWhere('deleted')->eq(0)
  2353. ->andWhere('path')->like("%,{$objectID},%")
  2354. ->andWhere("`end` is true")
  2355. ->orderBy('end_desc')
  2356. ->fetch('maxEnd');
  2357. }
  2358. /**
  2359. * 获取执行的团队成员。
  2360. * get execution members.
  2361. *
  2362. * @param string $account
  2363. * @param array $executions
  2364. *
  2365. * @access protected
  2366. * @return array
  2367. */
  2368. protected function getExecutionMembers($account, $executions)
  2369. {
  2370. return $this->dao->select('t1.root,t2.name')->from(TABLE_TEAM)->alias('t1')
  2371. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.root=t2.id')
  2372. ->where('t1.root')->in($executions)
  2373. ->andWhere('t1.type')->eq('execution')
  2374. ->andWhere('t1.account')->eq($account)
  2375. ->fetchPairs();
  2376. }
  2377. /**
  2378. * 根据项目状态和权限生成列表中操作列按钮。
  2379. * Build table action menu for project browse page.
  2380. *
  2381. * @param object $project
  2382. * @access public
  2383. * @return array
  2384. */
  2385. public function buildActionList($project)
  2386. {
  2387. $actions = array();
  2388. /* Set status button. */
  2389. if($project->status == 'wait' || $project->status == 'suspended') $actions[] = 'start';
  2390. if($project->status == 'doing') $actions[] = 'close';
  2391. if($project->status == 'closed') $actions[] = 'active';
  2392. /* A drop-down button to set the status. */
  2393. $canClose = common::hasPriv('project', 'close') && $project->status != 'doing';
  2394. $canActivate = common::hasPriv('project', 'activate') && $project->status != 'closed';
  2395. if(common::hasPriv('project', 'suspend') || $canClose || $canActivate)
  2396. {
  2397. $menu = 'pause';
  2398. if($project->status != 'doing') $menu .= ',close';
  2399. if($project->status != 'closed') $menu .= ',active';
  2400. $actions[] = 'other:' . $menu;
  2401. }
  2402. $actions[] = 'edit';
  2403. $actions[] = 'group';
  2404. if($this->config->vision != 'lite')
  2405. {
  2406. $actions[] = 'perm';
  2407. if(common::hasPriv('project', 'manageProducts') || common::hasPriv('project', 'whitelist') || common::hasPriv('project', 'delete')) $actions[] = 'more:link,whitelist,delete';
  2408. }
  2409. else
  2410. {
  2411. $actions[] = 'whitelist';
  2412. $actions[] = 'delete';
  2413. }
  2414. /* Set whether the button can be clicked. */
  2415. foreach($actions as &$action)
  2416. {
  2417. if(strpos($action, ':'))
  2418. {
  2419. $actionList = explode(':', $action);
  2420. $action = $actionList[0] . ':';
  2421. foreach(explode(',', $actionList[1]) as $actionName)
  2422. {
  2423. if(!$this->isClickable($project, $actionName)) $action .= '-';
  2424. $action .= $actionName . ',';
  2425. }
  2426. continue;
  2427. }
  2428. if(!$this->isClickable($project, $action)) $action = array('name' => $action, 'disabled' => true);
  2429. }
  2430. return $actions;
  2431. }
  2432. /**
  2433. * 格式化要在数据表格打印的数据。
  2434. * Format data for list.
  2435. *
  2436. * @param object $project
  2437. * @param array $PMList
  2438. * @access public
  2439. * @return object
  2440. */
  2441. public function formatDataForList($project, $PMList)
  2442. {
  2443. $projectBudget = $this->getBudgetWithUnit($project->budget);
  2444. $project->budget = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future;
  2445. $project->statusTitle = $this->processStatus('project', $project);
  2446. $project->estimate = $project->estimate . $this->lang->project->workHourUnit;
  2447. $project->consume = $project->consumed . $this->lang->project->workHourUnit;
  2448. $project->surplus = $project->left . $this->lang->project->workHourUnit;
  2449. $project->progress = $project->progress;
  2450. $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end;
  2451. $project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->consumed / $this->config->execution->defaultWorkhours, 2) : 0;
  2452. if($project->PM)
  2453. {
  2454. $user = zget($PMList, $project->PM, '');
  2455. $project->PM = zget($user, 'realname', $project->PM);
  2456. $project->PMAvatar = zget($user, 'avatar', '');
  2457. $project->PMUserID = zget($user, 'id', 0);
  2458. }
  2459. return $project;
  2460. }
  2461. /**
  2462. * 记录项目启动时的计划完成日期。
  2463. * Record the end date when the project is started.
  2464. *
  2465. * @param int $projectID
  2466. * @access public
  2467. * @return bool
  2468. */
  2469. public function recordFirstEnd($projectID)
  2470. {
  2471. $project = $this->dao->select('end')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
  2472. $this->dao->update(TABLE_PROJECT)->set('firstEnd')->eq(helper::isZeroDate($project->end) ? null : $project->end)->where('id')->eq($projectID)->exec();
  2473. return !dao::isError();
  2474. }
  2475. /*
  2476. * 获取旧页面1.5级下拉。
  2477. * Get project swapper.
  2478. *
  2479. * @param int $projectID
  2480. * @param string $currentModule
  2481. * @param string $currentMethod
  2482. * @access public
  2483. * @return string
  2484. */
  2485. /**
  2486. * @param int $projectID
  2487. * @param string $currentModule
  2488. * @param string $currentMethod
  2489. */
  2490. public function getSwitcher($projectID, $currentModule, $currentMethod)
  2491. {
  2492. if($currentModule == 'project' && $currentMethod == 'browse') return '';
  2493. $currentProjectName = $this->lang->project->common;
  2494. if($projectID)
  2495. {
  2496. $currentProject = $this->getById($projectID);
  2497. $currentProjectName = $currentProject->name;
  2498. }
  2499. if($this->app->viewType == 'mhtml' && $projectID)
  2500. {
  2501. $output = $this->lang->project->common . $this->lang->hyphen;
  2502. $output .= "<a id='currentItem' href=\"javascript:showSearchMenu('project', '$projectID', '$currentModule', '$currentMethod', '')\">{$currentProjectName} <span class='icon-caret-down'></span></a><div id='currentItemDropMenu' class='hidden affix enter-from-bottom layer'></div>";
  2503. return $output;
  2504. }
  2505. $this->app->loadConfig('index');
  2506. $dropMenuMethod = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'ajaxGetOldDropMenu' : 'ajaxGetDropMenu';
  2507. $dataRide = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'searchList' : 'dropmenu' ;
  2508. $dropMenuLink = helper::createLink('project', $dropMenuMethod, "objectID=$projectID&module=$currentModule&method=$currentMethod");
  2509. $output = "<div class='btn-group header-btn' id='swapper'><button data-toggle='dropdown' type='button' class='btn' id='currentItem' title='{$currentProjectName}'><span class='text'>{$currentProjectName}</span> <span class='caret' style='margin-bottom: -1px'></span></button><div id='dropMenu' class='dropdown-menu search-list' data-ride='{$dataRide}' data-url='$dropMenuLink'>";
  2510. $output .= '<div class="input-control search-box has-icon-left has-icon-right search-example"><input type="search" class="form-control search-input" /><label class="input-control-icon-left search-icon"><i class="icon icon-search"></i></label><a class="input-control-icon-right search-clear-btn"><i class="icon icon-close icon-sm"></i></a></div>';
  2511. $output .= "</div></div>";
  2512. return $output;
  2513. }
  2514. /**
  2515. * 检查阶段是否有相关数据。
  2516. * Check whether the stage has related data.
  2517. *
  2518. * @param int $executionID
  2519. * @access public
  2520. * @return bool
  2521. */
  2522. public function hasStageData($executionID)
  2523. {
  2524. if(empty($executionID)) return false;
  2525. $childStage = $this->dao->select('id')->from(TABLE_EXECUTION)->where('parent')->eq($executionID)->andWhere('type')->in('sprint,stage,kanban')->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2526. if(!empty($childStage)) return false;
  2527. $task = $this->dao->select('id')->from(TABLE_TASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2528. if(!empty($task)) return true;
  2529. $effort = $this->dao->select('id')->from(TABLE_EFFORT)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2530. if(!empty($effort)) return true;
  2531. $bug = $this->dao->select('id')->from(TABLE_BUG)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2532. if(!empty($bug)) return true;
  2533. $story = $this->dao->select('t1.id')->from(TABLE_STORY)->alias('t1')
  2534. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story')
  2535. ->where('t2.project')->eq($executionID)
  2536. ->andWhere('t1.deleted')->eq(0)
  2537. ->limit(1)->fetch();
  2538. if(!empty($story)) return true;
  2539. $case = $this->dao->select('t1.id')->from(TABLE_CASE)->alias('t1')
  2540. ->leftJoin(TABLE_PROJECTCASE)->alias('t2')->on('t1.id=t2.case')
  2541. ->where('t2.project')->eq($executionID)
  2542. ->andWhere('t1.deleted')->eq(0)
  2543. ->limit(1)->fetch();
  2544. if(!empty($case)) return true;
  2545. $build = $this->dao->select('id')->from(TABLE_BUILD)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2546. if(!empty($build)) return true;
  2547. $testtask = $this->dao->select('id')->from(TABLE_TESTTASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2548. if(!empty($testtask)) return true;
  2549. $testreport = $this->dao->select('id')->from(TABLE_TESTREPORT)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2550. if(!empty($testreport)) return true;
  2551. $doc = $this->dao->select('id')->from(TABLE_DOC)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2552. if(!empty($doc)) return true;
  2553. $doclib = $this->dao->select('id')->from(TABLE_DOCLIB)->where('execution')->eq($executionID)->andWhere('type')->eq('execution')->andWhere('main')->ne('1')->andWhere('deleted')->eq(0)->limit(1)->fetch();
  2554. if(!empty($doclib)) return true;
  2555. $libID = $this->dao->select('id')->from(TABLE_DOCLIB)->where('type')->eq('execution')->andWhere('execution')->eq($executionID)->andWhere('main')->eq('1')->limit(1)->fetch('id');
  2556. $module = $this->dao->select('id')->from(TABLE_MODULE)
  2557. ->where('deleted')->eq(0)
  2558. ->andWhere('((type')->eq('task')
  2559. ->andWhere('root')->eq($executionID)
  2560. ->markRight(1)
  2561. ->orWhere('(type')->eq('doc')
  2562. ->andWhere('root')->eq($libID)
  2563. ->markRight(2)
  2564. ->limit(1)
  2565. ->fetch();
  2566. if(!empty($module)) return true;
  2567. return false;
  2568. }
  2569. /**
  2570. * 获取瀑布/融合瀑布项目不允许解除关联的产品。
  2571. * Get waterfall/waterfallplus unmodifiable products.
  2572. *
  2573. * @param object $project
  2574. * @param array $linkedProducts
  2575. * @access public
  2576. * @return array
  2577. */
  2578. public function getDisabledProducts($project, $linkedProducts = array())
  2579. {
  2580. $disabledProducts = array();
  2581. if(empty($linkedProducts)) return $disabledProducts;
  2582. if(!in_array($project->model, array('waterfall', 'waterfallplus'))) return $disabledProducts;
  2583. $projectStories = $this->dao->select('t2.product')->from(TABLE_STORY)->alias('t1')
  2584. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story')
  2585. ->where('t1.deleted')->eq(0)
  2586. ->andWhere('t1.type')->eq('story')
  2587. ->andWhere('t2.project')->eq($project->id)
  2588. ->andWhere('t2.product')->in(array_keys($linkedProducts))
  2589. ->fetchPairs();
  2590. $executionIdSQL = $this->dao->select('id')->from(TABLE_EXECUTION)->where('deleted')->eq('0')->andWhere('project')->eq($project->id)->get();
  2591. $executionProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->subIn($executionIdSQL)->andWhere('product')->in(array_keys($linkedProducts))->fetchPairs();
  2592. $executionStories = array();
  2593. if($project->stageBy == 'project')
  2594. {
  2595. $executionStories = $this->dao->select('t2.product')->from(TABLE_STORY)->alias('t1')
  2596. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story')
  2597. ->where('t1.deleted')->eq(0)
  2598. ->andWhere('t1.type')->eq('story')
  2599. ->andWhere('t2.project')->subIn($executionIdSQL)
  2600. ->andWhere('t2.product')->in($executionProducts)
  2601. ->fetchPairs();
  2602. }
  2603. foreach($linkedProducts as $productID => $product)
  2604. {
  2605. if($project->stageBy == 'product')
  2606. {
  2607. if(isset($projectStories[$productID]) && !isset($executionProducts[$productID])) $disabledProducts[$productID] = $this->lang->project->disabledHint->linkedStory;
  2608. if(!isset($projectStories[$productID]) && isset($executionProducts[$productID])) $disabledProducts[$productID] = $this->lang->project->disabledHint->createdStage;
  2609. if(isset($projectStories[$productID]) && isset($executionProducts[$productID])) $disabledProducts[$productID] = $this->lang->project->disabledHint->linkedStoryAndStage;
  2610. }
  2611. else
  2612. {
  2613. if(isset($projectStories[$productID]) && !isset($executionStories[$productID])) $disabledProducts[$productID] = $this->lang->project->disabledHint->linkedStory;
  2614. if(isset($projectStories[$productID]) && isset($executionStories[$productID])) $disabledProducts[$productID] = $this->lang->project->disabledHint->linkedStoryAndExecution;
  2615. }
  2616. }
  2617. return $disabledProducts;
  2618. }
  2619. /**
  2620. * 查找项目下是否有冻结的对象。
  2621. * Has frozen object.
  2622. *
  2623. * @param int $projectID
  2624. * @param string $category SRS|PP
  2625. * @return bool
  2626. */
  2627. public function hasFrozenObject($projectID, $category)
  2628. {
  2629. if(empty($projectID) || empty($category)) return false;
  2630. $frozenObject = $this->dao->select('1')->from(TABLE_PROJECTDELIVERABLE)->alias('t1')
  2631. ->leftJoin(TABLE_DELIVERABLE)->alias('t2')->on('t1.deliverable = t2.id')
  2632. ->where('t1.project')->eq($projectID)
  2633. ->andWhere('t1.frozen')->ne('')
  2634. ->andWhere('t2.category')->eq($category)
  2635. ->limit(1)
  2636. ->fetch('1');
  2637. return !empty($frozenObject);
  2638. }
  2639. }