diff --git a/app/Api/V1/Controllers/Chart/BalanceController.php b/app/Api/V1/Controllers/Chart/BalanceController.php new file mode 100644 index 0000000000..b9ed26cd35 --- /dev/null +++ b/app/Api/V1/Controllers/Chart/BalanceController.php @@ -0,0 +1,97 @@ +middleware( + function ($request, $next) { + $this->repository = app(AccountRepositoryInterface::class); + $this->collector = app(GroupCollectorInterface::class); + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); + $this->collector->setUserGroup($userGroup); + $this->chartData = new ChartData(); + // $this->default = app('amount')->getPrimaryCurrency(); + + return $next($request); + } + ); + } + + /** + * The code is practically a duplicate of ReportController::operations. + * + * Currency is up to the account/transactions in question, but conversion to the default + * currency is possible. + * + * If the transaction being processed is already in native currency OR if the + * foreign amount is in the native currency, the amount will not be converted. + * + * @throws FireflyException + */ + public function balance(ChartRequest $request): JsonResponse + { + $queryParameters = $request->getParameters(); + $accounts = $this->getAccountList($queryParameters); + + // prepare for currency conversion and data collection: + /** @var TransactionCurrency $primary */ + $primary = Amount::getPrimaryCurrency(); + + // get journals for entire period: + + $this->collector->setRange($queryParameters['start'], $queryParameters['end']) + ->withAccountInformation() + ->setXorAccounts($accounts) + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::RECONCILIATION->value, TransactionTypeEnum::TRANSFER->value]); + $journals = $this->collector->getExtractedJournals(); + + $object = new AccountBalanceGrouped(); + $object->setPreferredRange($queryParameters['period']); + $object->setPrimary($primary); + $object->setAccounts($accounts); + $object->setJournals($journals); + $object->setStart($queryParameters['start']); + $object->setEnd($queryParameters['end']); + $object->groupByCurrencyAndPeriod(); + $data = $object->convertToChartData(); + foreach ($data as $entry) { + $this->chartData->add($entry); + } + + return response()->json($this->chartData->render()); + } +} diff --git a/app/Console/Commands/Correction/CorrectsPrimaryCurrencyAmounts.php b/app/Console/Commands/Correction/CorrectsPrimaryCurrencyAmounts.php index f4544f8aa8..563cd6c08f 100644 --- a/app/Console/Commands/Correction/CorrectsPrimaryCurrencyAmounts.php +++ b/app/Console/Commands/Correction/CorrectsPrimaryCurrencyAmounts.php @@ -40,6 +40,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\UserGroup; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use Illuminate\Console\Command; @@ -72,6 +73,8 @@ class CorrectsPrimaryCurrencyAmounts extends Command /** @var UserGroupRepositoryInterface $repository */ $repository = app(UserGroupRepositoryInterface::class); + Preferences::mark(); + /** @var UserGroup $userGroup */ foreach ($repository->getAll() as $userGroup) { $this->recalculateForGroup($userGroup); @@ -87,8 +90,7 @@ class CorrectsPrimaryCurrencyAmounts extends Command $this->recalculateAccounts($userGroup); // do a check with the group's currency so we can skip some stuff. - Preferences::mark(); - $currency = app('amount')->getPrimaryCurrencyByUserGroup($userGroup); + $currency = Amount::getPrimaryCurrencyByUserGroup($userGroup); $this->recalculatePiggyBanks($userGroup, $currency); $this->recalculateBudgets($userGroup, $currency); diff --git a/app/Events/Preferences/UserGroupChangedPrimaryCurrency.php b/app/Events/Preferences/UserGroupChangedPrimaryCurrency.php index f39b72b2fc..4f6fc7fbb2 100644 --- a/app/Events/Preferences/UserGroupChangedPrimaryCurrency.php +++ b/app/Events/Preferences/UserGroupChangedPrimaryCurrency.php @@ -35,6 +35,6 @@ class UserGroupChangedPrimaryCurrency extends Event public function __construct(public UserGroup $userGroup) { - Log::debug('User group changed default currency.'); + Log::debug('User group changed primary currency.'); } } diff --git a/app/Handlers/Events/PreferencesEventHandler.php b/app/Handlers/Events/PreferencesEventHandler.php index c510844e45..f816f890f9 100644 --- a/app/Handlers/Events/PreferencesEventHandler.php +++ b/app/Handlers/Events/PreferencesEventHandler.php @@ -73,6 +73,7 @@ class PreferencesEventHandler $repository = app(PiggyBankRepositoryInterface::class); $repository->setUserGroup($userGroup); $piggyBanks = $repository->getPiggyBanks(); + Log::debug(sprintf('Resetting %d piggy bank(s).', $piggyBanks->count())); /** @var PiggyBank $piggyBank */ foreach ($piggyBanks as $piggyBank) { @@ -104,6 +105,8 @@ class PreferencesEventHandler $repository->setUserGroup($userGroup); $set = $repository->getBudgets(); + Log::debug(sprintf('Resetting %d budget(s).', $set->count())); + /** @var Budget $budget */ foreach ($set as $budget) { foreach ($budget->autoBudgets as $autoBudget) { diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 2a24db8cee..7d9010c80c 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -35,6 +35,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\AugumentData; use FireflyIII\Support\Http\Controllers\ChartGeneration; @@ -42,7 +43,6 @@ use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; - use function Safe\json_encode; /** @@ -78,6 +78,7 @@ class AccountController extends Controller /** * Shows the balances for all the user's expense accounts (on the front page). + * 2025-08-06 validated for multi (primary) currency * * This chart is (multi) currency aware. */ @@ -86,14 +87,14 @@ class AccountController extends Controller Log::debug('ExpenseAccounts'); /** @var Carbon $start */ - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); $start->startOfDay(); $end->endOfDay(); - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -103,19 +104,19 @@ class AccountController extends Controller } // prep some vars: - $currencies = []; - $chartData = []; - $tempData = []; + $currencies = []; + $chartData = []; + $tempData = []; // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::EXPENSE->value]); - $accountNames = $this->extractNames($accounts); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::EXPENSE->value]); + $accountNames = $this->extractNames($accounts); // grab all balances Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s'))); Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s'))); - $startBalances = Steam::finalAccountsBalance($accounts, $start); - $endBalances = Steam::finalAccountsBalance($accounts, $end); + $startBalances = Steam::finalAccountsBalance($accounts, $start, $this->primaryCurrency, $this->convertToPrimary); + $endBalances = Steam::finalAccountsBalance($accounts, $end, $this->primaryCurrency, $this->convertToPrimary); // loop the accounts, then check for balance and currency info. foreach ($accounts as $account) { @@ -143,21 +144,21 @@ class AccountController extends Controller continue; } // Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); - $searchCode = $this->convertToPrimary ? $this->primaryCurrency->code : $key; - $searchCode = 'balance' === $searchCode || 'pc_balance' === $searchCode ? $this->primaryCurrency->code : $searchCode; + $searchCode = $this->convertToPrimary ? $this->primaryCurrency->code : $key; + $searchCode = 'balance' === $searchCode || 'pc_balance' === $searchCode ? $this->primaryCurrency->code : $searchCode; // Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. $startBalance = ($startBalances[$account->id][$key] ?? '0'); // Log::debug(sprintf('Start balance is %s', $startBalance)); - $diff = bcsub($endBalance, $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[$account->id], 'difference' => $diff, - 'diff_float' => (float) $diff, // intentional float + 'diff_float' => (float)$diff, // intentional float 'currency_id' => $currencies[$searchCode]->id, ]; } @@ -168,26 +169,26 @@ class AccountController extends Controller foreach ($currencies as $currency) { $newCurrencies[$currency->id] = $currency; } - $currencies = $newCurrencies; + $currencies = $newCurrencies; // sort temp array by amount. - $amounts = array_column($tempData, 'diff_float'); + $amounts = array_column($tempData, 'diff_float'); array_multisort($amounts, SORT_DESC, $tempData); // loop all found currencies and build the data array for the chart. /** - * @var int $currencyId + * @var int $currencyId * @var TransactionCurrency $currency */ foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string) trans('firefly.spent'), - 'type' => 'bar', - 'currency_symbol' => $currency->symbol, - 'currency_code' => $currency->code, - 'entries' => $this->expandNames($tempData), - ]; + 'label' => (string)trans('firefly.spent'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, + 'entries' => $this->expandNames($tempData), + ]; $chartData[$currencyId] = $dataSet; } @@ -195,10 +196,10 @@ class AccountController extends Controller foreach ($tempData as $entry) { $currencyId = $entry['currency_id']; $name = $entry['name']; - $chartData[$currencyId]['entries'][$name] = (float) $entry['difference']; + $chartData[$currencyId]['entries'][$name] = (float)$entry['difference']; } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -206,6 +207,7 @@ class AccountController extends Controller /** * Expenses per budget for all time, as shown on account overview. + * */ public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account): JsonResponse { @@ -220,9 +222,10 @@ class AccountController extends Controller */ public function expenseBudget(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); + $cache->addProperty($this->convertToPrimary); $cache->addProperty($end); $cache->addProperty('chart.account.expense-budget'); if ($cache->has()) { @@ -231,7 +234,9 @@ class AccountController extends Controller /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); - $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withBudgetInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $collector->setAccounts(new Collection([$account])) + ->setRange($start, $end) + ->withBudgetInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); $journals = $collector->getExtractedJournals(); $chartData = []; $result = []; @@ -239,31 +244,49 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $budgetId = (int) $journal['budget_id']; - $key = sprintf('%d-%d', $budgetId, $journal['currency_id']); - $budgetIds[] = $budgetId; + $budgetId = (int)$journal['budget_id']; + $key = sprintf('%d-%d', $budgetId, $journal['currency_id']); + $budgetIds[] = $budgetId; + + // currency info: + $currencyId = (int)$journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + $field = 'amount'; + if ($this->convertToPrimary && $this->primaryCurrency->id !== $currencyId) { + $field = 'pc_amount'; + $currencyName = $this->primaryCurrency->name; + $currencySymbol = $this->primaryCurrency->symbol; + $currencyCode = $this->primaryCurrency->code; + $currencyDecimalPlaces = $this->primaryCurrency->decimal_places; + + } + if (!array_key_exists($key, $result)) { $result[$key] = [ - 'total' => '0', - 'budget_id' => $budgetId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], + 'total' => '0', + 'budget_id' => $budgetId, + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + 'currency_decimal_places' => $currencyDecimalPlaces, ]; } - $result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']); + $result[$key]['total'] = bcadd((string)$journal[$field], $result[$key]['total']); } - $names = $this->getBudgetNames($budgetIds); + $names = $this->getBudgetNames($budgetIds); foreach ($result as $row) { $budgetId = $row['budget_id']; $name = $names[$budgetId]; - $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -285,13 +308,14 @@ class AccountController extends Controller */ public function expenseCategory(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToPrimary); $cache->addProperty('chart.account.expense-category'); if ($cache->has()) { - return response()->json($cache->get()); + return response()->json($cache->get()); } /** @var GroupCollectorInterface $collector */ @@ -303,28 +327,45 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); + $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); if (!array_key_exists($key, $result)) { + + // currency info: + $currencyId = (int)$journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + $field = 'amount'; + if ($this->convertToPrimary && $this->primaryCurrency->id !== $currencyId) { + $field = 'pc_amount'; + $currencyName = $this->primaryCurrency->name; + $currencySymbol = $this->primaryCurrency->symbol; + $currencyCode = $this->primaryCurrency->code; + $currencyDecimalPlaces = $this->primaryCurrency->decimal_places; + } + $result[$key] = [ - 'total' => '0', - 'category_id' => (int) $journal['category_id'], - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], + 'total' => '0', + 'category_id' => (int)$journal['category_id'], + 'currency_name' => $currencyName, + 'currency_code' => $currencyCode, + 'currency_symbol' => $currencySymbol, + 'currency_decimal_places' => $currencyDecimalPlaces, ]; } - $result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']); + $result[$key]['total'] = bcadd((string)$journal[$field], $result[$key]['total']); } - $names = $this->getCategoryNames(array_keys($result)); + $names = $this->getCategoryNames(array_keys($result)); foreach ($result as $row) { $categoryId = $row['category_id']; $name = $names[$categoryId] ?? '(unknown)'; - $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -337,18 +378,18 @@ class AccountController extends Controller * */ public function frontpage(AccountRepositoryInterface $repository): JsonResponse { - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); // Log::debug('Default set is ', $defaultSet); - $frontpage = app('preferences')->get('frontpageAccounts', $defaultSet); + $frontpage = Preferences::get('frontpageAccounts', $defaultSet); $frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data; Log::debug('Frontpage preference set is ', $frontpageArray); if (0 === count($frontpageArray)) { - app('preferences')->set('frontpageAccounts', $defaultSet); + Preferences::set('frontpageAccounts', $defaultSet); Log::debug('frontpage set is empty!'); } - $accounts = $repository->getAccountsById($frontpageArray); + $accounts = $repository->getAccountsById($frontpageArray); // move to end of day for $end. $end->endOfDay(); @@ -372,7 +413,7 @@ class AccountController extends Controller */ public function incomeCategory(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); @@ -392,7 +433,7 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); + $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); if (!array_key_exists($key, $result)) { $result[$key] = [ 'total' => '0', @@ -402,17 +443,17 @@ class AccountController extends Controller 'currency_code' => $journal['currency_code'], ]; } - $result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']); + $result[$key]['total'] = bcadd((string)$journal['amount'], $result[$key]['total']); } - $names = $this->getCategoryNames(array_keys($result)); + $names = $this->getCategoryNames(array_keys($result)); foreach ($result as $row) { $categoryId = $row['category_id']; $name = $names[$categoryId] ?? '(unknown)'; - $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -429,7 +470,7 @@ class AccountController extends Controller $end->endOfDay(); // TODO not sure if these date ranges will work as expected. Log::debug(sprintf('Now in period("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty('chart.account.period'); $cache->addProperty($start); $cache->addProperty($end); @@ -440,21 +481,21 @@ class AccountController extends Controller } // collect and filter balances for the entire period. - $step = $this->calculateStep($start, $end); + $step = $this->calculateStep($start, $end); Log::debug(sprintf('Step is %s', $step)); - $locale = Steam::getLocale(); - $return = []; + $locale = Steam::getLocale(); + $return = []; // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041 // have to make sure this chart is always based on the balance at the END of the period. // This period depends on the size of the chart $current = clone $start; $current = app('navigation')->endOfX($current, $step, null); - $format = (string) trans('config.month_and_day_js', [], $locale); + $format = (string)trans('config.month_and_day_js', [], $locale); $accountCurrency = $this->accountRepository->getAccountCurrency($account); - $range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToPrimary); - $range = Steam::filterAccountBalances($range, $account, $this->convertToPrimary, $accountCurrency); + $range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToPrimary); + $range = Steam::filterAccountBalances($range, $account, $this->convertToPrimary, $accountCurrency); // temp, get end balance. Log::debug(sprintf('period: Call finalAccountBalance with date/time "%s"', $end->toIso8601String())); @@ -465,14 +506,14 @@ class AccountController extends Controller $accountCurrency ??= $this->primaryCurrency; // do this AFTER getting the balances. Log::debug('Start chart loop.'); - $newRange = []; - $expectedIndex = 0; + $newRange = []; + $expectedIndex = 0; Log::debug('Balances exist at:'); foreach ($range as $key => $value) { $newRange[] = ['date' => $key, 'info' => $value]; Log::debug(sprintf('%d - %s (%s)', count($newRange) - 1, $key, json_encode($value))); } - $carbon = Carbon::createFromFormat('Y-m-d', $newRange[0]['date'])->endOfDay(); + $carbon = Carbon::createFromFormat('Y-m-d', $newRange[0]['date'])->endOfDay(); Log::debug(sprintf('Start of loop, $carbon is %s', $carbon->format('Y-m-d H:i:s'))); while ($end->gte($current)) { $momentBalance = $previous; @@ -493,26 +534,26 @@ class AccountController extends Controller } } Log::debug(sprintf('momentBalance is now %s', json_encode($momentBalance))); - $return = $this->updateChartKeys($return, $momentBalance); - $previous = $momentBalance; + $return = $this->updateChartKeys($return, $momentBalance); + $previous = $momentBalance; // process each balance thing. foreach ($momentBalance as $key => $amount) { $label = $current->isoFormat($format); $return[$key]['entries'][$label] = $amount; } - $current = app('navigation')->addPeriod($current, $step, 0); + $current = app('navigation')->addPeriod($current, $step, 0); // here too, to fix #8041, the data is corrected to the end of the period. - $current = app('navigation')->endOfX($current, $step, null); + $current = app('navigation')->endOfX($current, $step, null); } Log::debug('End of chart loop.'); // second loop (yes) to create nice array with info! Yay! - $chartData = []; + $chartData = []; foreach ($return as $key => $info) { if ('balance' !== $key && 'pc_balance' !== $key) { // assume it's a currency: - $setCurrency = $this->currencyRepository->findByCode((string) $key); + $setCurrency = $this->currencyRepository->findByCode((string)$key); $info['currency_symbol'] = $setCurrency->symbol; $info['currency_code'] = $setCurrency->code; $info['label'] = sprintf('%s (%s)', $account->name, $setCurrency->symbol); @@ -525,12 +566,12 @@ class AccountController extends Controller if ('pc_balance' === $key) { $info['currency_symbol'] = $this->primaryCurrency->symbol; $info['currency_code'] = $this->primaryCurrency->code; - $info['label'] = sprintf('%s (%s) (%s)', $account->name, (string) trans('firefly.sum'), $this->primaryCurrency->symbol); + $info['label'] = sprintf('%s (%s) (%s)', $account->name, (string)trans('firefly.sum'), $this->primaryCurrency->symbol); } $chartData[] = $info; } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -567,15 +608,15 @@ class AccountController extends Controller public function revenueAccounts(): JsonResponse { /** @var Carbon $start */ - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); $start->startOfDay(); $end->endOfDay(); - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -585,13 +626,13 @@ class AccountController extends Controller } // prep some vars: - $currencies = []; - $chartData = []; - $tempData = []; + $currencies = []; + $chartData = []; + $tempData = []; // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); - $accountNames = $this->extractNames($accounts); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); + $accountNames = $this->extractNames($accounts); // grab all balances Log::debug(sprintf('revAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s'))); @@ -626,21 +667,21 @@ class AccountController extends Controller continue; } // Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); - $searchCode = $this->convertToPrimary ? $this->primaryCurrency->code : $key; - $searchCode = 'balance' === $searchCode || 'pc_balance' === $searchCode ? $this->primaryCurrency->code : $searchCode; + $searchCode = $this->convertToPrimary ? $this->primaryCurrency->code : $key; + $searchCode = 'balance' === $searchCode || 'pc_balance' === $searchCode ? $this->primaryCurrency->code : $searchCode; // Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. $startBalance = ($startBalances[$account->id][$key] ?? '0'); // Log::debug(sprintf('Start balance is %s', $startBalance)); - $diff = bcsub($endBalance, $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[$account->id], 'difference' => $diff, - 'diff_float' => (float) $diff, // intentional float + 'diff_float' => (float)$diff, // intentional float 'currency_id' => $currencies[$searchCode]->id, ]; } @@ -653,26 +694,26 @@ class AccountController extends Controller foreach ($currencies as $currency) { $newCurrencies[$currency->id] = $currency; } - $currencies = $newCurrencies; + $currencies = $newCurrencies; // sort temp array by amount. - $amounts = array_column($tempData, 'diff_float'); + $amounts = array_column($tempData, 'diff_float'); array_multisort($amounts, SORT_ASC, $tempData); // loop all found currencies and build the data array for the chart. /** - * @var int $currencyId + * @var int $currencyId * @var TransactionCurrency $currency */ foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string) trans('firefly.earned'), - 'type' => 'bar', - 'currency_symbol' => $currency->symbol, - 'currency_code' => $currency->code, - 'entries' => $this->expandNames($tempData), - ]; + 'label' => (string)trans('firefly.earned'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, + 'entries' => $this->expandNames($tempData), + ]; $chartData[$currencyId] = $dataSet; } @@ -683,7 +724,7 @@ class AccountController extends Controller $chartData[$currencyId]['entries'][$name] = bcmul($entry['difference'], '-1'); } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index 84f1d18732..71a09fd706 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; +use FireflyIII\Support\Singleton\PreferencesSingleton; use JsonException; use Carbon\Carbon; use FireflyIII\Enums\AccountTypeEnum; @@ -269,7 +270,9 @@ class PreferencesController extends Controller if ($convertToPrimary && !$this->convertToPrimary) { // set to true! Log::debug('User sets convertToPrimary to true.'); - Preferences::set('convert_to_primary', $convertToPrimary); + Preferences::set('convert_to_primary', true); + $singleton = PreferencesSingleton::getInstance(); + $singleton->resetPreferences(); event(new UserGroupChangedPrimaryCurrency(auth()->user()->userGroup)); } Preferences::set('convert_to_primary', $convertToPrimary); diff --git a/app/Http/Controllers/Transaction/ShowController.php b/app/Http/Controllers/Transaction/ShowController.php index f9eaa1a490..bb6c60af1a 100644 --- a/app/Http/Controllers/Transaction/ShowController.php +++ b/app/Http/Controllers/Transaction/ShowController.php @@ -25,16 +25,20 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Transaction; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface; use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; +use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment; use FireflyIII\Transformers\TransactionGroupTransformer; +use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\JsonResponse; use Illuminate\View\View; use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class ShowController @@ -57,7 +61,7 @@ class ShowController extends Controller $this->repository = app(TransactionGroupRepositoryInterface::class); $this->aleRepository = app(ALERepositoryInterface::class); - app('view')->share('title', (string) trans('firefly.transactions')); + app('view')->share('title', (string)trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); return $next($request); @@ -80,43 +84,67 @@ class ShowController extends Controller */ public function show(TransactionGroup $transactionGroup) { + /** @var User $admin */ + $admin = auth()->user(); + + // use new group collector: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($admin)->setTransactionGroup($transactionGroup)->withAPIInformation(); + + $selectedGroup = $collector->getGroups()->first(); + if (null === $selectedGroup) { + throw new NotFoundHttpException(); + } + + // enrich + $enrichment = new TransactionGroupEnrichment(); + $enrichment->setUser($admin); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); + + /** @var null|TransactionJournal $first */ - $first = $transactionGroup->transactionJournals()->first(['transaction_journals.*']); - $splits = $transactionGroup->transactionJournals()->count(); + $first = $transactionGroup->transactionJournals()->first(['transaction_journals.*']); + $splits = $transactionGroup->transactionJournals()->count(); + $splits = count($selectedGroup['transactions']); + $keys = array_keys($selectedGroup['transactions']); + $first = $selectedGroup['transactions'][array_shift($keys)]; + unset($keys); if (null === $first) { throw new FireflyException('This transaction is broken :(.'); } + $type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type'])); + $title = 1 === $splits ? $first['description'] : $selectedGroup['title']; + $subTitle = sprintf('%s: "%s"', $type, $title); - $type = (string) trans(sprintf('firefly.%s', $first->transactionType->type)); - $title = 1 === $splits ? $first->description : $transactionGroup->title; - $subTitle = sprintf('%s: "%s"', $type, $title); + // enrich + $enrichment = new TransactionGroupEnrichment(); + $enrichment->setUser($admin); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters(new ParameterBag()); - $groupArray = $transformer->transformObject($transactionGroup); + $groupArray = $transformer->transformObject($transactionGroup); // do some calculations: - $amounts = $this->getAmounts($groupArray); - $accounts = $this->getAccounts($groupArray); + $amounts = $this->getAmounts($selectedGroup); + $accounts = $this->getAccounts($selectedGroup); - foreach (array_keys($groupArray['transactions']) as $index) { - $groupArray['transactions'][$index]['tags'] = $this->repository->getTagObjects( - (int) $groupArray['transactions'][$index]['transaction_journal_id'] - ); + foreach (array_keys($selectedGroup['transactions']) as $index) { + $selectedGroup['transactions'][$index]['tags'] = $this->repository->getTagObjects((int)$selectedGroup['transactions'][$index]['transaction_journal_id']); } - // get audit log entries: $groupLogEntries = $this->aleRepository->getForObject($transactionGroup); $logEntries = []; - foreach ($transactionGroup->transactionJournals as $journal) { - $logEntries[$journal->id] = $this->aleRepository->getForObject($journal); + foreach ($selectedGroup['transactions'] as $journal) { + $logEntries[$journal['transaction_journal_id']] = $this->aleRepository->getForId(TransactionJournal::class, $journal['transaction_journal_id']); } - $events = $this->repository->getPiggyEvents($transactionGroup); - $attachments = $this->repository->getAttachments($transactionGroup); - $links = $this->repository->getLinks($transactionGroup); + $events = $this->repository->getPiggyEvents($transactionGroup); + $attachments = $this->repository->getAttachments($transactionGroup); + $links = $this->repository->getLinks($transactionGroup); return view( 'transactions.show', @@ -129,6 +157,7 @@ class ShowController extends Controller 'groupLogEntries', 'subTitle', 'splits', + 'selectedGroup', 'groupArray', 'events', 'attachments', @@ -142,34 +171,38 @@ class ShowController extends Controller { $amounts = []; foreach ($group['transactions'] as $transaction) { + // add normal amount: $symbol = $transaction['currency_symbol']; - if (!array_key_exists($symbol, $amounts)) { - $amounts[$symbol] = [ - 'amount' => '0', - 'symbol' => $symbol, - 'decimal_places' => $transaction['currency_decimal_places'], - ]; - } - $amounts[$symbol]['amount'] = bcadd($amounts[$symbol]['amount'], (string) $transaction['amount']); - if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] - && 0 !== bccomp( - '0', - (string) $transaction['foreign_amount'] - )) { + $amounts[$symbol] ??= [ + 'amount' => '0', + 'symbol' => $symbol, + 'decimal_places' => $transaction['currency_decimal_places'], + ]; + $amounts[$symbol]['amount'] = bcadd($amounts[$symbol]['amount'], (string)$transaction['amount']); + + // add foreign amount: + if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] && 0 !== bccomp('0', (string)$transaction['foreign_amount'])) { // same for foreign currency: $foreignSymbol = $transaction['foreign_currency_symbol']; - if (!array_key_exists($foreignSymbol, $amounts)) { - $amounts[$foreignSymbol] = [ - 'amount' => '0', - 'symbol' => $foreignSymbol, - 'decimal_places' => $transaction['foreign_currency_decimal_places'], - ]; - } - $amounts[$foreignSymbol]['amount'] = bcadd( - $amounts[$foreignSymbol]['amount'], - (string) $transaction['foreign_amount'] - ); + $amounts[$foreignSymbol] ??= [ + 'amount' => '0', + 'symbol' => $foreignSymbol, + 'decimal_places' => $transaction['foreign_currency_decimal_places'], + ]; + $amounts[$foreignSymbol]['amount'] = bcadd($amounts[$foreignSymbol]['amount'], (string)$transaction['foreign_amount']); } + // add primary currency amount + if (null !== $transaction['pc_amount'] && $transaction['currency_id'] !== $this->primaryCurrency->id) { + // same for foreign currency: + $primarySymbol = $this->primaryCurrency->symbol; + $amounts[$primarySymbol] ??= [ + 'amount' => '0', + 'symbol' => $this->primaryCurrency->symbol, + 'decimal_places' => $this->primaryCurrency->decimal_places, + ]; + $amounts[$primarySymbol]['amount'] = bcadd($amounts[$primarySymbol]['amount'], (string)$transaction['pc_amount']); + } + } return $amounts; @@ -177,23 +210,23 @@ class ShowController extends Controller private function getAccounts(array $group): array { - $accounts = [ + $accounts = [ 'source' => [], 'destination' => [], ]; foreach ($group['transactions'] as $transaction) { $accounts['source'][] = [ - 'type' => $transaction['source_type'], - 'id' => $transaction['source_id'], - 'name' => $transaction['source_name'], - 'iban' => $transaction['source_iban'], + 'type' => $transaction['source_account_type'], + 'id' => $transaction['source_account_id'], + 'name' => $transaction['source_account_name'], + 'iban' => $transaction['source_account_iban'], ]; $accounts['destination'][] = [ - 'type' => $transaction['destination_type'], - 'id' => $transaction['destination_id'], - 'name' => $transaction['destination_name'], - 'iban' => $transaction['destination_iban'], + 'type' => $transaction['destination_account_type'], + 'id' => $transaction['destination_account_id'], + 'name' => $transaction['destination_account_name'], + 'iban' => $transaction['destination_account_iban'], ]; } diff --git a/app/Repositories/AuditLogEntry/ALERepository.php b/app/Repositories/AuditLogEntry/ALERepository.php index 97cc76def6..50e4adaf08 100644 --- a/app/Repositories/AuditLogEntry/ALERepository.php +++ b/app/Repositories/AuditLogEntry/ALERepository.php @@ -52,4 +52,10 @@ class ALERepository implements ALERepositoryInterface return $auditLogEntry; } + + public function getForId(string $model, int $modelId): Collection + { + // all Models have an ID. + return AuditLogEntry::where('auditable_id', $modelId)->where('auditable_type', $model)->get(); + } } diff --git a/app/Repositories/AuditLogEntry/ALERepositoryInterface.php b/app/Repositories/AuditLogEntry/ALERepositoryInterface.php index ff4353d5ca..389450e97a 100644 --- a/app/Repositories/AuditLogEntry/ALERepositoryInterface.php +++ b/app/Repositories/AuditLogEntry/ALERepositoryInterface.php @@ -45,6 +45,8 @@ use Illuminate\Support\Collection; interface ALERepositoryInterface { public function getForObject(Model $model): Collection; + public function getForId(string $model, int $modelId): Collection; + public function store(array $data): AuditLogEntry; } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index e7a7d41ecf..ed834e8f54 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -117,18 +117,23 @@ class Amount public function convertToPrimary(?User $user = null): bool { $instance = PreferencesSingleton::getInstance(); - $pref = $instance->getPreference('convert_to_primary'); - if (null === $pref) { - if (!$user instanceof User) { + if (!$user instanceof User) { + $pref = $instance->getPreference('convert_to_primary_no_user'); + if (null === $pref) { $res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled'); - $instance->setPreference('convert_to_primary', $res); + $instance->setPreference('convert_to_primary_no_user', $res); return $res; } - + return $pref; + } + $key = sprintf('convert_to_primary_%d', $user->id); + $pref = $instance->getPreference($key); + if(null === $pref) { $res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled'); - $instance->setPreference('convert_to_primary', $res); + $instance->setPreference($key, $res); return $res; } + return $pref; } diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index 3f2b3d43cd..246e7ecc9a 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -56,7 +56,7 @@ trait ChartGeneration $cache->addProperty($accounts); $cache->addProperty($convertToPrimary); if ($cache->has()) { - return $cache->get(); + return $cache->get(); } Log::debug('Regenerate chart.account.account-balance-chart from scratch.'); $locale = app('steam')->getLocale(); @@ -67,7 +67,7 @@ trait ChartGeneration /** @var AccountRepositoryInterface $accountRepos */ $accountRepos = app(AccountRepositoryInterface::class); - $default = app('amount')->getPrimaryCurrency(); + $primary = app('amount')->getPrimaryCurrency(); $chartData = []; Log::debug(sprintf('Start of accountBalanceChart(list, %s, %s)', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); @@ -75,10 +75,10 @@ trait ChartGeneration /** @var Account $account */ foreach ($accounts as $account) { Log::debug(sprintf('Now at account #%d ("%s)', $account->id, $account->name)); - $currency = $accountRepos->getAccountCurrency($account) ?? $default; - $usePrimary = $convertToPrimary && $default->id !== $currency->id; + $currency = $accountRepos->getAccountCurrency($account) ?? $primary; + $usePrimary = $convertToPrimary && $primary->id !== $currency->id; $field = $convertToPrimary ? 'pc_balance' : 'balance'; - $currency = $usePrimary ? $default : $currency; + $currency = $usePrimary ? $primary : $currency; Log::debug(sprintf('Will use field %s', $field)); $currentSet = [ 'label' => $account->name, diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index 6d670aec84..21f81d1492 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -22,6 +22,10 @@ class PreferencesSingleton return self::$instance; } + public function resetPreferences(): void { + $this->preferences = []; + } + public function setPreference(string $key, mixed $value): void { $this->preferences[$key] = $value; diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 6e38065006..cb0c6c1f3a 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -64,7 +64,7 @@
{{ trans_choice('firefly.source_accounts', accounts['source']|length ) }} @@ -134,7 +134,7 @@ | ||||||||
{{ trans_choice('firefly.destination_accounts', accounts['destination']|length ) }} @@ -159,17 +159,17 @@ | {{ 'total_amount'|_ }} |
{% for amount in amounts %}
- {% if first.transactiontype.type == 'Withdrawal' %}
+ {% if first.transaction_type_type == 'Withdrawal' %}
{{ formatAmountBySymbol(amount.amount*-1,amount.symbol, amount.decimal_places) }}{% if loop.index0 != amounts|length -1 %}, {% endif %}
- {% elseif first.transactiontype.type == 'Deposit' %}
+ {% elseif first.transaction_type_type == 'Deposit' %}
{{ formatAmountBySymbol(amount.amount,amount.symbol, amount.decimal_places) }}{% if loop.index0 != amounts|length -1 %}, {% endif %}
- {% elseif first.transactiontype.type == 'Transfer' %}
+ {% elseif first.transaction_type_type == 'Transfer' %}
{{ formatAmountBySymbol(amount.amount, amount.symbol, amount.decimal_places, false) }}{% if loop.index0 != amounts|length -1 %}, {% endif %}
- {% elseif first.transactiontype.type == 'Opening balance' %}
+ {% elseif first.transaction_type_type == 'Opening balance' %}
{# Opening balance stored amount is always negative: find out which way the money goes #}
- {% if groupArray.transactions[0].source_type == 'Initial balance account' %}
+ {% if groupArray.transactions[0].source_account_type == 'Initial balance account' %}
{{ formatAmountBySymbol(amount.amount*-1,amount.symbol, amount.decimal_places) }}
{% else %}
{{ formatAmountBySymbol(amount.amount,amount.symbol, amount.decimal_places) }}
@@ -202,7 +202,7 @@
{% set boxSize = 4 %}
{% endif %}
- {% for index,journal in groupArray.transactions %}
+ {% for index,journal in selectedGroup.transactions %}
@@ -289,48 +289,74 @@
|