generator = app(GeneratorInterface::class); } /** * checked * * @param BudgetRepositoryInterface $repository * @param Budget $budget * * @return \Symfony\Component\HttpFoundation\Response */ public function budget(BudgetRepositoryInterface $repository, Budget $budget) { $first = $repository->firstUseDate($budget); $range = Preferences::get('viewRange', '1M')->data; $last = session('end', new Carbon); $cache = new CacheProperties(); $cache->addProperty($first); $cache->addProperty($last); $cache->addProperty('chart.budget.budget'); if ($cache->has()) { return Response::json($cache->get()); } $final = clone $last; $final->addYears(2); $budgetCollection = new Collection([$budget]); $last = Navigation::endOfX($last, $range, $final); // not to overshoot. $entries = []; while ($first < $last) { // periodspecific dates: $currentStart = Navigation::startOfPeriod($first, $range); $currentEnd = Navigation::endOfPeriod($first, $range); // sub another day because reasons. $currentEnd->subDay(); $spent = $repository->spentInPeriod($budgetCollection, new Collection, $currentStart, $currentEnd); $format = Navigation::periodShow($first, $range); $entries[$format] = bcmul($spent, '-1'); $first = Navigation::addPeriod($first, $range, 0); } $data = $this->generator->singleSet(strval(trans('firefly.spent')), $entries); $cache->store($data); return Response::json($data); } /** * Shows the amount left in a specific budget limit. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. * @param BudgetRepositoryInterface $repository * @param Budget $budget * @param LimitRepetition $repetition * * @return \Symfony\Component\HttpFoundation\Response */ public function budgetLimit(BudgetRepositoryInterface $repository, Budget $budget, LimitRepetition $repetition) { $start = clone $repetition->startdate; $end = $repetition->enddate; $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('chart.budget.budget.limit'); $cache->addProperty($repetition->id); if ($cache->has()) { return Response::json($cache->get()); } $entries = []; $amount = $repetition->amount; $budgetCollection = new Collection([$budget]); while ($start <= $end) { $spent = $repository->spentInPeriod($budgetCollection, new Collection, $start, $start); $amount = bcadd($amount, $spent); $format = $start->formatLocalized(strval(trans('config.month_and_day'))); $entries[$format] = $amount; $start->addDay(); } $data = $this->generator->singleSet(strval(trans('firefly.left')), $entries); $cache->store($data); return Response::json($data); } /** * Shows a budget list with spent/left/overspent. * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // 46 lines, I'm fine with this. * * @param BudgetRepositoryInterface $repository * * @return \Symfony\Component\HttpFoundation\Response */ public function frontpage(BudgetRepositoryInterface $repository) { $start = session('start', Carbon::now()->startOfMonth()); $end = session('end', Carbon::now()->endOfMonth()); // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('chart.budget.frontpage'); if ($cache->has()) { return Response::json($cache->get()); } $budgets = $repository->getActiveBudgets(); $repetitions = $repository->getAllBudgetLimitRepetitions($start, $end); $chartData = [ ['label' => strval(trans('firefly.spent_in_budget')), 'entries' => [], 'type' => 'bar',], ['label' => strval(trans('firefly.left_to_spend')), 'entries' => [], 'type' => 'bar',], ['label' => strval(trans('firefly.overspent')), 'entries' => [], 'type' => 'bar',], ]; /** @var Budget $budget */ foreach ($budgets as $budget) { // get relevant repetitions: $filtered = $this->filterRepetitions($repetitions, $budget, $start, $end); $expenses = $this->getExpensesForBudget($filtered, $budget, $start, $end); foreach ($expenses as $name => $row) { $chartData[0]['entries'][$name] = $row['spent']; $chartData[1]['entries'][$name] = $row['left']; $chartData[2]['entries'][$name] = $row['overspent']; } } // for no budget: $spent = $this->spentInPeriodWithout($start, $end); $name = strval(trans('firefly.no_budget')); if (bccomp($spent, '0') !== 0) { $chartData[0]['entries'][$name] = bcmul($spent, '-1'); $chartData[1]['entries'][$name] = '0'; $chartData[2]['entries'][$name] = '0'; } $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); } /** * @param BudgetRepositoryInterface $repository * @param Budget $budget * @param Carbon $start * @param Carbon $end * @param Collection $accounts * * @return \Illuminate\Http\JsonResponse */ public function period(BudgetRepositoryInterface $repository, Budget $budget, Collection $accounts, Carbon $start, Carbon $end) { // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($accounts); $cache->addProperty($budget->id); $cache->addProperty('chart.budget.period'); if ($cache->has()) { return Response::json($cache->get()); } // get the expenses $budgeted = []; $periods = Navigation::listOfPeriods($start, $end); $entries = $repository->getBudgetPeriodReport(new Collection([$budget]), $accounts, $start, $end); $key = Navigation::preferredCarbonFormat($start, $end); $range = Navigation::preferredRangeFormat($start, $end); // get the budget limits (if any) $repetitions = $repository->getAllBudgetLimitRepetitions($start, $end); $current = clone $start; while ($current < $end) { $currentStart = Navigation::startOfPeriod($current, $range); $currentEnd = Navigation::endOfPeriod($current, $range); $reps = $repetitions->filter( function (LimitRepetition $repetition) use ($budget, $currentStart, $currentEnd) { if ($repetition->budget_id === $budget->id && $repetition->startdate >= $currentStart && $repetition->enddate <= $currentEnd) { return true; } return false; } ); $index = $currentStart->format($key); $budgeted[$index] = $reps->sum('amount'); $currentEnd->addDay(); $current = clone $currentEnd; } // join them into one set of data: $chartData = [ [ 'label' => strval(trans('firefly.spent')), 'type' => 'bar', 'entries' => [], ], [ 'label' => strval(trans('firefly.budgeted')), 'type' => 'bar', 'entries' => [], ], ]; foreach (array_keys($periods) as $period) { $label = $periods[$period]; $spent = isset($entries[$budget->id]['entries'][$period]) ? $entries[$budget->id]['entries'][$period] : '0'; $limit = isset($budgeted[$period]) ? $budgeted[$period] : 0; $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 2); $chartData[1]['entries'][$label] = $limit; } $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); } /** * @param BudgetRepositoryInterface $repository * @param Collection $accounts * @param Carbon $start * @param Carbon $end * * @return \Illuminate\Http\JsonResponse */ public function periodNoBudget(BudgetRepositoryInterface $repository, Collection $accounts, Carbon $start, Carbon $end) { // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($accounts); $cache->addProperty('chart.budget.no-budget'); if ($cache->has()) { return Response::json($cache->get()); } // the expenses: $periods = Navigation::listOfPeriods($start, $end); $entries = $repository->getNoBudgetPeriodReport($accounts, $start, $end); $chartData = []; // join them: foreach (array_keys($periods) as $period) { $label = $periods[$period]; $spent = isset($entries['entries'][$period]) ? $entries['entries'][$period] : '0'; $chartData[$label] = bcmul($spent, '-1'); } $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); return Response::json($data); } /** * @param Collection $repetitions * @param Budget $budget * @param Carbon $start * @param Carbon $end * * @return Collection */ private function filterRepetitions(Collection $repetitions, Budget $budget, Carbon $start, Carbon $end): Collection { return $repetitions->filter( function (LimitRepetition $repetition) use ($budget, $start, $end) { if ($repetition->startdate < $end && $repetition->enddate > $start && $repetition->budget_id === $budget->id) { return true; } return false; } ); } /** * * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but ok. * * @param Collection $repetitions * @param Budget $budget * @param Carbon $start * @param Carbon $end * * @return array */ private function getExpensesForBudget(Collection $repetitions, Budget $budget, Carbon $start, Carbon $end): array { /** @var BudgetRepositoryInterface $repository */ $repository = app(BudgetRepositoryInterface::class); $return = []; if ($repetitions->count() === 0) { $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); if (bccomp($spent, '0') !== 0) { $return[$budget->name]['spent'] = $spent; $return[$budget->name]['left'] = 0; $return[$budget->name]['overspent'] = 0; } return $return; } $rows = $this->spentInPeriodMulti($repository, $budget, $repetitions); foreach ($rows as $name => $row) { if (bccomp($row['spent'], '0') !== 0 || bccomp($row['left'], '0') !== 0) { $return[$name]['spent'] = bcmul($row['spent'], '-1'); $return[$name]['left'] = $row['left']; $return[$name]['overspent'] = bcmul($row['overspent'], '-1'); } } unset($rows, $row); return $return; } /** * Returns an array with the following values: * 0 => * 'name' => name of budget + repetition * 'left' => left in budget repetition (always zero) * 'overspent' => spent more than budget repetition? (always zero) * 'spent' => actually spent in period for budget * 1 => (etc) * * @param BudgetRepositoryInterface $repository * @param Budget $budget * @param Collection $repetitions * * @return array */ private function spentInPeriodMulti(BudgetRepositoryInterface $repository, Budget $budget, Collection $repetitions): array { $return = []; $format = strval(trans('config.month_and_day')); $name = $budget->name; /** @var LimitRepetition $repetition */ foreach ($repetitions as $repetition) { $expenses = $repository->spentInPeriod(new Collection([$budget]), new Collection, $repetition->startdate, $repetition->enddate); if ($repetitions->count() > 1) { $name = $budget->name . ' ' . trans( 'firefly.between_dates', ['start' => $repetition->startdate->formatLocalized($format), 'end' => $repetition->enddate->formatLocalized($format)] ); } $amount = $repetition->amount; $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); $spent = $expenses; $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; $return[$name] = [ 'left' => $left, 'overspent' => $overspent, 'spent' => $spent, ]; } return $return; } /** * Returns an array with the following values: * 'name' => "no budget" in local language * 'repetition_left' => left in budget repetition (always zero) * 'repetition_overspent' => spent more than budget repetition? (always zero) * 'spent' => actually spent in period for budget * * @param Carbon $start * @param Carbon $end * * @return string */ private function spentInPeriodWithout(Carbon $start, Carbon $end): string { // collector /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class, [auth()->user()]); $types = [TransactionType::WITHDRAWAL]; $collector->setAllAssetAccounts()->setTypes($types)->setRange($start, $end)->withoutBudget(); $journals = $collector->getJournals(); $sum = '0'; /** @var Transaction $entry */ foreach ($journals as $entry) { $sum = bcadd($entry->transaction_amount, $sum); } return $sum; } }