diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php index 407d900af3..3d4f8a6e58 100644 --- a/app/Http/Controllers/Report/BudgetController.php +++ b/app/Http/Controllers/Report/BudgetController.php @@ -26,13 +26,13 @@ use Carbon\Carbon; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\Budget; -use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Http\Controllers\BasicDataSupport; +use FireflyIII\Support\Report\Budget\BudgetReportGenerator; use Illuminate\Contracts\View\Factory; use Illuminate\Support\Collection; use Illuminate\View\View; @@ -312,143 +312,16 @@ class BudgetController extends Controller */ public function general(Collection $accounts, Carbon $start, Carbon $end) { + /** @var BudgetReportGenerator $generator */ + $generator = app(BudgetReportGenerator::class); - $report = [ - 'budgets' => [], - 'sums' => [], - ]; - $budgets = $this->repository->getBudgets(); - $defaultCurrency = app('amount')->getDefaultCurrency(); - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $budgetId = (int) $budget->id; - $report['budgets'][$budgetId] = $report['budgets'][$budgetId] ?? [ - 'budget_id' => $budgetId, - 'budget_name' => $budget->name, - 'no_budget' => false, - 'budget_limits' => [], - ]; + $generator->setUser(auth()->user()); + $generator->setAccounts($accounts); + $generator->setStart($start); + $generator->setEnd($end); - // get all budget limits for budget in period: - $limits = $this->blRepository->getBudgetLimits($budget, $start, $end); - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $limitId = (int) $limit->id; - $currency = $limit->transactionCurrency ?? $defaultCurrency; - $currencyId = (int) $currency->id; - $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $accounts, new Collection([$budget])); - $spent = $expenses[$currencyId]['sum'] ?? '0'; - $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); - $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; - - $report['budgets'][$budgetId]['budget_limits'][$limitId] = $report['budgets'][$budgetId]['budget_limits'][$limitId] ?? [ - 'budget_limit_id' => $limitId, - 'start_date' => $limit->start_date, - 'end_date' => $limit->end_date, - 'budgeted' => $limit->amount, - 'budgeted_pct' => '0', - 'spent' => $spent, - 'spent_pct' => '0', - 'left' => $left, - 'overspent' => $overspent, - 'currency_id' => $currencyId, - 'currency_code' => $currency->code, - 'currency_name' => $currency->name, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - ]; - - // make sum information: - $report['sums'][$currencyId] - = $report['sums'][$currencyId] ?? [ - 'budgeted' => '0', - 'spent' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $currencyId, - 'currency_code' => $currency->code, - 'currency_name' => $currency->name, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - ]; - $report['sums'][$currencyId]['budgeted'] = bcadd($report['sums'][$currencyId]['budgeted'], $limit->amount); - $report['sums'][$currencyId]['spent'] = bcadd($report['sums'][$currencyId]['spent'], $spent); - $report['sums'][$currencyId]['left'] = bcadd($report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent)); - $report['sums'][$currencyId]['overspent'] = bcadd($report['sums'][$currencyId]['overspent'], $overspent); - } - } - - - // add no budget info. - $report['budgets'][0] = $report['budgets'][0] ?? [ - 'budget_id' => null, - 'budget_name' => null, - 'no_budget' => true, - 'budget_limits' => [], - ]; - $noBudget = $this->nbRepository->sumExpenses($start, $end); - foreach ($noBudget as $noBudgetEntry) { - - // currency information: - $nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $defaultCurrency->id); - $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $defaultCurrency->code; - $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $defaultCurrency->name; - $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $defaultCurrency->symbol; - $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $defaultCurrency->decimal_places; - - $report['budgets'][0]['budget_limits'][] = [ - 'budget_limit_id' => null, - 'start_date' => $start, - 'end_date' => $end, - 'budgeted' => '0', - 'budgeted_pct' => '0', - 'spent' => $noBudgetEntry['sum'], - 'spent_pct' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $nbCurrencyId, - 'currency_code' => $nbCurrencyCode, - 'currency_name' => $nbCurrencyName, - 'currency_symbol' => $nbCurrencySymbol, - 'currency_decimal_places' => $nbCurrencyDp, - ]; - $report['sums'][$nbCurrencyId]['spent'] = bcadd($report['sums'][$nbCurrencyId]['spent'] ?? '0', $noBudgetEntry['sum']); - // append currency info because it may be missing: - $report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId; - $report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode; - $report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName; - $report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol; - $report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp; - - // append other sums because they might be missing: - $report['sums'][$nbCurrencyId]['overspent'] = $report['sums'][$nbCurrencyId]['overspent'] ?? '0'; - $report['sums'][$nbCurrencyId]['left'] = $report['sums'][$nbCurrencyId]['left'] ?? '0'; - $report['sums'][$nbCurrencyId]['budgeted'] = $report['sums'][$nbCurrencyId]['budgeted'] ?? '0'; - } - // make percentages based on total amount. - foreach ($report['budgets'] as $budgetId => $data) { - foreach ($data['budget_limits'] as $limitId => $entry) { - $budgetId = (int) $budgetId; - $limitId = (int) $limitId; - $currencyId = (int) $entry['currency_id']; - $spent = $entry['spent']; - $totalSpent = $report['sums'][$currencyId]['spent'] ?? '0'; - $spentPct = '0'; - $budgeted = $entry['budgeted']; - $totalBudgeted = $report['sums'][$currencyId]['budgeted'] ?? '0'; - $budgetedPct = '0'; - - if (0 !== bccomp($spent, '0') && 0 !== bccomp($totalSpent, '0')) { - $spentPct = round(bcmul(bcdiv($spent, $totalSpent), '100')); - } - if (0 !== bccomp($budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) { - $budgetedPct = round(bcmul(bcdiv($budgeted, $totalBudgeted), '100')); - } - $report['sums'][$currencyId]['budgeted'] = $report['sums'][$currencyId]['budgeted'] ?? '0'; - $report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; - $report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; - } - } + $generator->general(); + $report = $generator->getReport(); return view('reports.partials.budgets', compact('report'))->render(); } diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php new file mode 100644 index 0000000000..97e8497994 --- /dev/null +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -0,0 +1,298 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Report\Budget; + +use Carbon\Carbon; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; +use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * Class BudgetReportGenerator + * + * This class is basically a very long for-each loop disguised as a class. It's readable but not really OOP. + */ +class BudgetReportGenerator +{ + private User $user; + private Collection $accounts; + private Carbon $start; + private Carbon $end; + private BudgetRepositoryInterface $repository; + private BudgetLimitRepositoryInterface $blRepository; + private TransactionCurrency $currency; + private array $report; + private OperationsRepositoryInterface $opsRepository; + private NoBudgetRepositoryInterface $nbRepository; + + /** + * BudgetReportGenerator constructor. + */ + public function __construct() + { + $this->repository = app(BudgetRepositoryInterface::class); + $this->blRepository = app(BudgetLimitRepositoryInterface::class); + $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->nbRepository = app(NoBudgetRepositoryInterface::class); + $this->report = []; + } + + /** + * Generates the data necessary to create the card that displays + * the budget overview in the general report. + */ + public function general(): void + { + $this->report = [ + 'budgets' => [], + 'sums' => [], + ]; + + $this->generalBudgetReport(); + $this->noBudgetReport(); + $this->percentageReport(); + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->repository->setUser($user); + $this->blRepository->setUser($user); + $this->opsRepository->setUser($user); + $this->nbRepository->setUser($user); + $this->currency = app('amount')->getDefaultCurrencyByUser($this->user); + } + + /** + * @param Collection $accounts + */ + public function setAccounts(Collection $accounts): void + { + $this->accounts = $accounts; + } + + /** + * @param Carbon $start + */ + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + /** + * @param Carbon $end + */ + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + /** + * @return array + */ + public function getReport(): array + { + return $this->report; + } + + /** + * Start the report by processing every budget. + */ + private function generalBudgetReport(): void + { + $budgets = $this->repository->getBudgets(); + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $this->processBudget($budget); + } + } + + /** + * Process expenses etc. for a single budget. + * + * @param Budget $budget + */ + private function processBudget(Budget $budget): void + { + $budgetId = (int) $budget->id; + $this->report['budgets'][$budgetId] = $this->report['budgets'][$budgetId] ?? [ + 'budget_id' => $budgetId, + 'budget_name' => $budget->name, + 'no_budget' => false, + 'budget_limits' => [], + ]; + + // get all budget limits for budget in period: + $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $this->processLimit($budget, $limit); + } + } + + /** + * Process a single budget limit. + * @param Budget $budget + * @param BudgetLimit $limit + */ + private function processLimit(Budget $budget, BudgetLimit $limit): void + { + $budgetId = (int) $budget->id; + $limitId = (int) $limit->id; + $currency = $limit->transactionCurrency ?? $this->currency; + $currencyId = (int) $currency->id; + $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection([$budget])); + $spent = $expenses[$currencyId]['sum'] ?? '0'; + $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); + $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; + + $this->report['budgets'][$budgetId]['budget_limits'][$limitId] = $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ?? [ + 'budget_limit_id' => $limitId, + 'start_date' => $limit->start_date, + 'end_date' => $limit->end_date, + 'budgeted' => $limit->amount, + 'budgeted_pct' => '0', + 'spent' => $spent, + 'spent_pct' => '0', + 'left' => $left, + 'overspent' => $overspent, + 'currency_id' => $currencyId, + 'currency_code' => $currency->code, + 'currency_name' => $currency->name, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + ]; + + // make sum information: + $this->report['sums'][$currencyId] + = $this->report['sums'][$currencyId] ?? [ + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $currencyId, + 'currency_code' => $currency->code, + 'currency_name' => $currency->name, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + ]; + $this->report['sums'][$currencyId]['budgeted'] = bcadd($this->report['sums'][$currencyId]['budgeted'], $limit->amount); + $this->report['sums'][$currencyId]['spent'] = bcadd($this->report['sums'][$currencyId]['spent'], $spent); + $this->report['sums'][$currencyId]['left'] = bcadd($this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent)); + $this->report['sums'][$currencyId]['overspent'] = bcadd($this->report['sums'][$currencyId]['overspent'], $overspent); + } + + /** + * + */ + private function noBudgetReport(): void + { + // add no budget info. + $this->report['budgets'][0] = [ + 'budget_id' => null, + 'budget_name' => null, + 'no_budget' => true, + 'budget_limits' => [], + ]; + + $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end); + foreach ($noBudget as $noBudgetEntry) { + + // currency information: + $nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $this->currency->id); + $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code; + $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name; + $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol; + $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places; + + $this->report['budgets'][0]['budget_limits'][] = [ + 'budget_limit_id' => null, + 'start_date' => $this->start, + 'end_date' => $this->end, + 'budgeted' => '0', + 'budgeted_pct' => '0', + 'spent' => $noBudgetEntry['sum'], + 'spent_pct' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $nbCurrencyId, + 'currency_code' => $nbCurrencyCode, + 'currency_name' => $nbCurrencyName, + 'currency_symbol' => $nbCurrencySymbol, + 'currency_decimal_places' => $nbCurrencyDp, + ]; + $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', $noBudgetEntry['sum']); + // append currency info because it may be missing: + $this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId; + $this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode; + $this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName; + $this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol; + $this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp; + + // append other sums because they might be missing: + $this->report['sums'][$nbCurrencyId]['overspent'] = $this->report['sums'][$nbCurrencyId]['overspent'] ?? '0'; + $this->report['sums'][$nbCurrencyId]['left'] = $this->report['sums'][$nbCurrencyId]['left'] ?? '0'; + $this->report['sums'][$nbCurrencyId]['budgeted'] = $this->report['sums'][$nbCurrencyId]['budgeted'] ?? '0'; + } + } + + /** + * + */ + private function percentageReport(): void + { + // make percentages based on total amount. + foreach ($this->report['budgets'] as $budgetId => $data) { + foreach ($data['budget_limits'] as $limitId => $entry) { + $budgetId = (int) $budgetId; + $limitId = (int) $limitId; + $currencyId = (int) $entry['currency_id']; + $spent = $entry['spent']; + $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0'; + $spentPct = '0'; + $budgeted = $entry['budgeted']; + $totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0'; + $budgetedPct = '0'; + + if (0 !== bccomp($spent, '0') && 0 !== bccomp($totalSpent, '0')) { + $spentPct = round(bcmul(bcdiv($spent, $totalSpent), '100')); + } + if (0 !== bccomp($budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) { + $budgetedPct = round(bcmul(bcdiv($budgeted, $totalBudgeted), '100')); + } + $this->report['sums'][$currencyId]['budgeted'] = $this->report['sums'][$currencyId]['budgeted'] ?? '0'; + $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; + $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; + } + } + } +}