Update category overview to be multi-currency aware.

This commit is contained in:
James Cole
2025-08-15 07:44:14 +02:00
parent fc9ef290f1
commit 844b8d48c4
5 changed files with 71 additions and 41 deletions

View File

@@ -50,7 +50,7 @@ class BudgetController extends Controller
use CleansChartData; use CleansChartData;
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
protected OperationsRepositoryInterface $opsRepository; protected OperationsRepositoryInterface $opsRepository;
private BudgetLimitRepositoryInterface $blRepository; private BudgetLimitRepositoryInterface $blRepository;
@@ -81,15 +81,15 @@ class BudgetController extends Controller
* *
* @throws FireflyException * @throws FireflyException
*/ */
public function dashboard(DateRequest $request): JsonResponse public function overview(DateRequest $request): JsonResponse
{ {
$params = $request->getAll(); $params = $request->getAll();
/** @var Carbon $start */ /** @var Carbon $start */
$start = $params['start']; $start = $params['start'];
/** @var Carbon $end */ /** @var Carbon $end */
$end = $params['end']; $end = $params['end'];
// code from FrontpageChartGenerator, but not in separate class // code from FrontpageChartGenerator, but not in separate class
$budgets = $this->repository->getActiveBudgets(); $budgets = $this->repository->getActiveBudgets();
@@ -116,12 +116,12 @@ class BudgetController extends Controller
$expenses = $this->processExpenses($budget->id, $spent, $start, $end); $expenses = $this->processExpenses($budget->id, $spent, $start, $end);
/** /**
* @var int $currencyId * @var int $currencyId
* @var array $row * @var array $row
*/ */
foreach ($expenses as $currencyId => $row) { foreach ($expenses as $currencyId => $row) {
// budgeted, left and overspent are now 0. // budgeted, left and overspent are now 0.
$limit = $this->filterLimit($currencyId, $limits); $limit = $this->filterLimit($currencyId, $limits);
if (null !== $limit) { if (null !== $limit) {
$row['budgeted'] = $limit->amount; $row['budgeted'] = $limit->amount;
$row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1'));
@@ -140,7 +140,7 @@ class BudgetController extends Controller
// } // }
// is always an array // is always an array
$return = []; $return = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$current = [ $current = [
'label' => $budget->name, 'label' => $budget->name,
@@ -149,14 +149,20 @@ class BudgetController extends Controller
'currency_name' => $row['currency_name'], 'currency_name' => $row['currency_name'],
'currency_decimal_places' => $row['currency_decimal_places'], 'currency_decimal_places' => $row['currency_decimal_places'],
'period' => null, 'period' => null,
'start' => $row['start'], 'date' => $row['start'],
'end' => $row['end'], 'start_date' => $row['start'],
'end_date' => $row['end'],
'yAxisID' => 0,
'type' => 'bar',
'entries' => [ 'entries' => [
'budgeted' => $row['budgeted'], 'budgeted' => $row['budgeted'],
'spent' => $row['spent'], 'spent' => $row['spent'],
'left' => $row['left'], 'left' => $row['left'],
'overspent' => $row['overspent'], 'overspent' => $row['overspent'],
], ],
'pc_entries' => [
],
]; ];
$return[] = $current; $return[] = $current;
} }
@@ -191,7 +197,7 @@ class BudgetController extends Controller
* This array contains the expenses in this budget. Grouped per currency. * This array contains the expenses in this budget. Grouped per currency.
* The grouping is on the main currency only. * The grouping is on the main currency only.
* *
* @var int $currencyId * @var int $currencyId
* @var array $block * @var array $block
*/ */
foreach ($spent as $currencyId => $block) { foreach ($spent as $currencyId => $block) {
@@ -209,7 +215,7 @@ class BudgetController extends Controller
'left' => '0', 'left' => '0',
'overspent' => '0', 'overspent' => '0',
]; ];
$currentBudgetArray = $block['budgets'][$budgetId]; $currentBudgetArray = $block['budgets'][$budgetId];
// var_dump($return); // var_dump($return);
/** @var array $journal */ /** @var array $journal */
@@ -250,7 +256,7 @@ class BudgetController extends Controller
private function processLimit(Budget $budget, BudgetLimit $limit): array private function processLimit(Budget $budget, BudgetLimit $limit): array
{ {
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
$end = clone $limit->end_date; $end = clone $limit->end_date;
$end->endOfDay(); $end->endOfDay();
$spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget]));
$limitCurrencyId = $limit->transaction_currency_id; $limitCurrencyId = $limit->transaction_currency_id;
@@ -258,8 +264,8 @@ class BudgetController extends Controller
/** @var array $entry */ /** @var array $entry */
// only spent the entry where the entry's currency matches the budget limit's currency // only spent the entry where the entry's currency matches the budget limit's currency
// so $filtered will only have 1 or 0 entries // so $filtered will only have 1 or 0 entries
$filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); $filtered = array_filter($spent, fn($entry) => $entry['currency_id'] === $limitCurrencyId);
$result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end);
if (1 === count($result)) { if (1 === count($result)) {
$compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent']));
$result[$limitCurrencyId]['budgeted'] = $limit->amount; $result[$limitCurrencyId]['budgeted'] = $limit->amount;

View File

@@ -34,6 +34,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
@@ -77,10 +78,10 @@ class CategoryController extends Controller
* *
* @SuppressWarnings("PHPMD.UnusedFormalParameter") * @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/ */
public function dashboard(DateRequest $request): JsonResponse public function overview(DateRequest $request): JsonResponse
{ {
/** @var Carbon $start */ /** @var Carbon $start */
$start = $this->parameters->get('start'); $start = $this->parameters->get('start');
/** @var Carbon $end */ /** @var Carbon $end */
$end = $this->parameters->get('end'); $end = $this->parameters->get('end');
@@ -91,11 +92,11 @@ class CategoryController extends Controller
// get journals for entire period: // get journals for entire period:
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withAccountInformation(); $collector->setRange($start, $end)->withAccountInformation();
$collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation();
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]);
$journals = $collector->getExtractedJournals(); $journals = $collector->getExtractedJournals();
/** @var array $journal */ /** @var array $journal */
foreach ($journals as $journal) { foreach ($journals as $journal) {
@@ -108,44 +109,62 @@ class CategoryController extends Controller
$currencyCode = (string)$currency->code; $currencyCode = (string)$currency->code;
$currencySymbol = (string)$currency->symbol; $currencySymbol = (string)$currency->symbol;
$currencyDecimalPlaces = (int)$currency->decimal_places; $currencyDecimalPlaces = (int)$currency->decimal_places;
$amount = app('steam')->positive($journal['amount']); $amount = Steam::positive($journal['amount']);
$pcAmount = null;
// overrule if necessary: // overrule if necessary:
if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) {
$pcAmount = $amount;
}
if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) { if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) {
$currencyId = (int)$this->primaryCurrency->id; $currencyId = (int)$this->primaryCurrency->id;
$currencyName = (string)$this->primaryCurrency->name; $currencyName = (string)$this->primaryCurrency->name;
$currencyCode = (string)$this->primaryCurrency->code; $currencyCode = (string)$this->primaryCurrency->code;
$currencySymbol = (string)$this->primaryCurrency->symbol; $currencySymbol = (string)$this->primaryCurrency->symbol;
$currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places; $currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places;
$convertedAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); $pcAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount);
Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $convertedAmount)); Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount));
$amount = $convertedAmount;
} }
$categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category');
$key = sprintf('%s-%s', $categoryName, $currencyCode); $key = sprintf('%s-%s', $categoryName, $currencyCode);
// create arrays // create arrays
$return[$key] ??= [ $return[$key] ??= [
'label' => $categoryName, 'label' => $categoryName,
'currency_id' => (string)$currencyId, 'currency_id' => (string)$currencyId,
'currency_code' => $currencyCode, 'currency_name' => $currencyName,
'currency_name' => $currencyName, 'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol, 'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces, 'currency_decimal_places' => $currencyDecimalPlaces,
'period' => null, 'primary_currency_id' => (string)$this->primaryCurrency->id,
'start' => $start->toAtomString(), 'primary_currency_name' => (string)$this->primaryCurrency->name,
'end' => $end->toAtomString(), 'primary_currency_code' => (string)$this->primaryCurrency->code,
'amount' => '0', 'primary_currency_symbol' => (string)$this->primaryCurrency->symbol,
'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places,
'period' => null,
'start_date' => $start->toAtomString(),
'end_date' => $end->toAtomString(),
'yAxisID' => 0,
'type' => 'bar',
'entries' => [
'spent' => '0'
],
'pc_entries' => [
'spent' => '0'
],
]; ];
// add monies // add monies
$return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount); $return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount);
if (null !== $pcAmount) {
$return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount);
}
} }
$return = array_values($return); $return = array_values($return);
// order by amount // order by amount
usort($return, static fn (array $a, array $b) => (float)$a['amount'] < (float)$b['amount'] ? 1 : -1); usort($return, static fn(array $a, array $b) => (float)$a['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1);
return response()->json($this->clean($return)); return response()->json($this->clean($return));
} }

