Allow transactions to be moved to the future and still have the running balance calculated correctly.

This commit is contained in:
James Cole
2025-12-30 16:13:28 +01:00
parent dc3c3bb092
commit 27586a7ec2
6 changed files with 72 additions and 50 deletions

View File

@@ -24,11 +24,12 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use Illuminate\Support\Facades\Log;
use FireflyIII\Support\Facades\FireflyConfig;
/**
* Class TransactionObserver
@@ -42,7 +43,10 @@ class TransactionObserver
Log::debug('Observe "created" of a transaction.');
if (true === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data && (1 === bccomp($transaction->amount, '0') && self::$recalculate)) {
Log::debug('Trigger recalculateForJournal');
AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal);
$journal = $transaction->transactionJournal;
if ($journal instanceof TransactionJournal) {
AccountBalanceCalculator::recalculateForJournal($journal);
}
}
$this->updatePrimaryCurrencyAmount($transaction);
}
@@ -56,7 +60,10 @@ class TransactionObserver
$transaction->native_amount = null;
$transaction->native_foreign_amount = null;
// first normal amount
if ($transaction->transactionCurrency->id !== $userCurrency->id && (null === $transaction->foreign_currency_id || (null !== $transaction->foreign_currency_id && $transaction->foreign_currency_id !== $userCurrency->id))) {
if ($transaction->transactionCurrency->id !== $userCurrency->id &&
(null === $transaction->foreign_currency_id ||
(null !== $transaction->foreign_currency_id &&
$transaction->foreign_currency_id !== $userCurrency->id))) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($transaction->transactionJournal->user->userGroup);
$converter->setIgnoreSettings(true);

View File

@@ -38,7 +38,6 @@ use Illuminate\Support\Collection;
* @method getUserGroup()
* @method getUser()
* @method checkUserGroupAccess(UserRoleEnum $role)
* @method setUser(null|Authenticatable|User $user)
* @method setUserGroupById(int $userGroupId)
*/
interface AttachmentRepositoryInterface

View File

@@ -68,8 +68,7 @@ class JournalUpdateService
private ?Account $destinationAccount = null;
private ?Transaction $destinationTransaction = null;
private array $metaDate
= ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date',
'invoice_date', ];
= ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', '_internal_previous_date'];
private array $metaString
= [
'sepa_cc',
@@ -205,9 +204,7 @@ class JournalUpdateService
$validator->setUser($this->transactionJournal->user);
$result = $validator->validateSource(['id' => $sourceId, 'name' => $sourceName]);
Log::debug(
sprintf('hasValidSourceAccount(%d, "%s") will return %s', $sourceId, $sourceName, var_export($result, true))
);
Log::debug(sprintf('hasValidSourceAccount(%d, "%s") will return %s', $sourceId, $sourceName, var_export($result, true)));
// TODO type overrule the account validator may have a different opinion on the transaction type.
@@ -283,14 +280,7 @@ class JournalUpdateService
$validator->setUser($this->transactionJournal->user);
$validator->source = $this->getValidSourceAccount();
$result = $validator->validateDestination(['id' => $destId, 'name' => $destName]);
Log::debug(
sprintf(
'hasValidDestinationAccount(%d, "%s") will return %s',
$destId,
$destName,
var_export($result, true)
)
);
Log::debug(sprintf('hasValidDestinationAccount(%d, "%s") will return %s', $destId, $destName, var_export($result, true)));
// TODO typeOverrule: the account validator may have another opinion on the transaction type.
@@ -494,6 +484,23 @@ class JournalUpdateService
// do some parsing.
Log::debug(sprintf('Create date value from string "%s".', $value));
$this->transactionJournal->date_tz = $value->format('e');
$res = $value->gt($this->transactionJournal->date);
Log::debug(sprintf('Old date: %s, new date: %s', $this->transactionJournal->date->toW3cString(), $value->toW3cString()));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);
$set = [
'journal' => $this->transactionJournal,
'name' => '_internal_previous_date',
'data' => null,
];
if($res) {
Log::debug('Transaction is set to be AFTER its current date. Save also the "_internal_previous_date"-field.');
$set['data'] = clone $this->transactionJournal->date;
}
if(!$res) {
Log::debug('Transaction is NOT set to be AFTER its current date. Remove the "_internal_previous_date"-field.');
}
$factory->updateOrCreate($set);
}
event(new TriggeredAuditLog($this->transactionJournal->user, $this->transactionJournal, sprintf('update_%s', $fieldName), $this->transactionJournal->{$fieldName}, $value));

View File

@@ -72,7 +72,18 @@ class AccountBalanceCalculator
$set[$transaction->account_id] = $transaction->account;
}
$accounts = new Collection()->push(...$set);
$object->optimizedCalculation($accounts, $transactionJournal->date);
// find meta value:
$date = $transactionJournal->date;
$meta = $transactionJournal->transactionJournalMeta()->where('name', '_internal_previous_date')->where('data', '!=', '')->first();
Log::debug(sprintf('Date used is "%s"', $date->toW3cString()));
if (null !== $meta) {
$date = Carbon::parse($meta->data);
Log::debug(sprintf('Date is overruled with "%s"', $date->toW3cString()));
}
$object->optimizedCalculation($accounts, $date);
}
private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
@@ -93,8 +104,7 @@ class AccountBalanceCalculator
->orderBy('transaction_journals.id', 'DESC')
->orderBy('transaction_journals.description', 'DESC')
->orderBy('transactions.amount', 'DESC')
->where('transactions.account_id', $accountId)
;
->where('transactions.account_id', $accountId);
$notBefore->startOfDay();
$query->where('transaction_journals.date', '<', $notBefore);
@@ -122,8 +132,7 @@ class AccountBalanceCalculator
->orderBy('transaction_journals.order', 'desc')
->orderBy('transaction_journals.id', 'asc')
->orderBy('transaction_journals.description', 'asc')
->orderBy('transactions.amount', 'asc')
;
->orderBy('transactions.amount', 'asc');
if ($accounts->count() > 0) {
$query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray());
}

View File

@@ -75,7 +75,7 @@ class TransactionGroupTransformer extends AbstractTransformer
'recurrence_count',
'recurrence_total',
];
$this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
$this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', '_internal_previous_date'];
}
public function transform(array $group): array