/** * Gets budgeted cost for all quarters of specified year * * @param int $year The year * @param string $ccId optional The identifier of the cost centre or parent cost centre * @param string $projectId optional The identifier of the project * @throws InvalidArgumentException */ protected function getBudgetInfo($year, $ccId = null, $projectId = null) { if (!empty($projectId)) { $subjectId = $projectId; $subjectType = QuarterlyBudgetEntity::SUBJECT_TYPE_PROJECT; $subjectEntity = ProjectEntity::findPk($projectId); } else { if (!empty($ccId)) { $subjectId = $ccId; $subjectType = QuarterlyBudgetEntity::SUBJECT_TYPE_CC; $subjectEntity = CostCentreEntity::findPk($ccId); } } if ($subjectEntity->accountId !== $this->user->getAccountId()) { //throw new Scalr_Exception_InsufficientPermissions(); } if (empty($subjectId) || !preg_match('/^[[:xdigit:]-]{36}$/', $subjectId)) { throw new InvalidArgumentException(sprintf("Invalid identifier of the project or cost center.")); } if (!preg_match('/^\\d{4}$/', $year)) { throw new InvalidArgumentException(sprintf("Invalid year.")); } if ($subjectType == QuarterlyBudgetEntity::SUBJECT_TYPE_CC) { $collection = QuarterlyBudgetEntity::getCcBudget($year, $subjectId); $prevCollection = QuarterlyBudgetEntity::getCcBudget($year - 1, $subjectId); } else { $collection = QuarterlyBudgetEntity::getProjectBudget($year, $subjectId); $prevCollection = QuarterlyBudgetEntity::getProjectBudget($year - 1, $subjectId); } $quarters = new Quarters(SettingEntity::getQuarters(true)); $today = new DateTime('now', new DateTimeZone('UTC')); //Start dates for an each quarter $startDates = $quarters->getDays(); $budgets = []; for ($quarter = 1; $quarter <= 4; ++$quarter) { $period = $quarters->getPeriodForQuarter($quarter, $year); $prevPeriod = $quarters->getPeriodForQuarter($quarter, $year - 1); //Finds budget for specified quarter in the collection $entity = current($collection->filterByQuarter($quarter)); //Previous Year entity $prevEntity = current($prevCollection->filterByQuarter($quarter)); if ($entity instanceof QuarterlyBudgetEntity) { $budget = ['budget' => round($entity->budget), 'budgetFinalSpent' => round($entity->cumulativespend), 'spentondate' => $entity->spentondate instanceof DateTime ? $entity->spentondate->format('Y-m-d') : null, 'budgetSpent' => round($entity->cumulativespend)]; if ($budget['budget']) { $budget['budgetOverspend'] = max(round($entity->cumulativespend - $entity->budget), 0); $budget['budgetOverspendPct'] = round($budget['budgetOverspend'] / $budget['budget'] * 100); } } else { //Budget has not been set yet. $budget = ['budget' => 0, 'budgetFinalSpent' => 0, 'spentondate' => null, 'budgetSpent' => 0, 'budgetOverspend' => 0, 'budgetOverspendPct' => 0]; } $budget['year'] = $year; $budget['quarter'] = $quarter; $budget['startDate'] = $startDates[$quarter - 1]; //Whether this quarter has been closed or not $budget['closed'] = $period->end->format('Y-m-d') < gmdate('Y-m-d'); //The number of the days in the current quarter $daysInQuarter = $period->start->diff($period->end, true)->days + 1; //In case quarter is closed projection should be calculated if (!$budget['closed']) { $daysPassed = $period->start->diff($today, true)->days + 1; $budget['dailyAverage'] = $daysPassed == 0 ? 0 : round($budget['budgetSpent'] / $daysPassed, 2); $budget['projection'] = round($daysInQuarter * $budget['dailyAverage']); } else { $budget['dailyAverage'] = $daysInQuarter == 0 ? 0 : round($budget['budgetFinalSpent'] / $daysInQuarter, 2); } $budget['costVariance'] = round((isset($budget['projection']) ? $budget['projection'] : $budget['budgetSpent']) - $budget['budget'], 2); $budget['costVariancePct'] = $budget['budget'] == 0 ? null : round(abs($budget['costVariance']) / $budget['budget'] * 100); $budget['monthlyAverage'] = round($budget['dailyAverage'] * 30); if ($prevEntity instanceof QuarterlyBudgetEntity) { $budget['prev'] = ['budget' => $prevEntity->budget, 'budgetFinalSpent' => $prevEntity->cumulativespend, 'spentondate' => $prevEntity->spentondate instanceof DateTime ? $prevEntity->spentondate->format('Y-m-d') : null, 'closed' => $prevPeriod->end->format('Y-m-d') < gmdate('Y-m-d'), 'costVariance' => $prevEntity->cumulativespend - $prevEntity->budget, 'costVariancePct' => $prevEntity->budget == 0 ? null : round(abs($prevEntity->cumulativespend - $prevEntity->budget) / $prevEntity->budget * 100)]; } else { $budget['prev'] = ['budget' => null, 'budgetFinalSpent' => 0, 'spentondate' => null, 'closed' => $prevPeriod->end->format('Y-m-d') < gmdate('Y-m-d'), 'budgetSpent' => 0, 'costVariance' => 0, 'costVariancePct' => 0]; } $budgets[] = $budget; } $result = ['budgets' => $budgets, 'ccId' => $ccId, 'projectId' => $projectId, 'quarter' => $quarters->getQuarterForDate(), 'year' => $year]; if (!empty($projectId)) { $result['shared'] = $subjectEntity->shared; } return $result; }
public function xSaveAction() { $this->request->defineParams(array('ccId' => ['type' => 'string'], 'projectId' => ['type' => 'string'], 'year' => ['type' => 'int'], 'quarters' => ['type' => 'json'], 'selectedQuarter' => ['type' => 'string'])); $year = $this->getParam('year'); $selectedQuarter = $this->getParam('selectedQuarter'); if ($selectedQuarter !== 'year' && ($selectedQuarter < 1 || $selectedQuarter > 4)) { throw new OutOfBoundsException(sprintf("Invalid selectedQuarter number.")); } $quarterReq = []; foreach ($this->getParam('quarters') as $q) { if (!isset($q['quarter'])) { throw new InvalidArgumentException(sprintf("Missing quarter property for quarters data set in the request.")); } if ($q['quarter'] < 1 || $q['quarter'] > 4) { throw new OutOfRangeException(sprintf("Quarter value should be between 1 and 4.")); } if (!isset($q['budget'])) { throw new InvalidArgumentException(sprintf("Missing budget property for quarters data set in the request.")); } $quarterReq[$q['quarter']] = $q; } if ($this->getParam('projectId')) { $subjectType = QuarterlyBudgetEntity::SUBJECT_TYPE_PROJECT; $subjectId = $this->getParam('projectId'); } else { if ($this->getParam('ccId')) { $subjectType = QuarterlyBudgetEntity::SUBJECT_TYPE_CC; $subjectId = $this->getParam('ccId'); } else { throw new InvalidArgumentException(sprintf('Either ccId or projectId must be provided with the request.')); } } if (!preg_match("/^[[:xdigit:]-]{36}\$/", $subjectId)) { throw new InvalidArgumentException(sprintf("Invalid UUID has been passed.")); } if (!preg_match('/^\\d{4}$/', $year)) { throw new InvalidArgumentException(sprintf("Invalid year has been passed.")); } //Fetches the previous state of the entities from database if ($subjectType == QuarterlyBudgetEntity::SUBJECT_TYPE_CC) { $collection = QuarterlyBudgetEntity::getCcBudget($year, $subjectId); } else { $collection = QuarterlyBudgetEntity::getProjectBudget($year, $subjectId); } $quarters = new Quarters(SettingEntity::getQuarters(true)); //Updates|creates entities for ($quarter = 1; $quarter <= 4; ++$quarter) { if (!isset($quarterReq[$quarter])) { continue; } $period = $quarters->getPeriodForQuarter($quarter, $year); //Checks if period has already been closed and forbids update if ($period->end->format('Y-m-d') < gmdate('Y-m-d')) { continue; } $entity = current($collection->filterByQuarter($quarter)); if ($entity instanceof QuarterlyBudgetEntity) { //We should update an entity $entity->budget = abs((double) $quarterReq[$quarter]['budget']); } else { //We should create a new one. $entity = new QuarterlyBudgetEntity($year, $quarter); $entity->subjectType = $subjectType; $entity->subjectId = $subjectId; $entity->budget = abs((double) $quarterReq[$quarter]['budget']); } $entity->save(); } if ($selectedQuarter == 'year') { $selectedPeriod = $quarters->getPeriodForYear($year); } else { $selectedPeriod = $quarters->getPeriodForQuarter($selectedQuarter, $year); } if ($subjectType == QuarterlyBudgetEntity::SUBJECT_TYPE_PROJECT) { $data = $this->getProjectData(ProjectEntity::findPk($subjectId), $selectedPeriod, true); $budgetInfo = $this->getBudgetInfo($year, $data['ccId'], $data['projectId']); } else { $data = $this->getCostCenterData(CostCentreEntity::findPk($subjectId), $selectedPeriod); $budgetInfo = $this->getBudgetInfo($year, $subjectId); } $this->response->data(['data' => $data, 'budgetInfo' => $budgetInfo]); $this->response->success('Budget changes have been saved'); }
/** * Gets quarterly budget of specified year for the CC * * @param int $year The year * @throws \RuntimeException * @return \Scalr\Model\Collections\ArrayCollection Returns collection of the QuarterlyBudgetEntity objects */ public function getQuarterlyBudget($year) { if ($this->projectId === null) { throw new \RuntimeException(sprintf("Identifier of the cost center has not been initialized yet for %s", get_class($this))); } return QuarterlyBudgetEntity::getCcBudget($year, $this->ccId); }
/** * Gets cost center moving average to date * * @param string $ccId The identifier of the Cost center * @param string $mode The mode * @param string $date The date within specified period 'Y-m-d H:00' * @param string $startDate The start date of the period 'Y-m-d' * @param string $endDate The end date of the period 'Y-m-d' * @return array Returns cost center moving average to date */ public function getCostCenterMovingAverageToDate($ccId, $mode, $date, $startDate, $endDate) { $iterator = ChartPeriodIterator::create($mode, $startDate, $endDate ?: null, 'UTC'); if (!preg_match('/^[\\d]{4}-[\\d]{2}-[\\d]{2} [\\d]{2}:00$/', $date)) { throw new InvalidArgumentException(sprintf("Invalid date:%s. 'YYYY-MM-DD HH:00' is expected.", strip_tags($date))); } if (!preg_match('/^[[:xdigit:]-]{36}$/', $ccId)) { throw new InvalidArgumentException(sprintf("Invalid identifier of the Cost center:%s. UUID is expected.", strip_tags($ccId))); } //Interval which is used in the database query for grouping $queryInterval = preg_replace('/^1 /', '', $iterator->getInterval()); $data = $this->getRollingAvg(['ccId' => $ccId], $queryInterval, $date); $data['budgetUseToDate'] = null; $data['budgetUseToDatePct'] = null; //Does not calculate budget use to date for those cases if ($mode != 'custom' && $mode != 'year') { $pointPosition = $iterator->searchPoint($date); if ($pointPosition !== false) { $chartPoint = $iterator->current(); //Gets the end date of the selected interval $end = clone $chartPoint->dt; if ($chartPoint->interval != '1 day') { $end->modify("+" . $chartPoint->interval . " -1 day"); } //Gets quarters config $quarters = new Quarters(SettingEntity::getQuarters()); //Gets start and end of the quarter for the end date of the current interval $period = $quarters->getPeriodForDate($end); $data['year'] = $period->year; $data['quarter'] = $period->quarter; $data['quarterStartDate'] = $period->start->format('Y-m-d'); $data['quarterEndDate'] = $period->end->format('Y-m-d'); //Gets budgeted cost $budget = current(QuarterlyBudgetEntity::getCcBudget($period->year, $ccId)->filterByQuarter($period->quarter)); //If budget has not been set we should not calculate anything if ($budget instanceof QuarterlyBudgetEntity && round($budget->budget) > 0) { $data['budget'] = round($budget->budget); //Calculates usage from the start date of the quarter to date $usage = $this->get(['ccId' => $ccId], $period->start, $end); $data['budgetUseToDate'] = $usage['cost']; $data['budgetUseToDatePct'] = $data['budget'] == 0 ? null : min(100, round($usage['cost'] / $data['budget'] * 100)); } } } return ['data' => $data]; }