From 7b3a5c1afd4085ea59cd7b084ba95d3fdc5c2d56 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 24 Dec 2024 16:56:31 +0100 Subject: [PATCH] Frontpage seems to be multi currency aware. --- .../Controllers/Summary/BasicController.php | 2 + .../Correction/RecalculateNativeAmounts.php | 1 + .../Observer/AvailableBudgetObserver.php | 1 + app/Handlers/Observer/TransactionObserver.php | 2 + app/Helpers/Collector/GroupCollector.php | 10 ++ .../Collector/GroupCollectorInterface.php | 5 + .../Controllers/Chart/AccountController.php | 53 ++++++---- app/Repositories/Bill/BillRepository.php | 3 +- .../Budget/OperationsRepository.php | 34 ++++--- .../Chart/Budget/FrontpageChartGenerator.php | 24 ++++- .../Http/Api/ExchangeRateConverter.php | 79 ++++++++------- .../Http/Controllers/ChartGeneration.php | 2 +- app/Support/Steam.php | 97 +++++++++++++------ app/Support/Twig/General.php | 6 +- 14 files changed, 211 insertions(+), 108 deletions(-) diff --git a/app/Api/V1/Controllers/Summary/BasicController.php b/app/Api/V1/Controllers/Summary/BasicController.php index 89217caf55..e374d0f78c 100644 --- a/app/Api/V1/Controllers/Summary/BasicController.php +++ b/app/Api/V1/Controllers/Summary/BasicController.php @@ -322,6 +322,7 @@ class BasicController extends Controller private function getNetWorthInfo(Carbon $start, Carbon $end): array { + Log::debug('getNetWorthInfo'); /** @var User $user */ $user = auth()->user(); $date = now(config('app.timezone')); @@ -369,6 +370,7 @@ class BasicController extends Controller 'sub_title' => '', ]; } + Log::debug('End of getNetWorthInfo'); return $return; } diff --git a/app/Console/Commands/Correction/RecalculateNativeAmounts.php b/app/Console/Commands/Correction/RecalculateNativeAmounts.php index c973ca0090..9d97a2c091 100644 --- a/app/Console/Commands/Correction/RecalculateNativeAmounts.php +++ b/app/Console/Commands/Correction/RecalculateNativeAmounts.php @@ -108,6 +108,7 @@ class RecalculateNativeAmounts extends Command private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void { $converter = new ExchangeRateConverter(); + $converter->setUserGroup($userGroup); $converter->setIgnoreSettings(true); $repository = app(PiggyBankRepositoryInterface::class); $repository->setUserGroup($userGroup); diff --git a/app/Handlers/Observer/AvailableBudgetObserver.php b/app/Handlers/Observer/AvailableBudgetObserver.php index 56a4af3cd8..e7102aa6c5 100644 --- a/app/Handlers/Observer/AvailableBudgetObserver.php +++ b/app/Handlers/Observer/AvailableBudgetObserver.php @@ -48,6 +48,7 @@ class AvailableBudgetObserver $availableBudget->native_amount = null; if ($availableBudget->transactionCurrency->id !== $userCurrency->id) { $converter = new ExchangeRateConverter(); + $converter->setUserGroup($availableBudget->user->userGroup); $converter->setIgnoreSettings(true); $availableBudget->native_amount = $converter->convert($availableBudget->transactionCurrency, $userCurrency, today(), $availableBudget->amount); } diff --git a/app/Handlers/Observer/TransactionObserver.php b/app/Handlers/Observer/TransactionObserver.php index bedb0c02e4..8c79d83ac5 100644 --- a/app/Handlers/Observer/TransactionObserver.php +++ b/app/Handlers/Observer/TransactionObserver.php @@ -73,12 +73,14 @@ class TransactionObserver // first normal amount if ($transaction->transactionCurrency->id !== $userCurrency->id && (null === $transaction->foreign_currency_id || (null !== $transaction->foreign_currency_id && $transaction->foreign_currency_id !== $userCurrency->id))) { $converter = new ExchangeRateConverter(); + $converter->setUserGroup($transaction->transactionJournal->user->userGroup); $converter->setIgnoreSettings(true); $transaction->native_amount = $converter->convert($transaction->transactionCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->amount); } // then foreign amount if ($transaction->foreignCurrency?->id !== $userCurrency->id && null !== $transaction->foreign_amount && null !== $transaction->foreignCurrency) { $converter = new ExchangeRateConverter(); + $converter->setUserGroup($transaction->transactionJournal->user->userGroup); $converter->setIgnoreSettings(true); $transaction->native_foreign_amount = $converter->convert($transaction->foreignCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->foreign_amount); } diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 271e7c8255..00a08e70f6 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -874,6 +874,16 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit results to a specific currency, only normal one. + */ + public function setNormalCurrency(TransactionCurrency $currency): GroupCollectorInterface + { + $this->query->where('source.transaction_currency_id', $currency->id); + + return $this; + } + public function setEndRow(int $endRow): self { $this->endRow = $endRow; diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index 8338565de4..6ff39b81b1 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -457,6 +457,11 @@ interface GroupCollectorInterface */ public function setCurrency(TransactionCurrency $currency): self; + /** + * Limit results to a specific currency, either foreign or normal one. + */ + public function setNormalCurrency(TransactionCurrency $currency): self; + /** * Set destination accounts. */ diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index ed15024f06..b48438aa10 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -83,6 +83,7 @@ class AccountController extends Controller */ public function expenseAccounts(): JsonResponse { + Log::debug('RevenueAccounts'); /** @var Carbon $start */ $start = clone session('start', today(config('app.timezone'))->startOfMonth()); @@ -95,7 +96,7 @@ class AccountController extends Controller $cache->addProperty($convertToNative); $cache->addProperty('chart.account.expense-accounts'); if ($cache->has()) { - // return response()->json($cache->get()); + // return response()->json($cache->get()); } $start->subDay(); @@ -113,7 +114,6 @@ class AccountController extends Controller $startBalances = app('steam')->finalAccountsBalance($accounts, $start); $endBalances = app('steam')->finalAccountsBalance($accounts, $end); - // loop the end balances. This is an array for each account ($expenses) // loop the accounts, then check for balance and currency info. foreach($accounts as $account) { Log::debug(sprintf('Now in account #%d ("%s")', $account->id, $account->name)); @@ -518,11 +518,13 @@ class AccountController extends Controller /** @var Carbon $end */ $end = clone session('end', today(config('app.timezone'))->endOfMonth()); $cache = new CacheProperties(); + $convertToNative = app('preferences')->get('convert_to_native', false)->data; $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($convertToNative); $cache->addProperty('chart.account.revenue-accounts'); if ($cache->has()) { - return response()->json($cache->get()); + // return response()->json($cache->get()); } $start->subDay(); @@ -530,9 +532,10 @@ class AccountController extends Controller $currencies = []; $chartData = []; $tempData = []; + $default = Amount::getDefaultCurrency(); // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountType::REVENUE]); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); $accountNames = $this->extractNames($accounts); // grab all balances @@ -540,33 +543,49 @@ class AccountController extends Controller $endBalances = app('steam')->finalAccountsBalance($accounts, $end); - - // loop the end balances. This is an array for each account ($expenses) - foreach ($endBalances as $accountId => $expenses) { - $accountId = (int) $accountId; - // loop each expense entry (each entry can be a different currency). - foreach ($expenses as $currencyCode => $endAmount) { - if (3 !== strlen($currencyCode)) { +// loop the accounts, then check for balance and currency info. + foreach($accounts as $account) { + Log::debug(sprintf('Now in account #%d ("%s")', $account->id, $account->name)); + $expenses = $endBalances[$account->id] ?? false; + if(false === $expenses) { + Log::error(sprintf('Found no end balance for account #%d',$account->id)); + continue; + } + /** + * @var string $key + * @var string $endBalance + */ + foreach ($expenses as $key => $endBalance) { + if(!$convertToNative && 'native_balance' === $key) { + Log::debug(sprintf('[a] Will skip expense array "%s"', $key)); continue; } - + if($convertToNative && 'native_balance' !== $key) { + Log::debug(sprintf('[b] Will skip expense array "%s"', $key)); + continue; + } + Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); + $searchCode = $convertToNative ? $default->code: $key; + Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. - $startAmount = (string) ($startBalances[$accountId][$currencyCode] ?? '0'); - $diff = bcsub((string) $endAmount, $startAmount); - $currencies[$currencyCode] ??= $this->currencyRepository->findByCode($currencyCode); + $startBalance = ($startBalances[$account->id][$key] ?? '0'); + Log::debug(sprintf('Start balance is %s', $startBalance)); + $diff = bcsub($endBalance, $startBalance); + $currencies[$searchCode] ??= $this->currencyRepository->findByCode($searchCode); if (0 !== bccomp($diff, '0')) { // store the values in a temporary array. $tempData[] = [ - 'name' => $accountNames[$accountId], + 'name' => $accountNames[$account->id], 'difference' => $diff, 'diff_float' => (float) $diff, // intentional float - 'currency_id' => $currencies[$currencyCode]->id, + 'currency_id' => $currencies[$searchCode]->id, ]; } } } + // recreate currencies, but on ID instead of code. $newCurrencies = []; foreach ($currencies as $currency) { diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index fb8c0a087e..7dd401ecf5 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -563,11 +563,12 @@ class BillRepository implements BillRepositoryInterface $minField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_min' : 'amount_min'; $maxField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_max' : 'amount_max'; - Log::debug(sprintf('min field is %s, max field is %s', $minField, $maxField)); + //Log::debug(sprintf('min field is %s, max field is %s', $minField, $maxField)); if ($total > 0) { $currency = $convertToNative && $bill->transactionCurrency->id !== $default->id ? $default : $bill->transactionCurrency; $average = bcdiv(bcadd($bill->$maxField, $bill->$minField), '2'); + Log::debug(sprintf('Amount to pay is %s %s (%d times)', $currency->code, $average, $total)); $return[$currency->id] ??= [ 'id' => (string) $currency->id, 'name' => $currency->name, diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index 0acfcec310..95f649a71a 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Budget; @@ -239,7 +240,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setUser($this->user) ->setRange($start, $end) // ->excludeDestinationAccounts($selection) - ->setTypes([TransactionType::WITHDRAWAL]); + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); if (null !== $accounts) { $collector->setAccounts($accounts); @@ -249,26 +250,29 @@ class OperationsRepository implements OperationsRepositoryInterface } if (null !== $currency) { Log::debug(sprintf('Limit to currency %s', $currency->code)); - $collector->setCurrency($currency); + $collector->setNormalCurrency($currency); } $collector->setBudgets($budgets); $journals = $collector->getExtractedJournals(); // same but for transactions in the foreign currency: if (null !== $currency) { - // app('log')->debug(sprintf('Currency is "%s".', $currency->name)); - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setForeignCurrency($currency)->setBudgets($budgets); + Log::debug('STOP looking for transactions in the foreign currency.'); - if (null !== $accounts) { - $collector->setAccounts($accounts); - } - $result = $collector->getExtractedJournals(); - // app('log')->debug(sprintf('Found %d journals with currency %s.', count($result), $currency->code)); - // do not use array_merge because you want keys to overwrite (otherwise you get double results): - Log::debug(sprintf('Found %d extra journals in foreign currency.', count($result))); - $journals = $result + $journals; +// Log::debug(sprintf('Look for transactions with foreign currency %s', $currency->code)); +// // app('log')->debug(sprintf('Currency is "%s".', $currency->name)); +// /** @var GroupCollectorInterface $collector */ +// $collector = app(GroupCollectorInterface::class); +// $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setForeignCurrency($currency)->setBudgets($budgets); +// +// if (null !== $accounts) { +// $collector->setAccounts($accounts); +// } +// $result = $collector->getExtractedJournals(); +// // app('log')->debug(sprintf('Found %d journals with currency %s.', count($result), $currency->code)); +// // do not use array_merge because you want keys to overwrite (otherwise you get double results): +// Log::debug(sprintf('Found %d extra journals in foreign currency.', count($result))); +// $journals = $result + $journals; } $array = []; @@ -289,6 +293,7 @@ class OperationsRepository implements OperationsRepositoryInterface $useNative = $default->id !== (int) $journal['currency_id']; $amount = Amount::getAmountFromJournal($journal); if($useNative) { + Log::debug(sprintf('Journal #%d switches to native amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code'])); $currencyId = $default->id; $currencyName = $default->name; $currencySymbol = $default->symbol; @@ -301,6 +306,7 @@ class OperationsRepository implements OperationsRepositoryInterface // if the amount is not in $currency (but should be), use the foreign_amount if that one is correct. // otherwise, ignore the transaction all together. if (null !== $currency && $currencyId !== $currency->id && $currency->id === (int) $journal['foreign_currency_id']) { + Log::debug(sprintf('Journal #%d switches to foreign amount because it matches native.', $journal['transaction_journal_id'])); $amount = $journal['foreign_amount']; $currencyId = (int) $journal['foreign_currency_id']; $currencyName = $journal['foreign_currency_name']; diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php index f43cf44c18..b67973b1c2 100644 --- a/app/Support/Chart/Budget/FrontpageChartGenerator.php +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -150,8 +150,7 @@ class FrontpageChartGenerator $useNative = $this->convertToNative && $this->default->id !== $limit->transaction_currency_id; $currency = $limit->transactionCurrency; if ($useNative) { - Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $this->default->code, $limit->native_amount)); - $currency = null; + Log::debug(sprintf('Processing limit #%d with (native) %s %s', $limit->id, $this->default->code, $limit->native_amount)); } if (!$useNative) { Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $limit->transactionCurrency->code, $limit->amount)); @@ -167,6 +166,9 @@ class FrontpageChartGenerator Log::debug(sprintf('Process spent row (%s)', $entry['currency_code'])); $data = $this->processRow($data, $budget, $limit, $entry); } + if (!($entry['currency_id'] === $limit->transaction_currency_id || $useNative)) { + Log::debug(sprintf('Skipping spent row (%s).', $entry['currency_code'])); + } } return $data; @@ -181,6 +183,7 @@ class FrontpageChartGenerator private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array { $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + Log::debug(sprintf('Title is "%s"', $title)); if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { $title = sprintf( '%s (%s) (%s - %s)', @@ -190,15 +193,26 @@ class FrontpageChartGenerator $limit->end_date->isoFormat($this->monthAndDayFormat) ); } + $useNative = $this->convertToNative && $this->default->id !== $limit->transaction_currency_id; + $amount = $limit->amount; + if($useNative) { + $amount = $limit->native_amount; + } + + $sumSpent = bcmul($entry['sum'], '-1'); // spent $data[0]['entries'][$title] ??= '0'; $data[1]['entries'][$title] ??= '0'; $data[2]['entries'][$title] ??= '0'; - $data[0]['entries'][$title] = bcadd($data[0]['entries'][$title], 1 === bccomp($sumSpent, $limit->amount) ? $limit->amount : $sumSpent); // spent - $data[1]['entries'][$title] = bcadd($data[1]['entries'][$title],1 === bccomp($limit->amount, $sumSpent) ? bcadd($entry['sum'], $limit->amount) : '0'); // left to spent - $data[2]['entries'][$title] = bcadd($data[2]['entries'][$title],1 === bccomp($limit->amount, $sumSpent) ? '0' : bcmul(bcadd($entry['sum'], $limit->amount), '-1')); // overspent + $data[0]['entries'][$title] = bcadd($data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent + $data[1]['entries'][$title] = bcadd($data[1]['entries'][$title],1 === bccomp($amount, $sumSpent) ? bcadd($entry['sum'], $amount) : '0'); // left to spent + $data[2]['entries'][$title] = bcadd($data[2]['entries'][$title],1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd($entry['sum'], $amount), '-1')); // overspent + + Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title])); + Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title])); + Log::debug(sprintf('Amount [overspent] is now %s.', $data[2]['entries'][$title])); return $data; } diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index b63f0acad3..2062bacae4 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -28,7 +28,9 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\UserGroup; use FireflyIII\Support\CacheProperties; +use FireflyIII\User; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -45,6 +47,20 @@ class ExchangeRateConverter private array $prepared = []; private int $queryCount = 0; + private UserGroup $userGroup; + + public function __construct() + { + if (auth()->check()) { + $this->userGroup = auth()->user()->userGroup; + } + } + + public function setUserGroup(UserGroup $userGroup): void + { + $this->userGroup = $userGroup; + } + /** * @throws FireflyException */ @@ -85,8 +101,8 @@ class ExchangeRateConverter */ private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); // find in cache if (null !== $res) { @@ -96,7 +112,7 @@ class ExchangeRateConverter } // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { Cache::forever($key, $rate); Log::debug(sprintf('ExchangeRateConverter: Return DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); @@ -105,11 +121,11 @@ class ExchangeRateConverter } // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); if (null !== $rate) { $rate = bcdiv('1', $rate); Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); + Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); return $rate; } @@ -143,7 +159,7 @@ class ExchangeRateConverter if ($from === $to) { return '1'; } - $key = sprintf('cer-%d-%d-%s', $from, $to, $date); + $key = sprintf('cer-%d-%d-%s', $from, $to, $date); // perhaps the rate has been cached during this particular run $preparedRate = $this->prepared[$date][$from][$to] ?? null; @@ -153,7 +169,7 @@ class ExchangeRateConverter return $preparedRate; } - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($key); if ($cache->has()) { $rate = $cache->get(); @@ -166,16 +182,14 @@ class ExchangeRateConverter } /** @var null|CurrencyExchangeRate $result */ - $result = auth()->user() - ?->currencyExchangeRates() - ->where('from_currency_id', $from) - ->where('to_currency_id', $to) - ->where('date', '<=', $date) - ->orderBy('date', 'DESC') - ->first() - ; + $result = $this->userGroup->currencyExchangeRates() + ->where('from_currency_id', $from) + ->where('to_currency_id', $to) + ->where('date', '<=', $date) + ->orderBy('date', 'DESC') + ->first(); ++$this->queryCount; - $rate = (string) $result?->rate; + $rate = (string) $result?->rate; if ('' === $rate) { app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date)); @@ -215,13 +229,13 @@ class ExchangeRateConverter if ($euroId === $currency->id) { return '1'; } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); if (null !== $rate) { // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); return $rate; } - $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); if (null !== $rate) { return bcdiv('1', $rate); // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate)); @@ -250,7 +264,7 @@ class ExchangeRateConverter if ($cache->has()) { return (int) $cache->get(); } - $euro = TransactionCurrency::whereCode('EUR')->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); ++$this->queryCount; if (null === $euro) { throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.'); @@ -272,14 +286,13 @@ class ExchangeRateConverter $start->startOfDay(); $end->endOfDay(); Log::debug(sprintf('Preparing for %s to %s between %s and %s', $from->code, $to->code, $start->format('Y-m-d'), $end->format('Y-m-d'))); - $set = auth()->user() - ->currencyExchangeRates() - ->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ->where('date', '<=', $end->format('Y-m-d')) - ->where('date', '>=', $start->format('Y-m-d')) - ->orderBy('date', 'DESC')->get() - ; + $set = $this->userGroup + ->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', '<=', $end->format('Y-m-d')) + ->where('date', '>=', $start->format('Y-m-d')) + ->orderBy('date', 'DESC')->get(); ++$this->queryCount; if (0 === $set->count()) { Log::debug('No prepared rates found in this period, use the fallback'); @@ -293,10 +306,10 @@ class ExchangeRateConverter $this->isPrepared = true; // so there is a fallback just in case. Now loop the set of rates we DO have. - $temp = []; - $count = 0; + $temp = []; + $count = 0; foreach ($set as $rate) { - $date = $rate->date->format('Y-m-d'); + $date = $rate->date->format('Y-m-d'); $temp[$date] ??= [ $from->id => [ $to->id => $rate->rate, @@ -305,11 +318,11 @@ class ExchangeRateConverter ++$count; } Log::debug(sprintf('Found %d rates in this period.', $count)); - $currentStart = clone $start; + $currentStart = clone $start; while ($currentStart->lte($end)) { - $currentDate = $currentStart->format('Y-m-d'); + $currentDate = $currentStart->format('Y-m-d'); $this->prepared[$currentDate] ??= []; - $fallback = $temp[$currentDate][$from->id][$to->id] ?? $this->fallback[$from->id][$to->id] ?? '0'; + $fallback = $temp[$currentDate][$from->id][$to->id] ?? $this->fallback[$from->id][$to->id] ?? '0'; if (0 === count($this->prepared[$currentDate]) && 0 !== bccomp('0', $fallback)) { // fill from temp or fallback or from temp (see before) $this->prepared[$currentDate][$from->id][$to->id] = $fallback; diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index 738cd97142..e0fec6c262 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -54,7 +54,7 @@ trait ChartGeneration $cache->addProperty($accounts); $cache->addProperty($convertToNative); if ($cache->has()) { - return $cache->get(); + //return $cache->get(); } app('log')->debug('Regenerate chart.account.account-balance-chart from scratch.'); $locale = app('steam')->getLocale(); diff --git a/app/Support/Steam.php b/app/Support/Steam.php index babe606e75..c136007c53 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -62,7 +62,9 @@ class Steam $value = (string) ($transaction[$key] ?? '0'); $value = '' === $value ? '0' : $value; $sum = bcadd($sum, $value); + //Log::debug(sprintf('Add value from "%s": %s', $key, $value)); } + Log::debug(sprintf('Sum of "%s"-fields is %s', $key, $sum)); return $sum; } @@ -257,64 +259,95 @@ class Steam * THAT currency. * "native_balance" the balance according to the "native_amount" + "native_foreign_amount" fields. * "ABC" the balance in this particular currency code (may repeat for each found currency). + * + * Het maakt niet uit of de native currency wel of niet gelijk is aan de account currency. + * Optelsom zou hetzelfde moeten zijn. Als het EUR is en de rekening ook is native_amount 0. + * Zo niet is amount 0 en native_amount het bedrag. + * + * Eerst een som van alle transacties in de native currency. Alle EUR bij elkaar opgeteld. + * Om te weten wat er nog meer op de rekening gebeurt, pak alles waar currency niet EUR is, en de foreign ook niet, + * en tel native_amount erbij op. + * Daarna pak je alle transacties waar currency niet EUR is, en de foreign wel, en tel foreign_amount erbij op. + * + * Wil je niks weten van native currencies, pak je: + * + * Eerst een som van alle transacties gegroepeerd op currency. Einde. + * */ public function finalAccountBalance(Account $account, Carbon $date): array { + Log::debug(sprintf('Now in finalAccountBalance(#%d, "%s", "%s")', $account->id, $account->name, $date->format('Y-m-d H:i:s'))); $native = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $convertToNative = app('preferences')->getForUser($account->user, 'convert_to_native', false)->data; $accountCurrency = $this->getAccountCurrency($account); $hasCurrency = null !== $accountCurrency; - $currency = $accountCurrency ?? $native; + $currency = $hasCurrency ? $accountCurrency : $native; if (!$hasCurrency) { - Log::debug('Gave account fake currency.'); + //Log::debug('Pretend native currency is fake, because account has no currency.'); // fake currency - $native = new TransactionCurrency(); + //$native = new TransactionCurrency(); } - $return = [ - 'native_balance' => '0', - ]; - Log::debug(sprintf('Now in finalAccountBalance("%s", "%s")', $account->name, $date->format('Y-m-d H:i:s'))); + //unset($accountCurrency); + $return = []; + // first, the "balance", as described earlier. - $array = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->where('transactions.transaction_currency_id', $currency->id) - ->get(['transactions.amount'])->toArray(); - $return['balance'] = $this->sumTransactions($array, 'amount'); - //Log::debug(sprintf('balance is %s', $return['balance'])); - // add virtual balance: - $return['balance'] = bcadd('' === (string) $account->virtual_balance ? '0' : $account->virtual_balance, $return['balance']); - Log::debug(sprintf('balance is %s (with virtual balance)', $return['balance'])); + if ($convertToNative) { + // normal balance + $return['balance'] = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->where('transactions.transaction_currency_id', $native->id) + ->sum('transactions.amount'); + // plus virtual balance, if the account has a virtual_balance in the native currency + if($native->id === $accountCurrency?->id) { + $return['balance'] = bcadd('' === (string) $account->virtual_balance ? '0' : $account->virtual_balance, $return['balance']); + } + Log::debug(sprintf('balance is (%s only) %s (with virtual balance)', $native->code, $return['balance'])); - // then, native balance (if necessary( - if ($native->id !== $currency->id) { - Log::debug('Will grab native balance for transactions on this account.'); - $array = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->get(['transactions.native_amount'])->toArray(); - $return['native_balance'] = $this->sumTransactions($array, 'native_amount'); -// Log::debug(sprintf('native_balance is %s', $return['native_balance'])); + // native balance + $return['native_balance'] = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->sum('transactions.native_amount'); + // plus native virtual balance. $return['native_balance'] = bcadd('' === (string) $account->native_virtual_balance ? '0' : $account->native_virtual_balance, $return['native_balance']); - Log::debug(sprintf('native_balance is %s (with virtual balance)', $return['native_balance'])); + Log::debug(sprintf('native_balance is (all transactions to %s) %s (with virtual balance)', $native->code, $return['native_balance'])); + + // plus foreign transactions in THIS currency. + $sum = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->where('transactions.foreign_currency_id', $native->id) + ->sum('transactions.foreign_amount'); + $return['native_balance'] = bcadd($return['native_balance'], $sum); + + Log::debug(sprintf('Foreign amount transactions add (%s only) %s, total native_balance is now %s', $native->code, $sum, $return['native_balance'])); } - // balance(s) in other currencies. + // balance(s) in other (all) currencies. $array = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) ->get(['transaction_currencies.code', 'transactions.amount'])->toArray(); $others = $this->groupAndSumTransactions($array, 'code', 'amount'); - + Log::debug('All balances are (joined)', $others); // if the account has no own currency preference, drop balance in favor of native balance + if ($hasCurrency && !$convertToNative) { + $return['balance'] = $others[$currency->code] ?? null; + $return['native_balance'] = $others[$currency->code] ?? null; + Log::debug(sprintf('Set balance + native_balance to %s', $return['balance'])); + } if (!$hasCurrency) { Log::debug('Account has no currency preference, dropping balance in favor of native balance.'); - $return['native_balance'] = bcadd($return['balance'], $return['native_balance']); + $sum = bcadd($return['balance'], $return['native_balance']); + Log::debug(sprintf('%s + %s = %s', $return['balance'], $return['native_balance'], $sum)); + $return['native_balance'] = $sum; unset($return['balance']); } - Log::debug('All others are (joined)', $others); - return array_merge($return, $others); } diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 80db203ee9..e7d396808e 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -79,10 +79,6 @@ class General extends AbstractExtension if (!$useNative) { $strings[] = app('amount')->formatAnything($currency, $balance, false); } - if($useNative) { - $strings[] =sprintf('(%s)', app('amount')->formatAnything($currency, $balance, false)); - } - continue; } if ('native_balance' === $key) { @@ -94,7 +90,7 @@ class General extends AbstractExtension continue; } // for multi currency accounts. - if ($key !== $currency->code) { + if ($useNative && $key !== $default->code) { $strings[] = app('amount')->formatAnything(TransactionCurrency::where('code', $key)->first(), $balance, false); } }