zen.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. <?php
  2. /**
  3. * The zen file of programplan module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author lanzongjun <lanzongjun@easycorp.ltd>
  8. * @link https://www.zentao.net
  9. */
  10. class programplanZen extends programplan
  11. {
  12. /**
  13. * Check legally date.
  14. *
  15. * @param object $plan
  16. * @param object $project
  17. * @param object|null $parent
  18. * @access private
  19. * @return void
  20. * @param int $rowID
  21. */
  22. private function checkLegallyDate($plan, $project, $parent, $rowID)
  23. {
  24. if(isset($plan->enabled) && $plan->enabled == 'off') return;
  25. if(!empty($project->isTpl)) return; // 模板不校验日期
  26. $beginIsZeroDate = helper::isZeroDate($plan->begin);
  27. $endIsZeroDate = helper::isZeroDate($plan->end);
  28. if(!$beginIsZeroDate and !$endIsZeroDate and $plan->end < $plan->begin) dao::$errors["end[{$rowID}]"] = $this->lang->programplan->error->planFinishSmall;
  29. if(!empty($parent))
  30. {
  31. if(!$beginIsZeroDate and $plan->begin < $parent->begin) dao::$errors["begin[{$rowID}]"] = sprintf($this->lang->programplan->error->letterParent, $parent->begin);
  32. if(!$endIsZeroDate and $plan->end > $parent->end) dao::$errors["end[{$rowID}]"] = sprintf($this->lang->programplan->error->greaterParent, $parent->end);
  33. }
  34. if(!$beginIsZeroDate and $plan->begin < $project->begin) dao::$errors["begin[{$rowID}]"] = sprintf($this->lang->programplan->errorBegin, $project->begin);
  35. if(!$endIsZeroDate and $plan->end > $project->end) dao::$errors["end[{$rowID}]"] = sprintf($this->lang->programplan->errorEnd, $project->end);
  36. }
  37. /**
  38. * Process formData before use it to create programplan.
  39. *
  40. * @param int $projectID
  41. * @param int $parentID
  42. * @access protected
  43. * @return array|false
  44. */
  45. protected function buildPlansForCreate($projectID, $parentID)
  46. {
  47. /* Check parent name is not empty when has child task. */
  48. $levelNames = array();
  49. foreach($this->post->level as $i => $level)
  50. {
  51. $level = (int)$level;
  52. $levelNames[$level]['name'] = trim($this->post->name[$i]);
  53. $levelNames[$level]['index'] = $i;
  54. $preLevel = $level - 1;
  55. if($level > 0 && !empty($levelNames[$level]['name']) && empty($levelNames[$preLevel]['name'])) dao::$errors["name[" . $levelNames[$preLevel]['index'] . "]"] = $this->lang->programplan->error->emptyParentName;
  56. }
  57. if(dao::isError()) return false;
  58. $project = $this->loadModel('project')->getByID($projectID);
  59. $oldPlans = $this->loadModel('programplan')->getStage($projectID);
  60. if($parentID) $parentStage = $this->programplan->getByID($parentID);
  61. $fields = $this->config->programplan->form->create;
  62. foreach(explode(',', $this->config->programplan->create->requiredFields) as $field)
  63. {
  64. $field = trim($field);
  65. if(isset($fields[$field])) $fields[$field]['required'] = true;
  66. }
  67. $totalPercent = array();
  68. $lastLevels = array();
  69. $names = $codes = array();
  70. $plans = form::batchData($fields)->get();
  71. $orders = $this->programplan->computeOrders(array(), $plans);
  72. $group = 0;
  73. $levelGroup = array();
  74. $prevLevel = 0;
  75. foreach($plans as $rowID => $plan)
  76. {
  77. if(empty($parentID) and empty($oldPlans)) $plan->id = '';
  78. $plan->days = isset($plan->enabled) && $plan->enabled == 'on' ? $this->programplan->calcDaysForStage($plan->begin, $plan->end) : 0;
  79. $plan->project = $projectID;
  80. $plan->order = (int)array_shift($orders);
  81. $plan->hasProduct = $project->hasProduct;
  82. $plan->parent = $parentID ? $parentID : $projectID;
  83. $plan->isTpl = $project->isTpl;
  84. if($plan->id && isset($oldPlans[$plan->id])) $plan->parent = $oldPlans[$plan->id]->parent;
  85. if($plan->type != 'stage' || empty($plan->percent)) $plan->percent = 0; // 非阶段类型,工作量占比为0
  86. if(empty($plan->days)) $plan->days = helper::diffDate($plan->end, $plan->begin) + 1;
  87. if(!empty($parentID) && !empty($parentStage) && $parentStage->attribute != 'mix') $plan->attribute = $parentStage->attribute;;
  88. if(!empty($parentID) && !empty($parentStage)) $plan->acl = $parentStage->acl;
  89. if(in_array($this->config->edition, array('max', 'ipd')) && !dao::isError())
  90. {
  91. $plan->planDuration = $this->programplan->getDuration((string)$plan->begin, (string)$plan->end);
  92. $plan->realDuration = $this->programplan->getDuration((string)$plan->realBegan, (string)$plan->realEnd);
  93. }
  94. /* 阶段停用和删除是一样的效果,方便控制相关数据的展示。 */
  95. if($project->model == 'ipd') $plan->deleted = $plan->enabled == 'off' ? '1' : '0';
  96. /* Check duplicated names to avoid to save same names. */
  97. if(in_array($plan->name, $names)) dao::$errors["name[{$rowID}]"] = empty($plan->type) ? $this->lang->programplan->error->sameName : str_replace($this->lang->execution->stage, '', $this->lang->programplan->error->sameName);
  98. if(isset($plan->code) && (!isset($plan->enabled) || (isset($plan->enabled) && $plan->enabled == 'on')))
  99. {
  100. if(in_array($plan->code, $codes)) dao::$errors["code[{$rowID}]"] = sprintf($this->lang->error->repeat, $plan->type == 'stage' ? $this->lang->execution->code : $this->lang->code, $plan->code);
  101. if(!empty($this->config->setCode) && empty($plan->code) && strpos(",{$this->config->execution->create->requiredFields},", ',code,') !== false) dao::$errors["code[{$rowID}]"] = sprintf($this->lang->error->notempty, $plan->type == 'stage' ? $this->lang->execution->code : $this->lang->code);
  102. }
  103. if(!is_numeric($plan->percent) || $plan->percent < 0) dao::$errors["percent[$rowID]"] = $this->lang->programplan->error->percentNumber;
  104. $customKey = 'create' . ucfirst($project->model) . 'Fields';
  105. if(strpos(",{$this->config->programplan->custom->$customKey},", ',percent,') !== false && isset($plan->percent) && !dao::isError())
  106. {
  107. if($plan->level == 0)
  108. {
  109. $levelGroup = array();
  110. $totalPercent[0] = isset($totalPercent[0]) ? $totalPercent[0] + $plan->percent : $plan->percent;
  111. }
  112. else
  113. {
  114. if(isset($levelGroup[$plan->level]))
  115. {
  116. $group = $levelGroup[$plan->level];
  117. }
  118. elseif($plan->level != $prevLevel)
  119. {
  120. $group++;
  121. }
  122. $totalPercent[$group] = isset($totalPercent[$group]) ? $totalPercent[$group] + $plan->percent : $plan->percent;
  123. }
  124. $levelGroup[$plan->level] = $group;
  125. }
  126. $prevLevel = $plan->level;
  127. $names[] = $plan->name;
  128. if(!empty($plan->code)) $codes[] = $plan->code;
  129. $this->checkLegallyDate($plan, $project, !empty($parentStage) ? $parentStage : null, $rowID);
  130. $lastLevels[$plan->level] = $plan;
  131. if($plan->level > 0) $this->checkLegallyDate($plan, $project, zget($lastLevels, $plan->level - 1, null), $rowID);
  132. }
  133. foreach($totalPercent as $group => $percent)
  134. {
  135. if(!empty($this->config->setPercent) and $percent > 100)
  136. {
  137. dao::$errors["percent"] = $this->lang->programplan->error->percentOver;
  138. break;
  139. }
  140. }
  141. return $plans;
  142. }
  143. /**
  144. * 生成创建项目阶段视图数据。
  145. * Build create view data.
  146. *
  147. * @param object $viewData
  148. * @access protected
  149. * @return void
  150. */
  151. public function buildCreateView($viewData)
  152. {
  153. /* Compute fields for create view. */
  154. list($visibleFields, $requiredFields, $customFields, $showFields, $defaultFields) = $this->computeFieldsCreateView($viewData);
  155. if($viewData->project->model == 'ipd') $this->config->programplan->form->create['attribute']['options'] = $this->lang->stage->ipdTypeList;
  156. $this->view->title = $this->lang->programplan->create . $this->lang->hyphen . $viewData->project->name;
  157. $this->view->productList = $viewData->productList;
  158. $this->view->project = $viewData->project;
  159. $this->view->productID = $viewData->productID ?: key($viewData->productList);
  160. $this->view->stages = empty($viewData->planID) ? $this->loadModel('stage')->getStages('order_asc', 0, $viewData->project->workflowGroup) : array();
  161. $this->view->programPlan = $viewData->programPlan;
  162. $this->view->plans = $viewData->plans;
  163. $this->view->planID = $viewData->planID;
  164. $this->view->syncData = $viewData->syncData;
  165. $this->view->type = 'lists';
  166. $this->view->executionType = $viewData->executionType;
  167. $this->view->PMUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst', $viewData->project->PM);
  168. $this->view->custom = $viewData->executionType == 'stage' ? 'custom' : 'customAgilePlus';
  169. $this->view->customFields = $customFields;
  170. $this->view->showFields = $showFields;
  171. $this->view->visibleFields = $visibleFields;
  172. $this->view->requiredFields = $requiredFields;
  173. $this->view->defaultFields = $defaultFields;
  174. $this->view->colspan = count($visibleFields) + 3;
  175. $this->view->enableOptionalAttr = empty($viewData->programPlan) || (!empty($viewData->programPlan) && $viewData->programPlan->attribute == 'mix');
  176. }
  177. /**
  178. * 处理编辑阶段的请求数据。
  179. * Processing edit request data.
  180. *
  181. * @param int $planID
  182. * @param int $projectID
  183. * @param object $plan
  184. * @param object|null $parentStage
  185. * @access protected
  186. * @return object|false
  187. */
  188. protected function prepareEditPlan($planID, $projectID, $plan, $parentStage = null)
  189. {
  190. if($plan->end < $plan->begin) dao::$errors['end'] = $this->lang->programplan->error->planFinishSmall;
  191. if($plan->parent)
  192. {
  193. if(!empty($parentStage) && $plan->begin < $parentStage->begin) dao::$errors['begin'] = sprintf($this->lang->programplan->error->letterParent, $parentStage->begin);
  194. if(!empty($parentStage) && $plan->end > $parentStage->end) dao::$errors['end'] = sprintf($this->lang->programplan->error->greaterParent, $parentStage->end);
  195. if(dao::isError()) return false;
  196. }
  197. if(!is_numeric($plan->percent) || $plan->percent < 0) dao::$errors['percent'] = $this->lang->programplan->error->percentNumber;
  198. if($projectID) $this->loadModel('execution')->checkBeginAndEndDate($projectID, $plan->begin, $plan->end, $plan->parent);
  199. if(dao::isError()) return false;
  200. $project = $this->loadModel('project')->getById($projectID);
  201. $oldPlan = $this->programplan->getByID($planID);
  202. if(!empty($this->config->setPercent))
  203. {
  204. if($plan->parent > 0)
  205. {
  206. $childrenTotalPercent = $this->programplan->getTotalPercent($parentStage, true);
  207. $childrenTotalPercent = $plan->parent == $oldPlan->parent ? ($childrenTotalPercent - $oldPlan->percent + $plan->percent) : ($childrenTotalPercent + $plan->percent);
  208. if($childrenTotalPercent > 100) dao::$errors['percent'][] = $this->lang->programplan->error->percentOver;
  209. }
  210. else
  211. {
  212. /* 相同父阶段的子阶段工作量占比之和不超过100%。 */
  213. /* The workload of the parent plan cannot exceed 100%. */
  214. $oldPlan->parent = $plan->parent;
  215. $totalPercent = $this->programplan->getTotalPercent($oldPlan);
  216. $totalPercent = $totalPercent + $plan->percent;
  217. if($totalPercent > 100) dao::$errors['percent'][] = $this->lang->programplan->error->percentOver;
  218. }
  219. }
  220. if(dao::isError()) return false;
  221. /* 如果是调研阶段,设置默认值。*/
  222. /* If it is research stage, set default value. */
  223. if($project->model == 'research')
  224. {
  225. $plan->acl = $oldPlan->acl;
  226. $plan->attribute = $oldPlan->attribute;
  227. $plan->milestone = $oldPlan->milestone;
  228. }
  229. return $plan;
  230. }
  231. /**
  232. * 生成编辑阶段数据。
  233. * Build edit view data.
  234. *
  235. * @param object $plan
  236. * @access protected
  237. * @return void
  238. */
  239. public function buildEditView($plan)
  240. {
  241. $this->loadModel('project');
  242. $this->loadModel('execution');
  243. $this->app->loadLang('stage');
  244. $project = $this->project->getByID($plan->project);
  245. $parentStage = $this->project->getByID($plan->parent, 'stage');
  246. $enableOptionalAttr = empty($parentStage) || (!empty($parentStage) && $parentStage->attribute == 'mix');
  247. if($project->model == 'ipd') $enableOptionalAttr = false;
  248. $this->view->title = $this->lang->programplan->edit;
  249. $this->view->isCreateTask = $this->programplan->isCreateTask($plan->id);
  250. $this->view->plan = $plan;
  251. $this->view->project = $project;
  252. $this->view->parentStageList = $this->programplan->getParentStageList($plan->project, $plan->id, $plan->product);
  253. $this->view->enableOptionalAttr = $enableOptionalAttr;
  254. $this->view->isTopStage = $this->programplan->isTopStage($plan->id);
  255. $this->view->isLeafStage = $this->programplan->checkLeafStage($plan->id);
  256. $this->view->PMUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst', $plan->PM);
  257. $this->view->project = $this->project->getByID($plan->project);
  258. $this->view->requiredFields = $this->config->execution->edit->requiredFields;
  259. $this->view->hasUploadedDeliverable = in_array($this->config->edition, array('max', 'ipd')) ? $this->execution->hasUploadedDeliverable($plan) : false;
  260. $this->display();
  261. }
  262. /**
  263. * 生成自定义设置视图。
  264. * Build custom setting view form data.
  265. *
  266. * @param string $owner
  267. * @param string $module
  268. * @param array $customFields
  269. * @access protected
  270. * @return void
  271. */
  272. protected function buildAjaxCustomView($owner, $module, $customFields)
  273. {
  274. $stageCustom = $this->loadModel('setting')->getItem("owner=$owner&module=$module&section=browse&key=stageCustom");
  275. $ganttFields = $this->setting->getItem("owner=$owner&module=$module&section=ganttCustom&key=ganttFields");
  276. $zooming = $this->setting->getItem("owner=$owner&module=$module&section=ganttCustom&key=zooming");
  277. $this->view->zooming = $zooming;
  278. $this->view->customFields = $customFields;
  279. $this->view->showFields = $this->config->programplan->ganttCustom->ganttFields;
  280. $this->view->ganttFields = $ganttFields;
  281. $this->view->stageCustom = $stageCustom;
  282. $this->display();
  283. }
  284. /**
  285. * 计算创建视图的可见字段字段和必填字段。
  286. * Compute visibleFields and requiredFields for create view.
  287. *
  288. * @param object $viewData
  289. * @access protected
  290. * @return array
  291. */
  292. protected function computeFieldsCreateView($viewData)
  293. {
  294. $visibleFields = array();
  295. $requiredFields = array();
  296. $customFields = array();
  297. $customModel = !empty($viewData->project->model) ? $viewData->project->model : '';
  298. $custom = $viewData->executionType == 'stage' ? 'custom' : 'customAgilePlus';
  299. $customCreateFields = $viewData->executionType == 'stage' ? 'customCreateFields' : 'customAgilePlusCreateFields';
  300. $createFields = $custom == 'customAgilePlus' ? 'createFields' : 'create' . ucfirst($customModel) . 'Fields';
  301. $defaultFields = $this->config->programplan->$custom->defaultFields;
  302. foreach(explode(',', $this->config->programplan->list->$customCreateFields) as $field) $customFields[$field] = $this->lang->programplan->{$field};
  303. $createRequiredFields = $this->config->execution->create->requiredFields;
  304. $showFields = $this->config->programplan->$custom->$createFields;
  305. $checkCodeIsRequired = !empty($this->config->setCode) && strpos(',' . trim($createRequiredFields, ',') . ',', ',code,') !== false;
  306. if($checkCodeIsRequired) $showFields .= ',code';
  307. foreach(explode(',', $showFields) as $field)
  308. {
  309. if($field) $visibleFields[$field] = '';
  310. }
  311. if($viewData->project->model == 'waterfallplus') $createRequiredFields = 'type,' . trim($createRequiredFields, ',');
  312. if($viewData->project->model == 'ipd') $createRequiredFields = ($viewData->planID ? 'type,' : 'enabled,point,type,') . trim($createRequiredFields, ',');
  313. foreach(explode(',', $createRequiredFields) as $field)
  314. {
  315. if($field)
  316. {
  317. $requiredFields[$field] = '';
  318. if(strpos(",{$this->config->programplan->list->$customCreateFields},", ",{$field},") !== false) $visibleFields[$field] = '';
  319. }
  320. }
  321. if(empty($this->config->setPercent)) unset($visibleFields['percent'], $requiredFields['percent']);
  322. if(empty($this->config->setCode)) unset($visibleFields['code'], $requiredFields['code']);
  323. if($checkCodeIsRequired) unset($customFields['code']);
  324. return array($visibleFields, $requiredFields, $customFields, $showFields, $defaultFields);
  325. }
  326. /**
  327. * 生成阶段列表页阶段数据。
  328. * Build gantt browse stage view data.
  329. *
  330. * @param int $projectID
  331. * @param int $productID
  332. * @param int $baselineID
  333. * @param string $type
  334. * @param string $orderBy
  335. * j
  336. * @access protected
  337. * @return array
  338. * @param string $browseType
  339. * @param int $queryID
  340. */
  341. protected function buildStages($projectID, $productID, $baselineID, $type, $orderBy, $browseType = '', $queryID = 0)
  342. {
  343. /* Obtain user page configuration items. */
  344. $this->loadModel('setting');
  345. $owner = $this->app->user->account;
  346. $module = 'programplan';
  347. if(!isset($this->config->programplan->browse->stageCustom)) $this->loadModel('setting')->setItem("$owner.$module.browse.stageCustom", 'date,task,point');
  348. $selectCustom = $this->loadModel('setting')->getItem("owner={$owner}&module=programplan&section=browse&key=stageCustom");
  349. $dateDetails = strpos($selectCustom, 'date') !== false ? false : true; // Gantt chart detail date display.
  350. foreach(explode(',', $this->config->programplan->custom->customGanttFields) as $field) $customFields[$field] = $this->lang->programplan->ganttCustom[$field];
  351. $this->view->customFields = $customFields;
  352. $this->view->showFields = $this->config->programplan->ganttCustom->ganttFields;
  353. $this->view->dateDetails = $dateDetails;
  354. $this->view->selectCustom = $selectCustom;
  355. /* Get data for gantt. */
  356. $stages = array();
  357. if($type == 'gantt') $stages = $this->programplan->getDataForGantt($projectID, $productID, $baselineID, $selectCustom, false, $browseType, $queryID);
  358. if($type == 'assignedTo') $stages = $this->programplan->getDataForGanttGroupByAssignedTo($projectID, $productID, $baselineID, $selectCustom, false, $browseType, $queryID);
  359. return $stages;
  360. }
  361. /**
  362. * 生成gantt图视图数据。
  363. * Build gantt browse view.
  364. *
  365. * @param int $projectID
  366. * @param int $productID
  367. * @param array $stages
  368. * @param string $type
  369. * @param string $orderBy
  370. * @param int $baselineID
  371. * @param string $browseType
  372. * @param int $queryID
  373. * @access protected
  374. * @return void
  375. */
  376. protected function buildBrowseView($projectID, $productID, $stages, $type, $orderBy, $baselineID, $browseType, $queryID)
  377. {
  378. $project = $this->loadModel('project')->getByID($projectID);
  379. if($project->model == 'ipd' and $this->config->edition == 'ipd')
  380. {
  381. $this->view->reviewPoints = $this->loadModel('review')->getReviewPointByProject($projectID);
  382. }
  383. $stageCustom = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=programplan&section=browse&key=stageCustom");
  384. $hasSearch = strpos(",{$stageCustom},", ',task,') !== false;
  385. if($hasSearch)
  386. {
  387. /* Build the search form. */
  388. $actionURL = $this->createLink('programplan', 'browse', "projectID=$projectID&productID=$productID&type=$type&orderBy=$orderBy&baselineID=$baselineID&browseType=bysearch&queryID=myQueryID");
  389. $executions = $this->programplan->getPairs($projectID, $productID, 'all');
  390. $this->loadModel('execution')->buildTaskSearchForm($projectID, $executions, $queryID, $actionURL, 'projectTask');
  391. }
  392. $this->view->title = $this->lang->programplan->browse;
  393. $this->view->projectID = $projectID;
  394. $this->view->productID = $productID;
  395. $this->view->type = $type;
  396. $this->view->ganttType = $type;
  397. $this->view->plans = $stages;
  398. $this->view->orderBy = $orderBy;
  399. $this->view->project = $project;
  400. $this->view->users = $this->loadModel('user')->getPairs('noletter');
  401. $this->view->product = $this->loadModel('product')->getByID($productID);
  402. $this->view->productList = $this->loadModel('product')->getProductPairsByProject($projectID, 'all', '', false);
  403. $this->view->zooming = !empty($this->config->programplan->ganttCustom->zooming) ? $this->config->programplan->ganttCustom->zooming : 'day';
  404. $this->view->hasSearch = $hasSearch;
  405. $this->view->browseType = $browseType;
  406. $this->view->queryID = $queryID;
  407. $this->display();
  408. }
  409. /**
  410. * 重新排序阶段,将子阶段放在父阶段之后。
  411. * Reorder stage, put sub stage after parent stage.
  412. *
  413. * @param array $plans
  414. * @access public
  415. * @return array
  416. */
  417. public function sortPlans($plans)
  418. {
  419. $parents = array();
  420. foreach($plans as $plan) $parents[$plan->parent][$plan->id] = $plan->id;
  421. $getChildren = function($planID) use($parents, &$getChildren)
  422. {
  423. if(!isset($parents[$planID])) return array();
  424. $children = array();
  425. foreach($parents[$planID] as $childPlanID)
  426. {
  427. $children[$childPlanID] = $childPlanID;
  428. $children = arrayUnion($children, $getChildren($childPlanID));
  429. }
  430. return $children;
  431. };
  432. $sortedPlans = array();
  433. foreach($plans as $plan)
  434. {
  435. if(isset($sortedPlans[$plan->id])) continue;
  436. $sortedPlans[$plan->id] = $plan;
  437. $children = $getChildren($plan->id);
  438. foreach($children as $childID) $sortedPlans[$childID] = $plans[$childID];
  439. }
  440. return $sortedPlans;
  441. }
  442. }