diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php
index 1242272dfb..3ada70161c 100644
--- a/app/Generator/Chart/Basic/ChartJsGenerator.php
+++ b/app/Generator/Chart/Basic/ChartJsGenerator.php
@@ -185,6 +185,7 @@ class ChartJsGenerator implements GeneratorInterface
// make larger than 0
$chartData['datasets'][0]['data'][] = (float)app('steam')->positive((string)$value);
$chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index);
+
$chartData['labels'][] = $key;
++$index;
}
diff --git a/app/Generator/Report/Budget/MonthReportGenerator.php b/app/Generator/Report/Budget/MonthReportGenerator.php
index d1adeb82b0..ab9832c1e2 100644
--- a/app/Generator/Report/Budget/MonthReportGenerator.php
+++ b/app/Generator/Report/Budget/MonthReportGenerator.php
@@ -79,7 +79,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
->render();
} catch (Throwable $e) {
Log::error(sprintf('Cannot render reports.account.report: %s', $e->getMessage()));
- $result = 'Could not render report view.';
+ $result = sprintf('Could not render report view: %s', $e->getMessage());
}
return $result;
diff --git a/app/Http/Controllers/Chart/BudgetReportController.php b/app/Http/Controllers/Chart/BudgetReportController.php
index 1f3a2537f4..7f22f41a12 100644
--- a/app/Http/Controllers/Chart/BudgetReportController.php
+++ b/app/Http/Controllers/Chart/BudgetReportController.php
@@ -1,4 +1,4 @@
-middleware(
function ($request, $next) {
- $this->generator = app(GeneratorInterface::class);
- $this->budgetRepository = app(BudgetRepositoryInterface::class);
- $this->blRepository = app(BudgetLimitRepositoryInterface::class);
+ $this->generator = app(GeneratorInterface::class);
+ $this->opsRepository = app(OperationsRepositoryInterface::class);
return $next($request);
}
);
}
-
/**
- * Chart that groups expenses by the account.
- *
- * TODO this chart is not multi-currency aware.
+ * Chart that groups the expenses by budget.
*
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
- * @param string $others
*
* @return JsonResponse
*/
- public function accountExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others): JsonResponse
+ public function budgetExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): JsonResponse
{
- /** @var MetaPieChartInterface $helper */
- $helper = app(MetaPieChartInterface::class);
- $helper->setAccounts($accounts);
- $helper->setBudgets($budgets);
- $helper->setStart($start);
- $helper->setEnd($end);
- $helper->setCollectOtherObjects(1 === (int)$others);
- $chartData = $helper->generate('expense', 'account');
- $data = $this->generator->pieChart($chartData);
+ $result = [];
+ $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $budgets);
+
+ // loop expenses.
+ foreach ($spent as $currency) {
+ foreach ($currency['budgets'] as $budget) {
+ $title = sprintf('%s (%s)', $budget['name'], $currency['currency_name']);
+ $result[$title] = $result[$title] ?? [
+ 'amount' => '0',
+ 'currency_symbol' => $currency['currency_symbol'],
+ ];
+ foreach ($budget['transaction_journals'] as $journal) {
+ $amount = app('steam')->positive($journal['amount']);
+ $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
+ }
+ }
+ }
+
+ $data = $this->generator->multiCurrencyPieChart($result);
return response()->json($data);
}
-
/**
* Chart that groups the expenses by budget.
*
- * TODO this chart is not multi-currency aware.
+ * @param Collection $accounts
+ * @param Collection $budgets
+ * @param Carbon $start
+ * @param Carbon $end
+ *
+ * @return JsonResponse
+ */
+ public function categoryExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): JsonResponse
+ {
+ $result = [];
+ $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $budgets);
+ // loop expenses.
+ foreach ($spent as $currency) {
+ foreach ($currency['budgets'] as $budget) {
+
+
+ foreach ($budget['transaction_journals'] as $journal) {
+ $categoryName = $journal['category_name'] ?? trans('firefly.no_category');
+ $title = sprintf('%s (%s)', $categoryName, $currency['currency_name']);
+ $result[$title] = $result[$title] ?? [
+ 'amount' => '0',
+ 'currency_symbol' => $currency['currency_symbol'],
+ ];
+
+ $amount = app('steam')->positive($journal['amount']);
+ $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
+ }
+ }
+ }
+
+ $data = $this->generator->multiCurrencyPieChart($result);
+
+ return response()->json($data);
+ }
+
+ /**
+ * Chart that groups expenses by the account.
*
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
- * @param string $others
*
* @return JsonResponse
*/
- public function budgetExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others): JsonResponse
+ public function destinationAccountExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): JsonResponse
{
- /** @var MetaPieChartInterface $helper */
- $helper = app(MetaPieChartInterface::class);
- $helper->setAccounts($accounts);
- $helper->setBudgets($budgets);
- $helper->setStart($start);
- $helper->setEnd($end);
- $helper->setCollectOtherObjects(1 === (int)$others);
- $chartData = $helper->generate('expense', 'budget');
- $data = $this->generator->pieChart($chartData);
+ $result = [];
+ $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $budgets);
+
+ // loop expenses.
+ foreach ($spent as $currency) {
+ foreach ($currency['budgets'] as $budget) {
+
+
+ foreach ($budget['transaction_journals'] as $journal) {
+ $title = sprintf('%s (%s)', $journal['destination_account_name'], $currency['currency_name']);
+ $result[$title] = $result[$title] ?? [
+ 'amount' => '0',
+ 'currency_symbol' => $currency['currency_symbol'],
+ ];
+
+ $amount = app('steam')->positive($journal['amount']);
+ $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
+ }
+ }
+ }
+
+ $data = $this->generator->multiCurrencyPieChart($result);
return response()->json($data);
}
-
/**
* Main overview of a budget in the budget report.
*
- * TODO this chart is not multi-currency aware.
+ * @param Collection $accounts
+ * @param Budget $budget
+ * @param Carbon $start
+ * @param Carbon $end
+ *
+ * @return JsonResponse
+ */
+ public function mainChart(Collection $accounts, Budget $budget, Carbon $start, Carbon $end): JsonResponse
+ {
+ $chartData = [];
+ $spent = $this->opsRepository->listExpenses($start, $end, $accounts, new Collection([$budget]));
+ $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
+
+ // loop expenses.
+ foreach ($spent as $currency) {
+ // add things to chart Data for each currency:
+ $spentKey = sprintf('%d-spent', $currency['currency_id']);
+ $chartData[$spentKey] = $chartData[$spentKey] ?? [
+ 'label' => sprintf(
+ '%s (%s)', (string)trans('firefly.spent_in_specific_budget', ['budget' => $budget['name']]), $currency['currency_name']
+ ),
+ 'type' => 'bar',
+ 'currency_symbol' => $currency['currency_symbol'],
+ 'currency_id' => $currency['currency_id'],
+ 'entries' => $this->makeEntries($start, $end),
+ ];
+
+ foreach ($currency['budgets'] as $currentBudget) {
+ foreach ($currentBudget['transaction_journals'] as $journal) {
+ $key = $journal['date']->formatLocalized($format);
+ $amount = app('steam')->positive($journal['amount']);
+ $chartData[$spentKey]['entries'][$key] = $chartData[$spentKey]['entries'][$key] ?? '0';
+ $chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount);
+ }
+ }
+ }
+
+ $data = $this->generator->multiSet($chartData);
+
+ return response()->json($data);
+ }
+
+ /**
+ * Chart that groups expenses by the account.
*
* @param Collection $accounts
* @param Collection $budgets
@@ -140,82 +230,56 @@ class BudgetReportController extends Controller
* @param Carbon $end
*
* @return JsonResponse
- *
*/
- public function mainChart(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): JsonResponse
+ public function sourceAccountExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): JsonResponse
{
- $cache = new CacheProperties;
- $cache->addProperty('chart.budget.report.main');
- $cache->addProperty($accounts);
- $cache->addProperty($budgets);
- $cache->addProperty($start);
- $cache->addProperty($end);
- if ($cache->has()) {
- return response()->json($cache->get()); // @codeCoverageIgnore
- }
- $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
- $function = app('navigation')->preferredEndOfPeriod($start, $end);
- $chartData = [];
- $currentStart = clone $start;
+ $result = [];
+ $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $budgets);
- // prep chart data:
- foreach ($budgets as $budget) {
- $chartData[$budget->id] = [
- 'label' => (string)trans('firefly.spent_in_specific_budget', ['budget' => $budget->name]),
- 'type' => 'bar',
- 'yAxisID' => 'y-axis-0',
- 'entries' => [],
- ];
- $chartData[$budget->id . '-sum'] = [
- 'label' => (string)trans('firefly.sum_of_expenses_in_budget', ['budget' => $budget->name]),
- 'type' => 'line',
- 'fill' => false,
- 'yAxisID' => 'y-axis-1',
- 'entries' => [],
- ];
- $chartData[$budget->id . '-left'] = [
- 'label' => (string)trans('firefly.left_in_budget_limit', ['budget' => $budget->name]),
- 'type' => 'bar',
- 'fill' => false,
- 'yAxisID' => 'y-axis-0',
- 'entries' => [],
- ];
- }
- $allBudgetLimits = $this->blRepository->getAllBudgetLimits($start, $end);
- $sumOfExpenses = [];
- $leftOfLimits = [];
- while ($currentStart < $end) {
- $currentEnd = clone $currentStart;
- $currentEnd = $currentEnd->$function();
- $expenses = $this->groupByBudget($this->getExpensesInBudgets($accounts, $budgets, $currentStart, $currentEnd));
- $label = $currentStart->formatLocalized($format);
+ // loop expenses.
+ foreach ($spent as $currency) {
+ foreach ($currency['budgets'] as $budget) {
- /** @var Budget $budget */
- foreach ($budgets as $budget) {
- // get budget limit(s) for this period):
- $budgetLimits = $this->filterBudgetLimits($allBudgetLimits, $budget, $currentStart, $currentEnd);
- $currentExpenses = $expenses[$budget->id] ?? '0';
- $sumOfExpenses[$budget->id] = $sumOfExpenses[$budget->id] ?? '0';
- $sumOfExpenses[$budget->id] = bcadd($currentExpenses, $sumOfExpenses[$budget->id]);
- $chartData[$budget->id]['entries'][$label] = bcmul($currentExpenses, '-1');
- $chartData[$budget->id . '-sum']['entries'][$label] = bcmul($sumOfExpenses[$budget->id], '-1');
- if (count($budgetLimits) > 0) {
- $budgetLimitId = $budgetLimits->first()->id;
- $leftOfLimits[$budgetLimitId] = $leftOfLimits[$budgetLimitId] ?? (string)$budgetLimits->sum('amount');
- $leftOfLimits[$budgetLimitId] = bcadd($leftOfLimits[$budgetLimitId], $currentExpenses);
- $chartData[$budget->id . '-left']['entries'][$label] = $leftOfLimits[$budgetLimitId];
+ foreach ($budget['transaction_journals'] as $journal) {
+ $title = sprintf('%s (%s)', $journal['source_account_name'], $currency['currency_name']);
+ $result[$title] = $result[$title] ?? [
+ 'amount' => '0',
+ 'currency_symbol' => $currency['currency_symbol'],
+ ];
+
+ $amount = app('steam')->positive($journal['amount']);
+ $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount);
}
}
- /** @var Carbon $currentStart */
- $currentStart = clone $currentEnd;
- $currentStart->addDay();
}
- $data = $this->generator->multiSet($chartData);
- $cache->store($data);
+ $data = $this->generator->multiCurrencyPieChart($result);
return response()->json($data);
}
+ /**
+ * @param Carbon $start
+ * @param Carbon $end
+ *
+ * @return array
+ */
+ private function makeEntries(Carbon $start, Carbon $end): array
+ {
+ $return = [];
+ $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
+ $preferredRange = app('navigation')->preferredRangeFormat($start, $end);
+ $currentStart = clone $start;
+ while ($currentStart <= $end) {
+ $currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
+ $key = $currentStart->formatLocalized($format);
+ $return[$key] = '0';
+ $currentStart = clone $currentEnd;
+ $currentStart->addDay()->startOfDay();
+ }
+
+ return $return;
+ }
+
}
diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php
index 61963427d1..ccd3df3563 100644
--- a/app/Http/Controllers/Report/BudgetController.php
+++ b/app/Http/Controllers/Report/BudgetController.php
@@ -148,6 +148,58 @@ class BudgetController extends Controller
return view('reports.budget.partials.accounts', compact('sums', 'report'));
}
+ /**
+ * @param Collection $accounts
+ * @param Collection $budgets
+ * @param Carbon $start
+ * @param Carbon $end
+ */
+ public function avgExpenses(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end)
+ {
+ // get all journals.
+ $opsRepository = app(OperationsRepositoryInterface::class);
+ $spent = $opsRepository->listExpenses($start, $end, $accounts, $budgets);
+ $result = [];
+ foreach ($spent as $currency) {
+ $currencyId = $currency['currency_id'];
+ foreach ($currency['budgets'] as $budget) {
+ foreach ($budget['transaction_journals'] as $journal) {
+ $destinationId = $journal['destination_account_id'];
+ $result[$destinationId] = $result[$destinationId] ?? [
+ 'transactions' => 0,
+ 'sum' => '0',
+ 'avg' => '0',
+ 'avg_float' => 0,
+ 'destination_account_name' => $journal['destination_account_name'],
+ 'destination_account_id' => $journal['destination_account_id'],
+ 'currency_id' => $currency['currency_id'],
+ 'currency_name' => $currency['currency_name'],
+ 'currency_symbol' => $currency['currency_symbol'],
+ 'currency_decimal_places' => $currency['currency_decimal_places'],
+ ];
+ $result[$destinationId]['transactions']++;
+ $result[$destinationId]['sum'] = bcadd($journal['amount'], $result[$destinationId]['sum']);
+ $result[$destinationId]['avg'] = bcdiv($result[$destinationId]['sum'], (string)$result[$destinationId]['transactions']);
+ $result[$destinationId]['avg_float'] = (float)$result[$destinationId]['avg'];
+ }
+ }
+ }
+ // sort by amount_float
+ // sort temp array by amount.
+ $amounts = array_column($result, 'avg_float');
+ array_multisort($amounts, SORT_ASC, $result);
+
+ try {
+ $result = view('reports.budget.partials.avg-expenses', compact('result'))->render();
+ // @codeCoverageIgnoreStart
+ } catch (Throwable $e) {
+ Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage()));
+ $result = sprintf('Could not render view: %s', $e->getMessage());
+ }
+
+ return $result;
+ }
+
/**
* @param Collection $accounts
* @param Collection $budgets
@@ -238,7 +290,6 @@ class BudgetController extends Controller
return $result;
}
-
/**
* Show budget overview for a period.
*
@@ -304,4 +355,52 @@ class BudgetController extends Controller
return $result;
}
+ /**
+ * @param Collection $accounts
+ * @param Collection $budgets
+ * @param Carbon $start
+ * @param Carbon $end
+ */
+ public function topExpenses(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end)
+ {
+ // get all journals.
+ $opsRepository = app(OperationsRepositoryInterface::class);
+ $spent = $opsRepository->listExpenses($start, $end, $accounts, $budgets);
+ $result = [];
+ foreach ($spent as $currency) {
+ $currencyId = $currency['currency_id'];
+ foreach ($currency['budgets'] as $budget) {
+ foreach ($budget['transaction_journals'] as $journal) {
+ $result[] = [
+ 'description' => $journal['description'],
+ 'transaction_group_id' => $journal['transaction_group_id'],
+ 'amount_float' => (float)$journal['amount'],
+ 'amount' => $journal['amount'],
+ 'date' => $journal['date']->formatLocalized($this->monthAndDayFormat),
+ 'destination_account_name' => $journal['destination_account_name'],
+ 'destination_account_id' => $journal['destination_account_id'],
+ 'currency_id' => $currency['currency_id'],
+ 'currency_name' => $currency['currency_name'],
+ 'currency_symbol' => $currency['currency_symbol'],
+ 'currency_decimal_places' => $currency['currency_decimal_places'],
+ ];
+ }
+ }
+ }
+ // sort by amount_float
+ // sort temp array by amount.
+ $amounts = array_column($result, 'amount_float');
+ array_multisort($amounts, SORT_ASC, $result);
+
+ try {
+ $result = view('reports.budget.partials.top-expenses', compact('result'))->render();
+ // @codeCoverageIgnoreStart
+ } catch (Throwable $e) {
+ Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage()));
+ $result = sprintf('Could not render view: %s', $e->getMessage());
+ }
+
+ return $result;
+ }
+
}
diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php
index 32d7b742fd..4ac417931a 100644
--- a/app/Repositories/Budget/OperationsRepository.php
+++ b/app/Repositories/Budget/OperationsRepository.php
@@ -211,7 +211,7 @@ class OperationsRepository implements OperationsRepositoryInterface
if (null === $budgets || (null !== $budgets && 0 === $budgets->count())) {
$collector->setBudgets($this->getBudgets());
}
- $collector->withBudgetInformation();
+ $collector->withBudgetInformation()->withAccountInformation()->withCategoryInformation();
$journals = $collector->getExtractedJournals();
$array = [];
@@ -248,9 +248,15 @@ class OperationsRepository implements OperationsRepositoryInterface
$array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = [
- 'amount' => app('steam')->negative($journal['amount']),
- 'source_account_id' => $journal['source_account_id'],
- 'date' => $journal['date'],
+ 'amount' => app('steam')->negative($journal['amount']),
+ 'destination_account_id' => $journal['destination_account_id'],
+ 'destination_account_name' => $journal['destination_account_name'],
+ 'source_account_id' => $journal['source_account_id'],
+ 'source_account_name' => $journal['source_account_name'],
+ 'category_name' => $journal['category_name'],
+ 'description' => $journal['description'],
+ 'transaction_group_id' => $journal['transaction_group_id'],
+ 'date' => $journal['date'],
];
}
diff --git a/public/v1/js/ff/reports/budget/month.js b/public/v1/js/ff/reports/budget/month.js
index 8586767b38..b2da1ab05e 100644
--- a/public/v1/js/ff/reports/budget/month.js
+++ b/public/v1/js/ff/reports/budget/month.js
@@ -18,8 +18,6 @@
* along with Firefly III. If not, see .
*/
-/** global: budgetExpenseUri, accountExpenseUri, mainUri */
-
$(function () {
"use strict";
drawChart();
@@ -28,14 +26,9 @@ $(function () {
loadAjaxPartial('budgetsHolder', budgetsUri);
loadAjaxPartial('accountPerbudgetHolder', accountPerBudgetUri);
+ loadAjaxPartial('topExpensesHolder', topExpensesUri);
+ loadAjaxPartial('avgExpensesHolder', avgExpensesUri);
- $('#budgets-out-pie-chart-checked').on('change', function () {
- redrawPieChart('budgets-out-pie-chart', budgetExpenseUri);
- });
-
- $('#accounts-out-pie-chart-checked').on('change', function () {
- redrawPieChart('accounts-out-pie-chart', accountExpenseUri);
- });
});
@@ -43,27 +36,21 @@ $(function () {
function drawChart() {
"use strict";
- // month view:
- doubleYNonStackedChart(mainUri, 'in-out-chart');
+ $.each($('.main_budget_canvas'), function (i, v) {
+ var canvas = $(v);
+ columnChart(canvas.data('url'), canvas.attr('id'));
+ });
// draw pie chart of income, depending on "show other transactions too":
redrawPieChart('budgets-out-pie-chart', budgetExpenseUri);
- redrawPieChart('accounts-out-pie-chart', accountExpenseUri);
+ redrawPieChart('categories-out-pie-chart', categoryExpenseUri);
+ redrawPieChart('source-accounts-pie-chart', sourceExpenseUri);
+ redrawPieChart('dest-accounts-pie-chart', destinationExpenseUri);
}
function redrawPieChart(container, uri) {
"use strict";
- var checkbox = $('#' + container + '-checked');
-
- var others = '0';
- // check if box is checked:
- if (checkbox.prop('checked')) {
- others = '1';
- }
- uri = uri.replace('OTHERS', others);
-
- pieChart(uri, container);
-
+ multiCurrencyPieChart(uri, container);
}
diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php
index 5aaf2125fc..f0038e4086 100644
--- a/resources/lang/en_US/firefly.php
+++ b/resources/lang/en_US/firefly.php
@@ -888,6 +888,11 @@ return [
'cannot_change_amount_reconciled' => 'You can\'t change the amount of reconciled transactions.',
'no_budget' => '(no budget)',
'account_per_budget' => 'Account per budget',
+ 'all_other_budgets' => '(all other budgets)',
+ 'all_other_accounts' => '(all other accounts)',
+ 'expense_per_source_account' => 'Expenses per source account',
+ 'expense_per_destination_account' => 'Expenses per destination account',
+ 'average_spending_per_destination' => 'Average expense per destination account',
'no_budget_squared' => '(no budget)',
'perm-delete-many' => 'Deleting many items in one go can be very disruptive. Please be cautious. You can delete part of a split transaction from this page, so take care.',
'mass_deleted_transactions_success' => 'Deleted :amount transaction(s).',
diff --git a/resources/views/v1/reports/budget/month.twig b/resources/views/v1/reports/budget/month.twig
index b6d9f0c526..aed5bea2b5 100644
--- a/resources/views/v1/reports/budget/month.twig
+++ b/resources/views/v1/reports/budget/month.twig
@@ -8,7 +8,6 @@
- {# spent in these budgets per account, per currency.#}
- {% if budgets.count > 1 %}
-
-
-
-
-
-
-
-
-
- {{ 'include_expense_not_in_budget'|_ }}
-
-
-
-
- {% endif %}
-
-
-
-
-
-
+
+
- {#
- Here be a chart with the budget limits as well if relevant.
- amount spent vs budget limit reps
- over the entire period the amount spent would rise and the budget limit rep would be like a heart beat jumping up and down
- needs to be two axes to work
#}
-
+
+
+
- {% if averageExpenses|length > 0 %}
-