View File

@@ -80,6 +80,7 @@ class AccountBalanceGrouped
'start_date' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(),
'end_date' => $this->end->toAtomString(), 'end_date' => $this->end->toAtomString(),
'yAxisID' => 0, 'yAxisID' => 0,
'type' => 'line',
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],
'pc_entries' => [], 'pc_entries' => [],
@@ -97,6 +98,7 @@ class AccountBalanceGrouped
'date' => $this->start->toAtomString(), 'date' => $this->start->toAtomString(),
'start_date' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(),
'end_date' => $this->end->toAtomString(), 'end_date' => $this->end->toAtomString(),
'type' => 'line',
'yAxisID' => 0, 'yAxisID' => 0,
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],

View File

@@ -61,7 +61,10 @@ trait CleansChartData
if (array_key_exists('primary_currency_id', $array)) { if (array_key_exists('primary_currency_id', $array)) {
$array['primary_currency_id'] = (string)$array['primary_currency_id']; $array['primary_currency_id'] = (string)$array['primary_currency_id'];
} }
$required = ['start_date', 'end_date', 'period', 'yAxisID']; $required = [
'start_date', 'end_date', 'period', 'yAxisID','type','entries','pc_entries',
'currency_id', 'primary_currency_id'
];
foreach ($required as $field) { foreach ($required as $field) {
if (!array_key_exists($field, $array)) { if (!array_key_exists($field, $array)) {
throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field));

View File

@@ -128,7 +128,7 @@ Route::group(
'as' => 'api.v1.chart.budget.', 'as' => 'api.v1.chart.budget.',
], ],
static function (): void { static function (): void {
Route::get('dashboard', ['uses' => 'BudgetController@dashboard', 'as' => 'dashboard']); Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']);
} }
); );
@@ -139,7 +139,7 @@ Route::group(
'as' => 'api.v1.chart.category.', 'as' => 'api.v1.chart.category.',
], ],
static function (): void { static function (): void {
Route::get('dashboard', ['uses' => 'CategoryController@dashboard', 'as' => 'dashboard']); Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']);
} }
); );