diff --git a/app/Helpers/Report/BudgetReportHelper.php b/app/Helpers/Report/BudgetReportHelper.php index 238df2b504..d41de90649 100644 --- a/app/Helpers/Report/BudgetReportHelper.php +++ b/app/Helpers/Report/BudgetReportHelper.php @@ -57,8 +57,8 @@ class BudgetReportHelper implements BudgetReportHelperInterface /** * Get the full budget report. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * TODO one big method is very complex. + * * @param Carbon $start * @param Carbon $end * @param Collection $accounts @@ -68,104 +68,142 @@ class BudgetReportHelper implements BudgetReportHelperInterface public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array { $set = $this->repository->getBudgets(); - $array = []; + $array = [ + 'budgets' => [], + 'sums' => [], + ]; /** @var Budget $budget */ foreach ($set as $budget) { + $entry = [ + 'budget_id' => $budget->id, + 'budget_name' => $budget->name, + 'no_budget' => false, + 'rows' => [], + ]; + // get multi currency expenses first: $budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end); - if (0 === $budgetLimits->count()) { // no budget limit(s) for this budget - $spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); // spent for budget in time range - if (bccomp($spent, '0') === -1) { - $line = [ - 'type' => 'budget', - 'id' => $budget->id, - 'name' => $budget->name, - 'budgeted' => '0', - 'spent' => $spent, - 'left' => '0', - 'overspent' => '0', + $expenses = $this->repository->spentInPeriodMc(new Collection([$budget]), $accounts, $start, $end); + if (0 === count($expenses)) { + // list the budget limits, basic amounts. + /** @var BudgetLimit $limit */ + foreach ($budgetLimits as $limit) { + $row = [ + 'limit_id' => $limit->id, + 'start_date' => $limit->start_date, + 'end_date' => $limit->end_date, + 'budgeted' => $limit->amount, + 'spent' => '0', + 'left' => $limit->amount, + 'overspent' => null, + 'currency_id' => $limit->transactionCurrency->id, + 'currency_code' => $limit->transactionCurrency->code, + 'currency_name' => $limit->transactionCurrency->name, + 'currency_symbol' => $limit->transactionCurrency->symbol, + 'currency_decimal_places' => $limit->transactionCurrency->decimal_places, ]; - $array[] = $line; + + $entry['rows'][] = $row; } - continue; } - /** @var BudgetLimit $budgetLimit */ - foreach ($budgetLimits as $budgetLimit) { // one or more repetitions for budget - $data = $this->calculateExpenses($budget, $budgetLimit, $accounts); - $line = [ - 'type' => 'budget-line', - 'start' => $budgetLimit->start_date, - 'end' => $budgetLimit->end_date, - 'limit' => $budgetLimit->id, - 'id' => $budget->id, - 'name' => $budget->name, - - 'budgeted' => (string)$budgetLimit->amount, - 'spent' => $data['expenses'], - 'left' => $data['left'], - 'overspent' => $data['overspent'], + foreach ($expenses as $expense) { + $limit = $this->budgetLimitInCurrency($expense['currency_id'], $budgetLimits); + $row = [ + 'limit_id' => null, + 'start_date' => null, + 'end_date' => null, + 'budgeted' => null, + 'spent' => $expense['amount'], + 'left' => null, + 'overspent' => null, + 'currency_id' => $expense['currency_id'], + 'currency_code' => $expense['currency_name'], + 'currency_name' => $expense['currency_name'], + 'currency_symbol' => $expense['currency_symbol'], + 'currency_decimal_places' => $expense['currency_decimal_places'], ]; - $array[] = $line; + if (null !== $limit) { + // yes + $row['start_date'] = $limit->start_date; + $row['end_date'] = $limit->end_date; + $row['budgeted'] = $limit->amount; + $row['limit_id'] = $limit->id; + + // less than zero? Set to 0.0 + $row['left'] = -1 === bccomp(bcadd($limit->amount, $row['spent']), '0') ? '0' : bcadd($limit->amount, $row['spent']); + + // spent > budgeted? then sum, otherwise other sum + $row['overspent'] = 1 === bccomp($row['spent'], $row['budgeted']) ? bcadd($row['spent'], $row['budgeted']) : '0'; + } + $entry['rows'][] = $row; } + $array['budgets'][] = $entry; } - $noBudget = $this->repository->spentInPeriodWoBudget($accounts, $start, $end); // stuff outside of budgets - $line = [ - 'type' => 'no-budget', - 'budgeted' => '0', - 'spent' => $noBudget, - 'left' => '0', - 'overspent' => '0', + $noBudget = $this->repository->spentInPeriodWoBudgetMc($accounts, $start, $end); + $noBudgetEntry = [ + 'budget_id' => null, + 'budget_name' => null, + 'no_budget' => true, + 'rows' => [], ]; - $array[] = $line; + foreach ($noBudget as $row) { + $noBudgetEntry['rows'][] = [ + 'limit_id' => null, + 'start_date' => null, + 'end_date' => null, + 'budgeted' => null, + 'spent' => $row['amount'], + 'left' => null, + 'overspent' => null, + 'currency_id' => $row['currency_id'], + 'currency_code' => $row['currency_code'], + 'currency_name' => $row['currency_name'], + 'currency_symbol' => $row['currency_symbol'], + 'currency_decimal_places' => $row['currency_decimal_places'], + ]; + } + $array['budgets'][] = $noBudgetEntry; - return $array; - } - - /** - * Get all budgets and the expenses in these budgets. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Collection - */ - public function getBudgetsWithExpenses(Carbon $start, Carbon $end, Collection $accounts): Collection - { - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $budgets = $repository->getActiveBudgets(); - - $set = new Collection; - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $total = $repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); - if (bccomp($total, '0') === -1) { - $set->push($budget); + // fill sums: + /** @var array $budget */ + foreach ($array['budgets'] as $budget) { + /** @var array $row */ + foreach ($budget['rows'] as $row) { + $currencyId = $row['currency_id']; + $array['sums'][$currencyId] = $array['sums'][$currencyId] ?? [ + 'currency_id' => $row['currency_id'], + 'currency_code' => $row['currency_code'], + 'currency_name' => $row['currency_name'], + 'currency_symbol' => $row['currency_symbol'], + 'currency_decimal_places' => $row['currency_decimal_places'], + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + ]; + $array['sums'][$currencyId]['budgeted'] = bcadd($array['sums'][$currencyId]['budgeted'], $row['budgeted'] ?? '0'); + $array['sums'][$currencyId]['spent'] = bcadd($array['sums'][$currencyId]['spent'], $row['spent'] ?? '0'); + $array['sums'][$currencyId]['left'] = bcadd($array['sums'][$currencyId]['left'], $row['left'] ?? '0'); + $array['sums'][$currencyId]['overspent'] = bcadd($array['sums'][$currencyId]['overspent'], $row['overspent'] ?? '0'); } } - - return $set; + return $array; } /** - * Calculate the expenses for a budget. + * Returns from the collection the budget limit with the indicated currency ID * - * @param Budget $budget - * @param BudgetLimit $budgetLimit - * @param Collection $accounts + * @param int $currencyId + * @param Collection $budgetLimits * - * @return array + * @return BudgetLimit|null */ - private function calculateExpenses(Budget $budget, BudgetLimit $budgetLimit, Collection $accounts): array + private function budgetLimitInCurrency(int $currencyId, Collection $budgetLimits): ?BudgetLimit { - $array = []; - $expenses = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $budgetLimit->start_date, $budgetLimit->end_date); - $array['left'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? bcadd($budgetLimit->amount, $expenses) : '0'; - $array['spent'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? $expenses : '0'; - $array['overspent'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? '0' : bcadd($expenses, $budgetLimit->amount); - $array['expenses'] = $expenses; - - return $array; + return $budgetLimits->first( + static function (BudgetLimit $limit) use ($currencyId) { + return $limit->transaction_currency_id === $currencyId; + } + ); } } diff --git a/app/Helpers/Report/BudgetReportHelperInterface.php b/app/Helpers/Report/BudgetReportHelperInterface.php index b470265709..a8adae656d 100644 --- a/app/Helpers/Report/BudgetReportHelperInterface.php +++ b/app/Helpers/Report/BudgetReportHelperInterface.php @@ -40,15 +40,4 @@ interface BudgetReportHelperInterface * @return array */ public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array; - - /** - * Get budgets and the expenses in each budget. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Collection - */ - public function getBudgetsWithExpenses(Carbon $start, Carbon $end, Collection $accounts): Collection; } diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php index 4cc0ac2cda..fe01dd7ac7 100644 --- a/app/Http/Controllers/Report/BudgetController.php +++ b/app/Http/Controllers/Report/BudgetController.php @@ -57,17 +57,17 @@ class BudgetController extends Controller $cache->addProperty('budget-report'); $cache->addProperty($accounts->pluck('id')->toArray()); if ($cache->has()) { - return $cache->get(); // @codeCoverageIgnore + //return $cache->get(); // @codeCoverageIgnore } $helper = app(BudgetReportHelperInterface::class); $budgets = $helper->getBudgetReport($start, $end, $accounts); - try { + //try { $result = view('reports.partials.budgets', compact('budgets'))->render(); // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::debug(sprintf('Could not render reports.partials.budgets: %s', $e->getMessage())); - $result = 'Could not render view.'; - } +// } catch (Throwable $e) { +// Log::debug(sprintf('Could not render reports.partials.budgets: %s', $e->getMessage())); +// $result = 'Could not render view.'; +// } // @codeCoverageIgnoreEnd $cache->store($result); diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index a371dcd1f6..6db33b7a89 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -179,10 +179,10 @@ class BudgetRepository implements BudgetRepositoryInterface public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection { if (null === $end && null === $start) { - return $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']); + return $budget->budgetlimits()->with(['transactionCurrency'])->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']); } if (null === $end xor null === $start) { - $query = $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC'); + $query = $budget->budgetlimits()->with(['transactionCurrency'])->orderBy('budget_limits.start_date', 'DESC'); // one of the two is null if (null !== $end) { // end date must be before $end. @@ -704,6 +704,7 @@ class BudgetRepository implements BudgetRepositoryInterface 'id' => $transaction['currency_id'], 'decimal_places' => $transaction['currency_decimal_places'], 'code' => $transaction['currency_code'], + 'name' => $transaction['currency_name'], 'symbol' => $transaction['currency_symbol'], ]; } @@ -720,9 +721,10 @@ class BudgetRepository implements BudgetRepositoryInterface $return[] = [ 'currency_id' => $currency['id'], 'currency_code' => $code, + 'currency_name' => $currency['name'], 'currency_symbol' => $currency['symbol'], 'currency_decimal_places' => $currency['decimal_places'], - 'amount' => round($spent, $currency['decimal_places']), + 'amount' => $spent, ]; } @@ -792,9 +794,10 @@ class BudgetRepository implements BudgetRepositoryInterface $return[] = [ 'currency_id' => $currency['id'], 'currency_code' => $code, + 'currency_name' => $currency['name'], 'currency_symbol' => $currency['symbol'], 'currency_decimal_places' => $currency['decimal_places'], - 'amount' => round($spent, $currency['decimal_places']), + 'amount' => $spent, ]; } diff --git a/resources/views/v1/reports/partials/budgets.twig b/resources/views/v1/reports/partials/budgets.twig index d9f5cb991a..9378ab4dda 100644 --- a/resources/views/v1/reports/partials/budgets.twig +++ b/resources/views/v1/reports/partials/budgets.twig @@ -11,19 +11,74 @@ - {% set sum_budgeted = 0 %} - {% set sum_spent = 0 %} - {% set sum_left = 0 %} - {% set sum_overspent = 0 %} - {% for line in budgets %} + {% for budget in budgets.budgets %} + {% for row in budget.rows %} + + + {% if budget.no_budget %} + + {{ 'no_budget'|_ }} ({{ row.currency_name }}) + + {% else %} + + {{ budget.budget_name }} + + {% endif %} + + + {% if null != row.limit_id %} + + {{ row.start_date.formatLocalized(monthAndDayFormat) }} + — + {{ row.end_date.formatLocalized(monthAndDayFormat) }} + + {% endif %} + + + + + {% if null != row.budgeted %} + {{ formatAmountBySymbol(row.budgeted, row.currency_symbol, row.currency_decimal_places) }} + {% endif %} + + + + + {{ formatAmountBySymbol(row.spent, row.currency_symbol, row.currency_decimal_places) }} + + + + + {% if row.spent != 0 %} + + {% endif %} + + + + + {% if null != row.left %} + {{ formatAmountBySymbol(row.left, row.currency_symbol, row.currency_decimal_places) }} + {% endif %} + + + + + {% if null != row.overspent %} + {{ formatAmountBySymbol(row.overspent, row.currency_symbol, row.currency_decimal_places) }} + {% endif %} + + + {% endfor %} + {# {% set sum_budgeted = sum_budgeted + line.budgeted %} {% set sum_spent = sum_spent + line.spent %} {% set sum_left = sum_left + line.left %} {% set sum_overspent = sum_overspent + line.overspent %} - {# Budget name, always visible #} + {% if line.type == 'no-budget' %} {{ 'no_budget'|_ }} @@ -33,7 +88,7 @@ {{ line.name }} {% endif %} - {# date, hidden on mobile #} + {% if line.type == 'budget-line' %} @@ -48,17 +103,17 @@ {% endif %} - {# budgeted, hidden on mobile #} + {{ line.budgeted|formatAmount }} - {# spent, visible on mobile #} + {{ line.spent|formatAmount }} - {# info button, not visible on mobile #} + {% if line.spent != 0 %} - {# left, hidden on mobile #} + {{ line.left|formatAmount }} - {# overspent, visible. #} + {{ line.overspent|formatAmount }} + #} {% endfor %} + {% for sum in budgets.sums %} + + {{ 'sum'|_ }} ({{ sum.currency_name }}) + {{ formatAmountBySymbol(sum.budgeted, sum.currency_symbol, sum.decimal_places) }} + {{ formatAmountBySymbol(sum.spent, sum.currency_symbol, sum.decimal_places) }} +   + {{ formatAmountBySymbol(sum.left, sum.currency_symbol, sum.decimal_places) }} + {{ formatAmountBySymbol(sum.overspent, sum.currency_symbol, sum.decimal_places) }} + + {% endfor %} + {# - {# title, visible #} + {{ 'sum'|_ }} - {# date, hidden #} + +   - {# sum of budgeted, hidden #} + {{ sum_budgeted|formatAmount }} - {# spent, visible #} + {{ sum_spent|formatAmount }} - {# info button, hidden #} +   - {# left, hidden #} + {{ sum_left|formatAmount }} {{ sum_overspent|formatAmount }} + #}