diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index 22e0693fa9..77bddb9593 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -36,6 +36,7 @@ use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Support\Http\Api\CleansChartData; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; @@ -110,25 +111,7 @@ class BudgetController extends Controller { // get all limits: $limits = $this->blRepository->getBudgetLimits($budget, $start, $end); - - // 'currency_id' => string '1' (length=1) - // 'currency_code' => string 'EUR' (length=3) - // 'currency_name' => string 'Euro' (length=4) - // 'currency_symbol' => string '€' (length=3) - // 'currency_decimal_places' => int 2 - // 'start' => string '2025-07-01T00:00:00+02:00' (length=25) - // 'end' => string '2025-07-31T23:59:59+02:00' (length=25) - // 'budgeted' => string '100.000000000000' (length=16) - // 'spent' => string '-421.230000000000' (length=17) - // 'left' => string '0' (length=1) - // 'overspent' => string '321.230000000000' (length=16) - - $rows = []; - - // instead of using the budget limits as a thing to collect all expenses, - // use the budget range itself to collect and group them, - // AND THEN add budgeted amounts from the limits to the rows. $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); $expenses = $this->processExpenses($budget->id, $spent, $start, $end); @@ -294,12 +277,35 @@ class BudgetController extends Controller private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit { - foreach ($limits as $limit) { - if ($limit->transaction_currency_id === $currencyId) { - return $limit; + $amount = '0'; + $limit = null; + $converter = new ExchangeRateConverter(); + /** @var BudgetLimit $current */ + foreach ($limits as $current) { + if(true === $this->convertToNative) { + if($current->transaction_currency_id === $this->nativeCurrency->id) { + // simply add it. + $amount = bcadd($amount, (string)$current->amount); + Log::debug(sprintf('Set amount in limit to %s' , $amount)); + } + if($current->transaction_currency_id !== $this->nativeCurrency->id) { + // convert and then add it. + $converted = $converter->convert($current->transactionCurrency,$this->nativeCurrency, $limit->start_date, $limit->amount); + $amount = bcadd($amount, $converted); + Log::debug(sprintf('Budgeted in limit #%d: %s %s, converted to %s %s', $current->id, $current->transactionCurrency->code, $current->amount, $this->nativeCurrency->code, $converted)); + Log::debug(sprintf('Set amount in limit to %s', $amount)); + } + } + if ($current->transaction_currency_id === $currencyId) { + $limit = $current; } } + if(null !== $limit && true === $this->convertToNative) { + // convert and add all amounts. + $limit->amount = app('steam')->positive($amount); + Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount)); + } - return null; + return $limit; } } diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index 726d06c212..7201ee93e5 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -24,14 +24,16 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Budget; -use Deprecated; use Carbon\Carbon; +use Deprecated; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Budget; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; @@ -55,17 +57,17 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn $total = '0'; $count = 0; foreach ($budget->budgetlimits as $limit) { - $diff = (int) $limit->start_date->diffInDays($limit->end_date, true); + $diff = (int)$limit->start_date->diffInDays($limit->end_date, true); $diff = 0 === $diff ? 1 : $diff; $amount = $limit->amount; - $perDay = bcdiv((string) $amount, (string) $diff); + $perDay = bcdiv((string)$amount, (string)$diff); $total = bcadd($total, $perDay); ++$count; app('log')->debug(sprintf('Found %d budget limits. Per day is %s, total is %s', $count, $perDay, $total)); } - $avg = $total; + $avg = $total; if ($count > 0) { - $avg = bcdiv($total, (string) $count); + $avg = bcdiv($total, (string)$count); } app('log')->debug(sprintf('%s / %d = %s = average.', $total, $count, $avg)); @@ -84,21 +86,21 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn // get all transactions: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end); $collector->setBudgets($budgets); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); // loop transactions: /** @var array $journal */ foreach ($journals as $journal) { // prep data array for currency: - $budgetId = (int) $journal['budget_id']; - $budgetName = $journal['budget_name']; - $currencyId = (int) $journal['currency_id']; - $key = sprintf('%d-%d', $budgetId, $currencyId); + $budgetId = (int)$journal['budget_id']; + $budgetName = $journal['budget_name']; + $currencyId = (int)$journal['currency_id']; + $key = sprintf('%d-%d', $budgetId, $currencyId); - $data[$key] ??= [ + $data[$key] ??= [ 'id' => $budgetId, 'name' => sprintf('%s (%s)', $budgetName, $journal['currency_name']), 'sum' => '0', @@ -110,7 +112,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn 'entries' => [], ]; $date = $journal['date']->format($carbonFormat); - $data[$key]['entries'][$date] = bcadd($data[$key]['entries'][$date] ?? '0', (string) $journal['amount']); + $data[$key]['entries'][$date] = bcadd($data[$key]['entries'][$date] ?? '0', (string)$journal['amount']); } return $data; @@ -136,27 +138,53 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn $collector->setBudgets($this->getBudgets()); } $collector->withBudgetInformation()->withAccountInformation()->withCategoryInformation(); - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $array = []; + + // if needs conversion to native. + $convertToNative = Amount::convertToNative($this->user); + $nativeCurrency = Amount::getNativeCurrencyByUserGroup($this->userGroup); + $currencyId = (int) $nativeCurrency->id; + $currencyCode = $nativeCurrency->code; + $currencyName = $nativeCurrency->name; + $currencySymbol = $nativeCurrency->symbol; + $currencyDecimalPlaces = $nativeCurrency->decimal_places; + $converter = new ExchangeRateConverter(); + $currencies = [ + $currencyId => $nativeCurrency, + ]; foreach ($journals as $journal) { - $currencyId = (int) $journal['currency_id']; - $budgetId = (int) $journal['budget_id']; - $budgetName = (string) $journal['budget_name']; + $amount = app('steam')->negative($journal['amount']); + $journalCurrencyId = (int)$journal['currency_id']; + if (false === $convertToNative) { + $currencyId = $journalCurrencyId; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + } + if(true === $convertToNative && $journalCurrencyId !== $currencyId) { + $currencies[$journalCurrencyId]??= TransactionCurrency::find($journalCurrencyId); + $amount = $converter->convert($currencies[$journalCurrencyId], $nativeCurrency, $journal['date'], $amount); + } - // catch "no category" entries. + $budgetId = (int)$journal['budget_id']; + $budgetName = (string)$journal['budget_name']; + + // catch "no budget" entries. if (0 === $budgetId) { continue; } // info about the currency: - $array[$currencyId] ??= [ + $array[$currencyId] ??= [ 'budgets' => [], 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + 'currency_decimal_places' => $currencyDecimalPlaces, ]; // info about the categories: @@ -168,9 +196,9 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn // add journal to array: // only a subset of the fields. - $journalId = (int) $journal['transaction_journal_id']; + $journalId = (int)$journal['transaction_journal_id']; $array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = [ - 'amount' => app('steam')->negative($journal['amount']), + 'amount' => $amount, 'destination_account_id' => $journal['destination_account_id'], 'destination_account_name' => $journal['destination_account_name'], 'source_account_id' => $journal['source_account_id'], @@ -203,7 +231,8 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn ?Collection $budgets = null, ?TransactionCurrency $currency = null, bool $convertToNative = false - ): array { + ): array + { Log::debug(sprintf('Start of %s(date, date, array, array, "%s", %s).', __METHOD__, $currency?->code, var_export($convertToNative, true))); // this collector excludes all transfers TO liabilities (which are also withdrawals) // because those expenses only become expenses once they move from the liability to the friend. @@ -211,8 +240,8 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn $repository = app(AccountRepositoryInterface::class); $repository->setUser($this->user); - $subset = $repository->getAccountsByType(config('firefly.valid_liabilities')); - $selection = new Collection(); + $subset = $repository->getAccountsByType(config('firefly.valid_liabilities')); + $selection = new Collection(); /** @var Account $account */ foreach ($subset as $account) { @@ -222,12 +251,11 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn } /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user) - ->setRange($start, $end) + ->setRange($start, $end) // ->excludeDestinationAccounts($selection) - ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) - ; + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); if ($accounts instanceof Collection) { $collector->setAccounts($accounts); @@ -242,7 +270,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn if ($budgets->count() > 0) { $collector->setBudgets($budgets); } - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); // same but for transactions in the foreign currency: if ($currency instanceof TransactionCurrency) { diff --git a/resources/assets/v2/src/pages/dashboard/budgets.js b/resources/assets/v2/src/pages/dashboard/budgets.js index 1195fff3c4..86062af1a2 100644 --- a/resources/assets/v2/src/pages/dashboard/budgets.js +++ b/resources/assets/v2/src/pages/dashboard/budgets.js @@ -48,9 +48,19 @@ export default () => ({ } this.getFreshData(); }, + + eventListeners: { + ['@convert-to-native.window'](event){ + console.log('I heard that! (dashboard/budgets)'); + this.convertToNative = event.detail; + chartData = null; + this.loadChart(); + } + }, + drawChart(options) { if (null !== chart) { - chart.data.datasets = options.data.datasets; + chart.data = options.data; chart.update(); return; } @@ -59,7 +69,7 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey('ds_bdg_chart', {start: start, end: end}); + const cacheKey = getCacheKey('ds_bdg_chart', {convertToNative: this.convertToNative, start: start, end: end}); //const cacheValid = window.store.get('cacheValid'); const cacheValid = false; let cachedData = window.store.get(cacheKey); diff --git a/resources/assets/v2/src/pages/dashboard/categories.js b/resources/assets/v2/src/pages/dashboard/categories.js index 922e5d6027..7e89098996 100644 --- a/resources/assets/v2/src/pages/dashboard/categories.js +++ b/resources/assets/v2/src/pages/dashboard/categories.js @@ -33,6 +33,17 @@ let afterPromises = false; export default () => ({ loading: false, convertToNative: false, + + eventListeners: { + ['@convert-to-native.window'](event){ + console.log('I heard that! (dashboard/categories)'); + this.convertToNative = event.detail; + chartData = null; + this.loadChart(); + } + }, + + generateOptions(data) { currencies = []; let options = getDefaultChartSettings('column'); @@ -147,7 +158,7 @@ export default () => ({ getFreshData() { const start = new Date(window.store.get('start')); const end = new Date(window.store.get('end')); - const cacheKey = getCacheKey('ds_ct_chart', {start: start, end: end}); + const cacheKey = getCacheKey('ds_ct_chart', {convertToNative: this.convertToNative, start: start, end: end}); const cacheValid = window.store.get('cacheValid'); let cachedData = window.store.get(cacheKey); diff --git a/resources/assets/v2/src/pages/dashboard/dashboard.js b/resources/assets/v2/src/pages/dashboard/dashboard.js index 748dea68a3..c57f82d842 100644 --- a/resources/assets/v2/src/pages/dashboard/dashboard.js +++ b/resources/assets/v2/src/pages/dashboard/dashboard.js @@ -71,9 +71,10 @@ let index = function () { return { convertToNative: false, saveNativeSettings(event) { - setVariable('convert_to_native', event.currentTarget.checked).then(() => { - console.log('Set convert to native to: ', event.currentTarget.checked); - this.$dispatch('convert-to-native', event.currentTarget.checked); + let target = event.currentTarget || event.target; + setVariable('convert_to_native',target.checked).then(() => { + console.log('Set convert to native to: ', target.checked); + this.$dispatch('convert-to-native', target.checked); }); }, init() { diff --git a/resources/views/v2/partials/dashboard/budget-chart.blade.php b/resources/views/v2/partials/dashboard/budget-chart.blade.php index 19fbd8e2c0..33e81a7c9c 100644 --- a/resources/views/v2/partials/dashboard/budget-chart.blade.php +++ b/resources/views/v2/partials/dashboard/budget-chart.blade.php @@ -1,4 +1,4 @@ -