From a29904704c0ba31a9c9de445a64253e186e76e54 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 11 May 2025 07:01:36 +0200 Subject: [PATCH] Fix #10265 --- app/Factory/TransactionJournalFactory.php | 96 +++++++++++-------- .../Controllers/Account/IndexController.php | 4 +- resources/views/javascript/variables.twig | 6 +- resources/views/list/groups.twig | 12 ++- 4 files changed, 74 insertions(+), 44 deletions(-) diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index fe17fa054d..d22e53807b 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Factory; use Carbon\Carbon; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Exceptions\FireflyException; @@ -103,7 +104,7 @@ class TransactionJournalFactory { app('log')->debug('Now in TransactionJournalFactory::create()'); // convert to special object. - $dataObject = new NullArrayObject($data); + $dataObject = new NullArrayObject($data); app('log')->debug('Start of TransactionJournalFactory::create()'); $collection = new Collection(); @@ -161,14 +162,14 @@ class TransactionJournalFactory $this->errorIfDuplicate($row['import_hash_v2']); // Some basic fields - $type = $this->typeRepository->findTransactionType(null, $row['type']); - $carbon = $row['date'] ?? today(config('app.timezone')); - $order = $row['order'] ?? 0; - $currency = $this->currencyRepository->findCurrency((int) $row['currency_id'], $row['currency_code']); - $foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']); - $bill = $this->billRepository->findBill((int) $row['bill_id'], $row['bill_name']); - $billId = TransactionTypeEnum::WITHDRAWAL->value === $type->type && null !== $bill ? $bill->id : null; - $description = (string) $row['description']; + $type = $this->typeRepository->findTransactionType(null, $row['type']); + $carbon = $row['date'] ?? today(config('app.timezone')); + $order = $row['order'] ?? 0; + $currency = $this->currencyRepository->findCurrency((int) $row['currency_id'], $row['currency_code']); + $foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']); + $bill = $this->billRepository->findBill((int) $row['bill_id'], $row['bill_name']); + $billId = TransactionTypeEnum::WITHDRAWAL->value === $type->type && null !== $bill ? $bill->id : null; + $description = (string) $row['description']; // Manipulate basic fields $carbon->setTimezone(config('app.timezone')); @@ -190,7 +191,7 @@ class TransactionJournalFactory } /** create or get source and destination accounts */ - $sourceInfo = [ + $sourceInfo = [ 'id' => $row['source_id'], 'name' => $row['source_name'], 'iban' => $row['source_iban'], @@ -199,7 +200,7 @@ class TransactionJournalFactory 'currency_id' => $currency->id, ]; - $destInfo = [ + $destInfo = [ 'id' => $row['destination_id'], 'name' => $row['destination_name'], 'iban' => $row['destination_iban'], @@ -209,8 +210,8 @@ class TransactionJournalFactory ]; app('log')->debug('Source info:', $sourceInfo); app('log')->debug('Destination info:', $destInfo); - $sourceAccount = $this->getAccount($type->type, 'source', $sourceInfo); - $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo); + $sourceAccount = $this->getAccount($type->type, 'source', $sourceInfo); + $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo); app('log')->debug('Done with getAccount(2x)'); // this is the moment for a reconciliation sanity check (again). @@ -218,15 +219,15 @@ class TransactionJournalFactory [$sourceAccount, $destinationAccount] = $this->reconciliationSanityCheck($sourceAccount, $destinationAccount); } - $currency = $this->getCurrencyByAccount($type->type, $currency, $sourceAccount, $destinationAccount); - $foreignCurrency = $this->compareCurrencies($currency, $foreignCurrency); - $foreignCurrency = $this->getForeignByAccount($type->type, $foreignCurrency, $destinationAccount); - $description = $this->getDescription($description); + $currency = $this->getCurrencyByAccount($type->type, $currency, $sourceAccount, $destinationAccount); + $foreignCurrency = $this->compareCurrencies($currency, $foreignCurrency); + $foreignCurrency = $this->getForeignByAccount($type->type, $foreignCurrency, $destinationAccount); + $description = $this->getDescription($description); app('log')->debug(sprintf('Date: %s (%s)', $carbon->toW3cString(), $carbon->getTimezone()->getName())); /** Create a basic journal. */ - $journal = TransactionJournal::create( + $journal = TransactionJournal::create( [ 'user_id' => $this->user->id, 'user_group_id' => $this->userGroup->id, @@ -244,7 +245,7 @@ class TransactionJournalFactory app('log')->debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description)); /** Create two transactions. */ - $transactionFactory = app(TransactionFactory::class); + $transactionFactory = app(TransactionFactory::class); $transactionFactory->setJournal($journal); $transactionFactory->setAccount($sourceAccount); $transactionFactory->setCurrency($currency); @@ -262,7 +263,7 @@ class TransactionJournalFactory } /** @var TransactionFactory $transactionFactory */ - $transactionFactory = app(TransactionFactory::class); + $transactionFactory = app(TransactionFactory::class); $transactionFactory->setJournal($journal); $transactionFactory->setAccount($destinationAccount); $transactionFactory->setAccountInformation($destInfo); @@ -274,10 +275,10 @@ class TransactionJournalFactory // Firefly III will save the foreign currency information in such a way that both // asset accounts can look at the "amount" and "transaction_currency_id" column and // see the currency they expect to see. - $amount = (string) $row['amount']; - $foreignAmount = (string) $row['foreign_amount']; - if (null !== $foreignCurrency && $foreignCurrency->id !== $currency->id - && TransactionTypeEnum::TRANSFER->value === $type->type + $amount = (string) $row['amount']; + $foreignAmount = (string) $row['foreign_amount']; + if (null !== $foreignCurrency && $foreignCurrency->id !== $currency->id && + (TransactionTypeEnum::TRANSFER->value === $type->type || $this->isBetweenAssetAndLiability($sourceAccount, $destinationAccount)) ) { $transactionFactory->setCurrency($foreignCurrency); $transactionFactory->setForeignCurrency($currency); @@ -295,7 +296,7 @@ class TransactionJournalFactory throw new FireflyException($e->getMessage(), 0, $e); } - $journal->completed = true; + $journal->completed = true; $journal->save(); $this->storeBudget($journal, $row); $this->storeCategory($journal, $row); @@ -320,7 +321,7 @@ class TransactionJournalFactory app('log')->error(sprintf('Could not encode dataRow: %s', $e->getMessage())); $json = microtime(); } - $hash = hash('sha256', $json); + $hash = hash('sha256', $json); app('log')->debug(sprintf('The hash is: %s', $hash), $dataRow); return $hash; @@ -341,13 +342,12 @@ class TransactionJournalFactory /** @var null|TransactionJournalMeta $result */ $result = TransactionJournalMeta::withTrashed() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') - ->whereNotNull('transaction_journals.id') - ->where('transaction_journals.user_id', $this->user->id) - ->where('data', \Safe\json_encode($hash, JSON_THROW_ON_ERROR)) - ->with(['transactionJournal', 'transactionJournal.transactionGroup']) - ->first(['journal_meta.*']) - ; + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') + ->whereNotNull('transaction_journals.id') + ->where('transaction_journals.user_id', $this->user->id) + ->where('data', \Safe\json_encode($hash, JSON_THROW_ON_ERROR)) + ->with(['transactionJournal', 'transactionJournal.transactionGroup']) + ->first(['journal_meta.*']); if (null !== $result) { app('log')->warning(sprintf('Found a duplicate in errorIfDuplicate because hash %s is not unique!', $hash)); $journal = $result->transactionJournal()->withTrashed()->first(); @@ -364,18 +364,18 @@ class TransactionJournalFactory private function validateAccounts(NullArrayObject $data): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); - $transactionType = $data['type'] ?? 'invalid'; + $transactionType = $data['type'] ?? 'invalid'; $this->accountValidator->setUser($this->user); $this->accountValidator->setTransactionType($transactionType); // validate source account. - $array = [ + $array = [ 'id' => null !== $data['source_id'] ? (int) $data['source_id'] : null, 'name' => null !== $data['source_name'] ? (string) $data['source_name'] : null, 'iban' => null !== $data['source_iban'] ? (string) $data['source_iban'] : null, 'number' => null !== $data['source_number'] ? (string) $data['source_number'] : null, ]; - $validSource = $this->accountValidator->validateSource($array); + $validSource = $this->accountValidator->validateSource($array); // do something with result: if (false === $validSource) { @@ -384,7 +384,7 @@ class TransactionJournalFactory app('log')->debug('Source seems valid.'); // validate destination account - $array = [ + $array = [ 'id' => null !== $data['destination_id'] ? (int) $data['destination_id'] : null, 'name' => null !== $data['destination_name'] ? (string) $data['destination_name'] : null, 'iban' => null !== $data['destination_iban'] ? (string) $data['destination_iban'] : null, @@ -468,7 +468,7 @@ class TransactionJournalFactory // return user's default: return app('amount')->getNativeCurrencyByUserGroup($this->user->userGroup); } - $result = $preference ?? $currency; + $result = $preference ?? $currency; app('log')->debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name)); return $result; @@ -556,7 +556,7 @@ class TransactionJournalFactory protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void { - $set = [ + $set = [ 'journal' => $journal, 'name' => $field, 'data' => (string) ($data[$field] ?? ''), @@ -605,4 +605,22 @@ class TransactionJournalFactory $this->piggyRepository->setUserGroup($userGroup); $this->accountRepository->setUserGroup($userGroup); } + + private function isBetweenAssetAndLiability(Account $source, Account $destination): bool + { + $sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; + + // source is liability, destination is asset + if(in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) { + Log::debug('Source is a liability account, destination is an asset account, return TRUE.'); + return true; + } + // source is asset, destination is liability + if(in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) { + Log::debug('Destination is a liability account, source is an asset account, return TRUE.'); + return true; + } + Log::debug('Not between asset and liability, return FALSE'); + return false; + } } diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php index 99ae94aa15..4e8ded5f1e 100644 --- a/app/Http/Controllers/Account/IndexController.php +++ b/app/Http/Controllers/Account/IndexController.php @@ -163,7 +163,9 @@ class IndexController extends Controller /** @var Carbon $end */ $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $start->subDay(); + + // 2025-05-11 removed this so start is exactly the start of the month. + // $start->subDay(); $ids = $accounts->pluck('id')->toArray(); Log::debug(sprintf('index start: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s'))); diff --git a/resources/views/javascript/variables.twig b/resources/views/javascript/variables.twig index 37c6ab8c13..8cc69e7538 100644 --- a/resources/views/javascript/variables.twig +++ b/resources/views/javascript/variables.twig @@ -55,9 +55,9 @@ var edit_selected_txt = "{{ trans('firefly.mass_edit')|escape('js') }}"; var edit_bulk_selected_txt = "{{ trans('firefly.bulk_edit')|escape('js') }}"; var delete_selected_txt = "{{ trans('firefly.mass_delete')|escape('js') }}"; -var mass_edit_url = '{{ route('transactions.mass.edit', ['']) }}'; -var bulk_edit_url = '{{ route('transactions.bulk.edit', ['']) }}'; -var mass_delete_url = '{{ route('transactions.mass.delete', ['']) }}'; +var mass_edit_url = '{{ route('transactions.mass.edit', ['0']) }}'; +var bulk_edit_url = '{{ route('transactions.bulk.edit', ['0']) }}'; +var mass_delete_url = '{{ route('transactions.mass.delete', ['0']) }}'; // for demo: var nextLabel = "{{ trans('firefly.intro_next_label')|escape('js') }}"; diff --git a/resources/views/list/groups.twig b/resources/views/list/groups.twig index dbe736414d..69cb65289c 100644 --- a/resources/views/list/groups.twig +++ b/resources/views/list/groups.twig @@ -259,7 +259,17 @@ {% if transaction.transaction_type_type == 'Deposit' %} {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} {% elseif transaction.transaction_type_type == 'Withdrawal' %} - {{ formatAmountBySymbol(transaction.source_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} + {% if 'Loan' == transaction.destination_account_type or 'Mortgage' == transaction.destination_account_type or 'Debt' == transaction.destination_account_type %} + {% if currency.id == transaction.currency_id %} + {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} + {% endif %} + {% if currency.id == transaction.foreign_currency_id %} + {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }} + {% endif %} + + {% else %} + {{ formatAmountBySymbol(transaction.source_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} + {% endif %} {% elseif transaction.transaction_type_type == 'Opening balance' %} {% if transaction.source_account_type == 'Initial balance account' %} {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }}