From 8805bcf6f66294ba8cf0a877017261f84b54c08b Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 23 Dec 2024 17:32:15 +0100 Subject: [PATCH] Various updates to display native/foreign amounts. --- .../Controllers/Summary/BasicController.php | 64 +++---- .../Correction/RecalculateNativeAmounts.php | 16 +- app/Handlers/Observer/TransactionObserver.php | 2 +- app/Http/Controllers/Json/BoxController.php | 158 ++++------------- app/Models/Transaction.php | 2 + app/Repositories/Bill/BillRepository.php | 161 +++++++++--------- .../Budget/AvailableBudgetRepository.php | 64 +++---- .../Budget/OperationsRepository.php | 102 ++++++----- 8 files changed, 253 insertions(+), 316 deletions(-) diff --git a/app/Api/V1/Controllers/Summary/BasicController.php b/app/Api/V1/Controllers/Summary/BasicController.php index a0d874c2b4..74a71fd4c2 100644 --- a/app/Api/V1/Controllers/Summary/BasicController.php +++ b/app/Api/V1/Controllers/Summary/BasicController.php @@ -40,6 +40,7 @@ use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\User; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; /** * Class BasicController @@ -91,24 +92,24 @@ class BasicController extends Controller public function basic(DateRequest $request): JsonResponse { // parameters for boxes: - $dates = $request->getAll(); - $start = $dates['start']; - $end = $dates['end']; - $code = $request->get('currency_code'); + $dates = $request->getAll(); + $start = $dates['start']; + $end = $dates['end']; + $code = $request->get('currency_code'); // balance information: $balanceData = $this->getBalanceInformation($start, $end); $billData = $this->getBillInformation($start, $end); $spentData = $this->getLeftToSpendInfo($start, $end); $netWorthData = $this->getNetWorthInfo($start, $end); - // $balanceData = []; - // $billData = []; - // $spentData = []; - // $netWorthData = []; + $balanceData = []; + $billData = []; +// $spentData = []; + $netWorthData = []; $total = array_merge($balanceData, $billData, $spentData, $netWorthData); // give new keys - $return = []; + $return = []; foreach ($total as $entry) { if (null === $code || ($code === $entry['currency_code'])) { $return[$entry['key']] = $entry; @@ -121,17 +122,17 @@ class BasicController extends Controller private function getBalanceInformation(Carbon $start, Carbon $end): array { // prep some arrays: - $incomes = []; - $expenses = []; - $sums = []; - $return = []; + $incomes = []; + $expenses = []; + $sums = []; + $return = []; // collect income of user using the new group collector. /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->setPage($this->parameters->get('page'))->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $set = $collector->getExtractedJournals(); + $set = $collector->getExtractedJournals(); /** @var array $transactionJournal */ foreach ($set as $transactionJournal) { @@ -149,7 +150,7 @@ class BasicController extends Controller /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->setPage($this->parameters->get('page'))->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $set = $collector->getExtractedJournals(); + $set = $collector->getExtractedJournals(); /** @var array $transactionJournal */ foreach ($set as $transactionJournal) { @@ -161,7 +162,7 @@ class BasicController extends Controller } // format amounts: - $keys = array_keys($sums); + $keys = array_keys($sums); foreach ($keys as $currencyId) { $currency = $this->currencyRepos->find($currencyId); if (null === $currency) { @@ -178,8 +179,8 @@ class BasicController extends Controller 'currency_decimal_places' => $currency->decimal_places, 'value_parsed' => app('amount')->formatAnything($currency, $sums[$currencyId] ?? '0', false), 'local_icon' => 'balance-scale', - 'sub_title' => app('amount')->formatAnything($currency, $expenses[$currencyId] ?? '0', false). - ' + '.app('amount')->formatAnything($currency, $incomes[$currencyId] ?? '0', false), + 'sub_title' => app('amount')->formatAnything($currency, $expenses[$currencyId] ?? '0', false) . + ' + ' . app('amount')->formatAnything($currency, $incomes[$currencyId] ?? '0', false), ]; $return[] = [ 'key' => sprintf('spent-in-%s', $currency->code), @@ -220,7 +221,7 @@ class BasicController extends Controller $paidAmount = $this->billRepository->sumPaidInRange($start, $end); $unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end); - $return = []; + $return = []; /** * @var array $info @@ -274,20 +275,23 @@ class BasicController extends Controller $available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end); $budgets = $this->budgetRepository->getActiveBudgets(); $spent = $this->opsRepository->sumExpenses($start, $end, null, $budgets); + $days = (int) $today->diffInDays($end, true) + 1; + Log::debug(sprintf('Now in getLeftToSpendInfo("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); foreach ($spent as $row) { // either an amount was budgeted or 0 is available. - $amount = (string) ($available[$row['currency_id']] ?? '0'); + $currencyId = $row['currency_id']; + $amount = (string) ($available[$currencyId] ?? '0'); $spentInCurrency = $row['sum']; $leftToSpend = bcadd($amount, $spentInCurrency); - - $days = (int) $today->diffInDays($end, true) + 1; $perDay = '0'; if (0 !== $days && bccomp($leftToSpend, '0') > -1) { $perDay = bcdiv($leftToSpend, (string) $days); } - $return[] = [ + Log::debug(sprintf('Spent %s %s', $row['currency_code'], $row['sum'])); + + $return[] = [ 'key' => sprintf('left-to-spend-in-%s', $row['currency_code']), 'title' => trans('firefly.box_left_to_spend_in_currency', ['currency' => $row['currency_symbol']]), 'monetary_value' => $leftToSpend, @@ -312,8 +316,8 @@ class BasicController extends Controller private function getNetWorthInfo(Carbon $start, Carbon $end): array { /** @var User $user */ - $user = auth()->user(); - $date = today(config('app.timezone'))->startOfDay(); + $user = auth()->user(); + $date = today(config('app.timezone'))->startOfDay(); // start and end in the future? use $end if ($this->notInDateRange($date, $start, $end)) { /** @var Carbon $date */ @@ -323,12 +327,12 @@ class BasicController extends Controller /** @var NetWorthInterface $netWorthHelper */ $netWorthHelper = app(NetWorthInterface::class); $netWorthHelper->setUser($user); - $allAccounts = $this->accountRepository->getActiveAccountsByType( + $allAccounts = $this->accountRepository->getActiveAccountsByType( [AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT] ); // filter list on preference of being included. - $filtered = $allAccounts->filter( + $filtered = $allAccounts->filter( function (Account $account) { $includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth'); @@ -336,13 +340,13 @@ class BasicController extends Controller } ); - $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); - $return = []; + $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); + $return = []; foreach ($netWorthSet as $key => $data) { if ('native' === $key) { continue; } - $amount = $data['balance']; + $amount = $data['balance']; if (0 === bccomp($amount, '0')) { continue; } diff --git a/app/Console/Commands/Correction/RecalculateNativeAmounts.php b/app/Console/Commands/Correction/RecalculateNativeAmounts.php index bb4097a542..c973ca0090 100644 --- a/app/Console/Commands/Correction/RecalculateNativeAmounts.php +++ b/app/Console/Commands/Correction/RecalculateNativeAmounts.php @@ -210,11 +210,19 @@ class RecalculateNativeAmounts extends Command $set = DB::table('transactions') ->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.user_group_id', $userGroup->id) - ->where(static function (DatabaseBuilder $q) use ($currency): void { - $q->whereNot('transactions.transaction_currency_id', $currency->id) - ->orWhereNot('transactions.foreign_currency_id', $currency->id) - ; + + ->where(function(DatabaseBuilder $q1) use ($currency) { + $q1->where(function(DatabaseBuilder $q2) use ($currency) { + $q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id'); + })->orWhere(function(DatabaseBuilder $q3) use ($currency) { + $q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id); + }); }) +// ->where(static function (DatabaseBuilder $q) use ($currency): void { +// $q->whereNot('transactions.transaction_currency_id', $currency->id) +// ->whereNot('transactions.foreign_currency_id', $currency->id) +// ; +// }) ->get(['transactions.id']) ; TransactionObserver::$recalculate = false; diff --git a/app/Handlers/Observer/TransactionObserver.php b/app/Handlers/Observer/TransactionObserver.php index 51246ec2bc..bedb0c02e4 100644 --- a/app/Handlers/Observer/TransactionObserver.php +++ b/app/Handlers/Observer/TransactionObserver.php @@ -71,7 +71,7 @@ class TransactionObserver $transaction->native_amount = null; $transaction->native_foreign_amount = null; // first normal amount - if ($transaction->transactionCurrency->id !== $userCurrency->id) { + 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->setIgnoreSettings(true); $transaction->native_amount = $converter->convert($transaction->transactionCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->amount); diff --git a/app/Http/Controllers/Json/BoxController.php b/app/Http/Controllers/Json/BoxController.php index 4a26d048d0..154160a8bc 100644 --- a/app/Http/Controllers/Json/BoxController.php +++ b/app/Http/Controllers/Json/BoxController.php @@ -47,108 +47,12 @@ class BoxController extends Controller use DateCalculation; /** - * This box has three types of info to display: - * 0) If the user has available amount this period and has overspent: overspent box. - * 1) If the user has available amount this period and has NOT overspent: left to spend box. - * 2) if the user has no available amount set this period: spent per day - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * Deprecated method, no longer in use. + * @deprecated */ public function available(): JsonResponse { - app('log')->debug('Now in available()'); - - /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); - - /** @var AvailableBudgetRepositoryInterface $abRepository */ - $abRepository = app(AvailableBudgetRepositoryInterface::class); - $abRepository->cleanup(); - - /** @var Carbon $start */ - $start = session('start', today(config('app.timezone'))->startOfMonth()); - - /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); - $today = today(config('app.timezone')); - $display = 2; // see method docs. - $boxTitle = (string) trans('firefly.spent'); - - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($today); - $cache->addProperty('box-available'); - if ($cache->has()) { - return response()->json($cache->get()); - } - $leftPerDayAmount = '0'; - $leftToSpendAmount = '0'; - - $currency = app('amount')->getDefaultCurrency(); - app('log')->debug(sprintf('Default currency is %s', $currency->code)); - $availableBudgets = $abRepository->getAvailableBudgetsByExactDate($start, $end); - app('log')->debug(sprintf('Found %d available budget(s)', $availableBudgets->count())); - $availableBudgets = $availableBudgets->filter( - static function (AvailableBudget $availableBudget) use ($currency) { // @phpstan-ignore-line - if ($availableBudget->transaction_currency_id === $currency->id) { - app('log')->debug(sprintf( - 'Will include AB #%d: from %s-%s amount %s', - $availableBudget->id, - $availableBudget->start_date->format('Y-m-d'), - $availableBudget->end_date->format('Y-m-d'), - $availableBudget->amount - )); - - return $availableBudget; - } - - return null; - } - ); - app('log')->debug(sprintf('Filtered back to %d available budgets', $availableBudgets->count())); - // spent in this period, in budgets, for default currency. - // also calculate spent per day. - $spent = $opsRepository->sumExpenses($start, $end, null, null, $currency); - $spentAmount = $spent[$currency->id]['sum'] ?? '0'; - app('log')->debug(sprintf('Spent for default currency for all budgets in this period: %s', $spentAmount)); - - $days = (int) ($today->between($start, $end) ? $today->diffInDays($start, true) + 1 : $end->diffInDays($start, true) + 1); - app('log')->debug(sprintf('Number of days left: %d', $days)); - $spentPerDay = bcdiv($spentAmount, (string) $days); - app('log')->debug(sprintf('Available to spend per day: %s', $spentPerDay)); - if ($availableBudgets->count() > 0) { - $display = 0; // assume user overspent - $boxTitle = (string) trans('firefly.overspent'); - $totalAvailableSum = (string) $availableBudgets->sum('amount'); - app('log')->debug(sprintf('Total available sum is %s', $totalAvailableSum)); - // calculate with available budget. - $leftToSpendAmount = bcadd($totalAvailableSum, $spentAmount); - app('log')->debug(sprintf('So left to spend is %s', $leftToSpendAmount)); - if (bccomp($leftToSpendAmount, '0') >= 0) { - app('log')->debug('Left to spend is positive or zero!'); - $boxTitle = (string) trans('firefly.left_to_spend'); - $activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description. - $display = 1; // not overspent - $leftPerDayAmount = 0 === $activeDaysLeft ? $leftToSpendAmount : bcdiv($leftToSpendAmount, (string) $activeDaysLeft); - app('log')->debug(sprintf('Left to spend per day is %s', $leftPerDayAmount)); - } - } - - $return = [ - 'display' => $display, - 'spent_total' => app('amount')->formatAnything($currency, $spentAmount, false), - 'spent_per_day' => app('amount')->formatAnything($currency, $spentPerDay, false), - 'left_to_spend' => app('amount')->formatAnything($currency, $leftToSpendAmount, false), - 'left_per_day' => app('amount')->formatAnything($currency, $leftPerDayAmount, false), - 'title' => $boxTitle, - ]; - app('log')->debug('Final output', $return); - - $cache->store($return); - app('log')->debug('Now done with available()'); - - return response()->json($return); + return response()->json([]); } /** @@ -158,35 +62,38 @@ class BoxController extends Controller { // Cache result, return cache if present. /** @var Carbon $start */ - $start = session('start', today(config('app.timezone'))->startOfMonth()); + $start = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); - $cache = new CacheProperties(); + $end = session('end', today(config('app.timezone'))->endOfMonth()); + $convertToNative = app('preferences')->get('convert_to_native', false)->data; + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($convertToNative); $cache->addProperty('box-balance'); if ($cache->has()) { return response()->json($cache->get()); } // prep some arrays: - $incomes = []; - $expenses = []; - $sums = []; - $currency = app('amount')->getDefaultCurrency(); + $incomes = []; + $expenses = []; + $sums = []; + $currency = app('amount')->getDefaultCurrency(); + // collect income of user: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end) - ->setTypes([TransactionType::DEPOSIT]) - ; - $set = $collector->getExtractedJournals(); + ->setTypes([TransactionType::DEPOSIT]); + $set = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($set as $journal) { - $currencyId = (int) $journal['currency_id']; - $amount = $journal['amount'] ?? '0'; + $field = $convertToNative && $currency->id !== $journal['currency_id'] ? 'native_amount' : 'amount'; + $currencyId = $convertToNative ? $currency->id : (int) $journal['currency_id']; + $amount = $journal[$field] ?? '0'; $incomes[$currencyId] ??= '0'; $incomes[$currencyId] = bcadd($incomes[$currencyId], app('steam')->positive($amount)); $sums[$currencyId] ??= '0'; @@ -197,21 +104,22 @@ class BoxController extends Controller /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end) - ->setTypes([TransactionType::WITHDRAWAL]) - ; - $set = $collector->getExtractedJournals(); + ->setTypes([TransactionType::WITHDRAWAL]); + $set = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($set as $journal) { - $currencyId = (int) $journal['currency_id']; + $field = $convertToNative && $currency->id !== $journal['currency_id'] ? 'native_amount' : 'amount'; + $currencyId = $convertToNative ? $currency->id : (int) $journal['currency_id']; + $amount = $journal[$field] ?? '0'; $expenses[$currencyId] ??= '0'; - $expenses[$currencyId] = bcadd($expenses[$currencyId], $journal['amount'] ?? '0'); + $expenses[$currencyId] = bcadd($expenses[$currencyId], $amount); $sums[$currencyId] ??= '0'; - $sums[$currencyId] = bcadd($sums[$currencyId], $journal['amount']); + $sums[$currencyId] = bcadd($sums[$currencyId], $amount); } // format amounts: - $keys = array_keys($sums); + $keys = array_keys($sums); foreach ($keys as $currencyId) { $currency = $repository->find($currencyId); $sums[$currencyId] = app('amount')->formatAnything($currency, $sums[$currencyId], false); @@ -225,7 +133,7 @@ class BoxController extends Controller $expenses[$currency->id] = app('amount')->formatAnything($currency, '0', false); } - $response = [ + $response = [ 'incomes' => $incomes, 'expenses' => $expenses, 'sums' => $sums, @@ -242,7 +150,7 @@ class BoxController extends Controller */ public function netWorth(): JsonResponse { - $date = today(config('app.timezone'))->endOfDay(); + $date = today(config('app.timezone'))->endOfDay(); // start and end in the future? use $end if ($this->notInSessionRange($date)) { @@ -251,7 +159,7 @@ class BoxController extends Controller } /** @var NetWorthInterface $netWorthHelper */ - $netWorthHelper = app(NetWorthInterface::class); + $netWorthHelper = app(NetWorthInterface::class); $netWorthHelper->setUser(auth()->user()); /** @var AccountRepositoryInterface $accountRepository */ @@ -262,7 +170,7 @@ class BoxController extends Controller app('log')->debug(sprintf('Found %d accounts.', $allAccounts->count())); // filter list on preference of being included. - $filtered = $allAccounts->filter( + $filtered = $allAccounts->filter( static function (Account $account) use ($accountRepository) { $includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth'); $result = null === $includeNetWorth ? true : '1' === $includeNetWorth; @@ -274,15 +182,15 @@ class BoxController extends Controller } ); - $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); - $return = []; + $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); + $return = []; foreach ($netWorthSet as $key => $data) { if ('native' === $key) { continue; } $return[$data['currency_id']] = app('amount')->formatFlat($data['currency_symbol'], $data['currency_decimal_places'], $data['balance'], false); } - $return = [ + $return = [ 'net_worths' => array_values($return), ]; diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 45071d460f..85e4a70b8c 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -67,6 +67,8 @@ class Transaction extends Model 'transaction_journal_id', 'description', 'amount', + 'native_amount', + 'native_foreign_amount', 'identifier', 'transaction_currency_id', 'foreign_currency_id', diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 21e6d36914..0edeb437cf 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -60,8 +60,7 @@ class BillRepository implements BillRepositoryInterface $search->whereLike('name', sprintf('%%%s', $query)); } $search->orderBy('name', 'ASC') - ->where('active', true) - ; + ->where('active', true); return $search->take($limit)->get(); } @@ -73,8 +72,7 @@ class BillRepository implements BillRepositoryInterface $search->whereLike('name', sprintf('%s%%', $query)); } $search->orderBy('name', 'ASC') - ->where('active', true) - ; + ->where('active', true); return $search->take($limit)->get(); } @@ -157,7 +155,7 @@ class BillRepository implements BillRepositoryInterface */ public function getAttachments(Bill $bill): Collection { - $set = $bill->attachments()->get(); + $set = $bill->attachments()->get(); /** @var \Storage $disk */ $disk = \Storage::disk('upload'); @@ -176,10 +174,9 @@ class BillRepository implements BillRepositoryInterface public function getBills(): Collection { return $this->user->bills() - ->orderBy('order', 'ASC') - ->orderBy('active', 'DESC') - ->orderBy('name', 'ASC')->get() - ; + ->orderBy('order', 'ASC') + ->orderBy('active', 'DESC') + ->orderBy('name', 'ASC')->get(); } public function getBillsForAccounts(Collection $accounts): Collection @@ -203,25 +200,24 @@ class BillRepository implements BillRepositoryInterface $ids = $accounts->pluck('id')->toArray(); return $this->user->bills() - ->leftJoin( - 'transaction_journals', - static function (JoinClause $join): void { - $join->on('transaction_journals.bill_id', '=', 'bills.id')->whereNull('transaction_journals.deleted_at'); - } - ) - ->leftJoin( - 'transactions', - static function (JoinClause $join): void { - $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); - } - ) - ->whereIn('transactions.account_id', $ids) - ->whereNull('transaction_journals.deleted_at') - ->orderBy('bills.active', 'DESC') - ->orderBy('bills.name', 'ASC') - ->groupBy($fields) - ->get($fields) - ; + ->leftJoin( + 'transaction_journals', + static function (JoinClause $join): void { + $join->on('transaction_journals.bill_id', '=', 'bills.id')->whereNull('transaction_journals.deleted_at'); + } + ) + ->leftJoin( + 'transactions', + static function (JoinClause $join): void { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->whereIn('transactions.account_id', $ids) + ->whereNull('transaction_journals.deleted_at') + ->orderBy('bills.active', 'DESC') + ->orderBy('bills.name', 'ASC') + ->groupBy($fields) + ->get($fields); } /** @@ -246,7 +242,7 @@ class BillRepository implements BillRepositoryInterface public function getOverallAverage(Bill $bill): array { /** @var JournalRepositoryInterface $repos */ - $repos = app(JournalRepositoryInterface::class); + $repos = app(JournalRepositoryInterface::class); $repos->setUser($this->user); // get and sort on currency @@ -259,7 +255,7 @@ class BillRepository implements BillRepositoryInterface $transaction = $journal->transactions()->where('amount', '<', 0)->first(); $currencyId = (int) $journal->transaction_currency_id; $currency = $journal->transactionCurrency; - $result[$currencyId] ??= [ + $result[$currencyId] ??= [ 'sum' => '0', 'count' => 0, 'avg' => '0', @@ -284,7 +280,7 @@ class BillRepository implements BillRepositoryInterface return $result; } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -294,9 +290,8 @@ class BillRepository implements BillRepositoryInterface public function getPaginator(int $size): LengthAwarePaginator { return $this->user->bills() - ->orderBy('active', 'DESC') - ->orderBy('name', 'ASC')->paginate($size) - ; + ->orderBy('active', 'DESC') + ->orderBy('name', 'ASC')->paginate($size); } /** @@ -309,14 +304,13 @@ class BillRepository implements BillRepositoryInterface Log::debug(sprintf('Search for linked journals between %s and %s', $start->toW3cString(), $end->toW3cString())); return $bill->transactionJournals() - ->before($end)->after($start)->get( + ->before($end)->after($start)->get( [ 'transaction_journals.id', 'transaction_journals.date', 'transaction_journals.transaction_group_id', ] - ) - ; + ); } /** @@ -325,11 +319,10 @@ class BillRepository implements BillRepositoryInterface public function getRulesForBill(Bill $bill): Collection { return $this->user->rules() - ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') - ->where('rule_actions.action_type', 'link_to_bill') - ->where('rule_actions.action_value', $bill->name) - ->get(['rules.*']) - ; + ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') + ->where('rule_actions.action_type', 'link_to_bill') + ->where('rule_actions.action_value', $bill->name) + ->get(['rules.*']); } /** @@ -340,16 +333,15 @@ class BillRepository implements BillRepositoryInterface */ public function getRulesForBills(Collection $collection): array { - $rules = $this->user->rules() - ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') - ->where('rule_actions.action_type', 'link_to_bill') - ->get(['rules.id', 'rules.title', 'rule_actions.action_value', 'rules.active']) - ; - $array = []; + $rules = $this->user->rules() + ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') + ->where('rule_actions.action_type', 'link_to_bill') + ->get(['rules.id', 'rules.title', 'rule_actions.action_value', 'rules.active']); + $array = []; /** @var Rule $rule */ foreach ($rules as $rule) { - $array[$rule->action_value] ??= []; + $array[$rule->action_value] ??= []; $array[$rule->action_value][] = ['id' => $rule->id, 'title' => $rule->title, 'active' => $rule->active]; } $return = []; @@ -363,28 +355,27 @@ class BillRepository implements BillRepositoryInterface public function getYearAverage(Bill $bill, Carbon $date): array { /** @var JournalRepositoryInterface $repos */ - $repos = app(JournalRepositoryInterface::class); + $repos = app(JournalRepositoryInterface::class); $repos->setUser($this->user); // get and sort on currency - $result = []; + $result = []; $journals = $bill->transactionJournals() - ->where('date', '>=', $date->year.'-01-01 00:00:00') - ->where('date', '<=', $date->year.'-12-31 23:59:59') - ->get() - ; + ->where('date', '>=', $date->year . '-01-01 00:00:00') + ->where('date', '<=', $date->year . '-12-31 23:59:59') + ->get(); /** @var TransactionJournal $journal */ foreach ($journals as $journal) { /** @var null|Transaction $transaction */ - $transaction = $journal->transactions()->where('amount', '<', 0)->first(); + $transaction = $journal->transactions()->where('amount', '<', 0)->first(); if (null === $transaction) { continue; } $currencyId = (int) $journal->transaction_currency_id; $currency = $journal->transactionCurrency; - $result[$currencyId] ??= [ + $result[$currencyId] ??= [ 'sum' => '0', 'count' => 0, 'avg' => '0', @@ -428,7 +419,7 @@ class BillRepository implements BillRepositoryInterface */ public function nextExpectedMatch(Bill $bill, Carbon $date): Carbon { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($bill->id); $cache->addProperty('nextExpectedMatch'); $cache->addProperty($date); @@ -436,17 +427,17 @@ class BillRepository implements BillRepositoryInterface return $cache->get(); } // find the most recent date for this bill NOT in the future. Cache this date: - $start = clone $bill->date; + $start = clone $bill->date; $start->startOfDay(); - app('log')->debug('nextExpectedMatch: Start is '.$start->format('Y-m-d')); + app('log')->debug('nextExpectedMatch: Start is ' . $start->format('Y-m-d')); while ($start < $date) { app('log')->debug(sprintf('$start (%s) < $date (%s)', $start->format('Y-m-d H:i:s'), $date->format('Y-m-d H:i:s'))); $start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip); - app('log')->debug('Start is now '.$start->format('Y-m-d H:i:s')); + app('log')->debug('Start is now ' . $start->format('Y-m-d H:i:s')); } - $end = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip); + $end = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip); $end->endOfDay(); // see if the bill was paid in this period. @@ -458,8 +449,8 @@ class BillRepository implements BillRepositoryInterface $start = clone $end; $end = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip); } - app('log')->debug('nextExpectedMatch: Final start is '.$start->format('Y-m-d')); - app('log')->debug('nextExpectedMatch: Matching end is '.$end->format('Y-m-d')); + app('log')->debug('nextExpectedMatch: Final start is ' . $start->format('Y-m-d')); + app('log')->debug('nextExpectedMatch: Matching end is ' . $end->format('Y-m-d')); $cache->store($start); @@ -510,15 +501,18 @@ class BillRepository implements BillRepositoryInterface public function sumPaidInRange(Carbon $start, Carbon $end): array { - $bills = $this->getActiveBills(); - $return = []; + $bills = $this->getActiveBills(); + $return = []; + $convertToNative = app('preferences')->getForUser($this->user, 'convert_to_native', false)->data; + $default = app('amount')->getDefaultCurrency(); /** @var Bill $bill */ foreach ($bills as $bill) { /** @var Collection $set */ - $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); - $currency = $bill->transactionCurrency; - + $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); + $currency = $convertToNative && $bill->transactionCurrency->id !== $default->id ? $default : $bill->transactionCurrency; + $field = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount' : 'amount'; + $foreignField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_foreign_amount' : 'foreign_amount'; $return[$currency->id] ??= [ 'id' => (string) $currency->id, 'name' => $currency->name, @@ -533,10 +527,10 @@ class BillRepository implements BillRepositoryInterface /** @var null|Transaction $sourceTransaction */ $sourceTransaction = $transactionJournal->transactions()->where('amount', '<', 0)->first(); if (null !== $sourceTransaction) { - $amount = $sourceTransaction->amount; + $amount = $sourceTransaction->$field; if ((int) $sourceTransaction->foreign_currency_id === $currency->id) { // use foreign amount instead! - $amount = (string) $sourceTransaction->foreign_amount; + $amount = (string) $sourceTransaction->$foreignField; } $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); } @@ -549,17 +543,19 @@ class BillRepository implements BillRepositoryInterface public function getActiveBills(): Collection { return $this->user->bills() - ->where('active', true) - ->orderBy('bills.name', 'ASC') - ->get(['bills.*', \DB::raw('((bills.amount_min + bills.amount_max) / 2) AS expectedAmount')]) // @phpstan-ignore-line - ; + ->where('active', true) + ->orderBy('bills.name', 'ASC') + ->get(['bills.*', \DB::raw('((bills.amount_min + bills.amount_max) / 2) AS expectedAmount')]) // @phpstan-ignore-line + ; } public function sumUnpaidInRange(Carbon $start, Carbon $end): array { app('log')->debug(sprintf('Now in sumUnpaidInRange("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); - $bills = $this->getActiveBills(); - $return = []; + $bills = $this->getActiveBills(); + $return = []; + $convertToNative = app('preferences')->getForUser($this->user, 'convert_to_native', false)->data; + $default = app('amount')->getDefaultCurrency(); /** @var Bill $bill */ foreach ($bills as $bill) { @@ -570,10 +566,13 @@ class BillRepository implements BillRepositoryInterface // app('log')->debug(sprintf('Pay dates: %d, count: %d, left: %d', $dates->count(), $count, $total)); // app('log')->debug('dates', $dates->toArray()); + $minField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_min' : 'amount_min'; + $maxField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_max' : 'amount_max'; + if ($total > 0) { - $currency = $bill->transactionCurrency; - $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); - $return[$currency->id] ??= [ + $currency = $convertToNative && $bill->transactionCurrency->id !== $default->id ? $default : $bill->transactionCurrency; + $average = bcdiv(bcadd($bill->$maxField, $bill->$minField), '2'); + $return[$currency->id] ??= [ 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, @@ -611,7 +610,7 @@ class BillRepository implements BillRepositoryInterface // app('log')->debug(sprintf('Currentstart (%s) has become %s.', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d'))); - $currentStart = clone $nextExpectedMatch; + $currentStart = clone $nextExpectedMatch; } return $set; diff --git a/app/Repositories/Budget/AvailableBudgetRepository.php b/app/Repositories/Budget/AvailableBudgetRepository.php index fc1e0ee5cd..30def078e2 100644 --- a/app/Repositories/Budget/AvailableBudgetRepository.php +++ b/app/Repositories/Budget/AvailableBudgetRepository.php @@ -47,9 +47,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface /** @var AvailableBudget $availableBudget */ foreach ($availableBudgets as $availableBudget) { - $start = $availableBudget->start_date->format('Y-m-d'); - $end = $availableBudget->end_date->format('Y-m-d'); - $key = sprintf('%s-%s-%s', $availableBudget->transaction_currency_id, $start, $end); + $start = $availableBudget->start_date->format('Y-m-d'); + $end = $availableBudget->end_date->format('Y-m-d'); + $key = sprintf('%s-%s-%s', $availableBudget->transaction_currency_id, $start, $end); if (array_key_exists($key, $exists)) { app('log')->debug(sprintf('Found duplicate AB: %s %s, %s-%s. Has been deleted', $availableBudget->transaction_currency_id, $availableBudget->amount, $start, $end)); $availableBudget->delete(); @@ -101,23 +101,21 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface public function find(TransactionCurrency $currency, Carbon $start, Carbon $end): ?AvailableBudget { return $this->user->availableBudgets() - ->where('transaction_currency_id', $currency->id) - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d')) - ->first() - ; + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d')) + ->first(); } public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string { - $amount = '0'; + $amount = '0'; /** @var null|AvailableBudget $availableBudget */ $availableBudget = $this->user->availableBudgets() - ->where('transaction_currency_id', $currency->id) - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->first() - ; + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); if (null !== $availableBudget) { $amount = $availableBudget->amount; } @@ -129,15 +127,20 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface { $return = []; $availableBudgets = $this->user->availableBudgets() - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->get() - ; + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->get(); + + // use native amount if necessary? + $convertToNative = app('preferences')->getForUser($this->user, 'convert_to_native', false)->data; + $default = app('amount')->getDefaultCurrency(); /** @var AvailableBudget $availableBudget */ foreach ($availableBudgets as $availableBudget) { - $return[$availableBudget->transaction_currency_id] = $availableBudget->amount; + $currencyId = $convertToNative && $availableBudget->transaction_currency_id !== $default->id ? $default->id : $availableBudget->transaction_currency_id; + $field = $convertToNative && $availableBudget->transaction_currency_id !== $default->id ? 'native_amount' : 'amount'; + $return[$currencyId] = $return[$currencyId] ?? '0'; + $return[$currencyId] = bcadd($return[$currencyId], $availableBudget->$field); } - return $return; } @@ -172,10 +175,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface public function getAvailableBudgetsByExactDate(Carbon $start, Carbon $end): Collection { return $this->user->availableBudgets() - ->where('start_date', '=', $start->format('Y-m-d')) - ->where('end_date', '=', $end->format('Y-m-d')) - ->get() - ; + ->where('start_date', '=', $start->format('Y-m-d')) + ->where('end_date', '=', $end->format('Y-m-d')) + ->get(); } public function getByCurrencyDate(Carbon $start, Carbon $end, TransactionCurrency $currency): ?AvailableBudget @@ -184,8 +186,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface ->availableBudgets() ->where('transaction_currency_id', $currency->id) ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->first() - ; + ->where('end_date', $end->format('Y-m-d'))->first(); } /** @@ -193,13 +194,12 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface */ public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): AvailableBudget { - $availableBudget = $this->user->availableBudgets() - ->where('transaction_currency_id', $currency->id) - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->first() - ; + $availableBudget = $this->user->availableBudgets() + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); if (null === $availableBudget) { - $availableBudget = new AvailableBudget(); + $availableBudget = new AvailableBudget(); $availableBudget->user()->associate($this->user); $availableBudget->transactionCurrency()->associate($currency); $availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line @@ -213,7 +213,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface return $availableBudget; } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -226,7 +226,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface if ($start instanceof Carbon) { $start = $data['start']->startOfDay(); } - $end = $data['end']; + $end = $data['end']; if ($end instanceof Carbon) { $end = $data['end']->endOfDay(); } diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index 153bb0a9f5..43f5a12d76 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -60,7 +60,7 @@ class OperationsRepository implements OperationsRepositoryInterface ++$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); } @@ -82,21 +82,21 @@ class OperationsRepository implements OperationsRepositoryInterface // 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', @@ -134,13 +134,13 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setBudgets($this->getBudgets()); } $collector->withBudgetInformation()->withAccountInformation()->withCategoryInformation(); - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $array = []; foreach ($journals as $journal) { - $currencyId = (int) $journal['currency_id']; - $budgetId = (int) $journal['budget_id']; - $budgetName = (string) $journal['budget_name']; + $currencyId = (int) $journal['currency_id']; + $budgetId = (int) $journal['budget_id']; + $budgetName = (string) $journal['budget_name']; // catch "no category" entries. if (0 === $budgetId) { @@ -148,7 +148,7 @@ class OperationsRepository implements OperationsRepositoryInterface } // info about the currency: - $array[$currencyId] ??= [ + $array[$currencyId] ??= [ 'budgets' => [], 'currency_id' => $currencyId, 'currency_name' => $journal['currency_name'], @@ -183,7 +183,7 @@ class OperationsRepository implements OperationsRepositoryInterface return $array; } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -207,11 +207,8 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null - ): array { - // app('log')->debug(sprintf('Now in %s', __METHOD__)); - $start->startOfDay(); - $end->endOfDay(); - + ): array + { // this collector excludes all transfers TO // liabilities (which are also withdrawals) // because those expenses only become expenses @@ -219,8 +216,12 @@ class OperationsRepository implements OperationsRepositoryInterface // TODO this filter must be somewhere in AccountRepositoryInterface because I suspect its needed more often (A113) $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(); + + // default currency information for native stuff. + $convertToNative = app('preferences')->get('convert_to_native', false)->data; + $default = app('amount')->getDefaultCurrency(); /** @var Account $account */ foreach ($subset as $account) { @@ -230,12 +231,11 @@ class OperationsRepository implements OperationsRepositoryInterface } /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user) - ->setRange($start, $end) - ->excludeDestinationAccounts($selection) - ->setTypes([TransactionType::WITHDRAWAL]) - ; + ->setRange($start, $end) + ->excludeDestinationAccounts($selection) + ->setTypes([TransactionType::WITHDRAWAL]); if (null !== $accounts) { $collector->setAccounts($accounts); @@ -247,7 +247,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setCurrency($currency); } $collector->setBudgets($budgets); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); // same but for foreign currencies: if (null !== $currency) { @@ -255,35 +255,51 @@ class OperationsRepository implements OperationsRepositoryInterface /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]) - ->setForeignCurrency($currency)->setBudgets($budgets) - ; + ->setForeignCurrency($currency)->setBudgets($budgets); if (null !== $accounts) { $collector->setAccounts($accounts); } - $result = $collector->getExtractedJournals(); + $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): - $journals = $result + $journals; + $journals = $result + $journals; } - $array = []; + $array = []; foreach ($journals as $journal) { - $currencyId = (int) $journal['currency_id']; - $array[$currencyId] ??= [ + $currencyId = (int) $journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + $field = 'amount'; + $foreignField = 'foreign_amount'; + // if the user wants everything in native currency, use it. + if ($convertToNative && $default->id !== (int) $journal['currency_id']) { + // use default currency info + $currencyId = $default->id; + $currencyName = $default->name; + $currencySymbol = $default->symbol; + $currencyCode = $default->code; + $currencyDecimalPlaces = $default->decimal_places; + $field = 'native_amount'; + $foreignField = 'native_foreign_amount'; + } + $array[$currencyId] ??= [ 'sum' => '0', '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, ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); + $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal[$field])); // also do foreign amount: - $foreignId = (int) $journal['foreign_currency_id']; - if (0 !== $foreignId) { - $array[$foreignId] ??= [ + $foreignId = (int) $journal['foreign_currency_id']; + if (0 !== $foreignId && $foreignId !== $currencyId) { + $array[$foreignId] ??= [ 'sum' => '0', 'currency_id' => $foreignId, 'currency_name' => $journal['foreign_currency_name'],