mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-11-02 20:30:37 -06:00
Merge pull request #11992 from firefly-iii/release-1774047683
🤖 Automatically merge the PR into the develop branch.
This commit is contained in:
17 files changed
+254
-226
No files matched your search
@@ -43,7 +43,7 @@ final class DestroyController extends Controller
|
||||
|
||||
public const string RESOURCE_KEY = 'exchange-rates';
|
||||
|
||||
protected array $acceptedRoles = [UserRoleEnum::OWNER];
|
||||
protected array $acceptedRoles = [UserRoleEnum::OWNER];
|
||||
private ExchangeRateRepositoryInterface $repository;
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -102,6 +102,7 @@ final class StoreController extends Controller
|
||||
$existing = $this->repository->updateExchangeRate($existing, $rate);
|
||||
$collection->push($existing);
|
||||
event(new UpdatedCurrencyExchangeRate($existing));
|
||||
|
||||
continue;
|
||||
}
|
||||
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
|
||||
@@ -44,7 +44,7 @@ final class UpdateController extends Controller
|
||||
|
||||
public const string RESOURCE_KEY = 'exchange-rates';
|
||||
|
||||
protected array $acceptedRoles = [UserRoleEnum::OWNER];
|
||||
protected array $acceptedRoles = [UserRoleEnum::OWNER];
|
||||
private ExchangeRateRepositoryInterface $repository;
|
||||
|
||||
public function __construct()
|
||||
@@ -68,7 +68,7 @@ final class UpdateController extends Controller
|
||||
$rate = $request->getRate();
|
||||
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
|
||||
event(new UpdatedCurrencyExchangeRate($exchangeRate));
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
|
||||
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* CreatedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
@@ -22,7 +25,6 @@
|
||||
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -31,7 +33,9 @@ class CreatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public CurrencyExchangeRate $rate,) {
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('CreatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* DestroyedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
@@ -31,8 +34,11 @@ class DestroyedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public TransactionCurrency $from, public TransactionCurrency $to, public UserGroup $userGroup)
|
||||
{
|
||||
public function __construct(
|
||||
public TransactionCurrency $from,
|
||||
public TransactionCurrency $to,
|
||||
public UserGroup $userGroup
|
||||
) {
|
||||
Log::debug(sprintf('DestroyedCurrencyExchangeRate(%s, %s) Event', $from->code, $to->code));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* UpdatedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
@@ -30,7 +33,9 @@ class UpdatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public CurrencyExchangeRate $rate) {
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('UpdatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,11 @@ class UpdatesAccountInformation implements ShouldQueue
|
||||
++$fixed;
|
||||
}
|
||||
// fix account number: // account_number
|
||||
if (array_key_exists('account_number', $oldData) && $oldData['account_number'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
if (
|
||||
array_key_exists('account_number', $oldData)
|
||||
&& $oldData['account_number'] === $trigger->trigger_value
|
||||
&& in_array($trigger->trigger_type, $numberFields, true)
|
||||
) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account account_number, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->iban;
|
||||
$trigger->save();
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* ProcessesExchangeRates.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
@@ -34,13 +37,14 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessesExchangeRates
|
||||
{
|
||||
public function handle(CreatedCurrencyExchangeRate | UpdatedCurrencyExchangeRate | DestroyedCurrencyExchangeRate $event): void
|
||||
public function handle(CreatedCurrencyExchangeRate|DestroyedCurrencyExchangeRate|UpdatedCurrencyExchangeRate $event): void
|
||||
{
|
||||
Preferences::mark();
|
||||
Cache::clear();
|
||||
if ($event instanceof DestroyedCurrencyExchangeRate) {
|
||||
$this->handleCurrency($event->userGroup, $event->from);
|
||||
$this->handleCurrency($event->userGroup, $event->to);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency);
|
||||
@@ -49,18 +53,15 @@ class ProcessesExchangeRates
|
||||
|
||||
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s.', $currency->code));
|
||||
|
||||
$calculator->recalculateForGroupAndCurrency($userGroup, $currency);
|
||||
// $calculator->recalculateForGroup($userGroup);
|
||||
// $calculator->recalculateForGroup($userGroup);
|
||||
|
||||
return;
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,15 +25,8 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Listeners\System;
|
||||
|
||||
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RecalculatesPrimaryCurrencyAmounts
|
||||
@@ -50,8 +43,4 @@ class RecalculatesPrimaryCurrencyAmounts
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -58,6 +58,7 @@ class CurrencyExchangeRate extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class);
|
||||
|
||||
@@ -368,10 +368,21 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
|
||||
|
||||
public function getBudgets(): Collection
|
||||
{
|
||||
if(null === $this->user) {
|
||||
return $this->userGroup->budgets()->orderBy('order', 'ASC')->orderBy('name', 'ASC')->get();
|
||||
if (null === $this->user) {
|
||||
return $this->userGroup
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')
|
||||
->get()
|
||||
;
|
||||
}
|
||||
return $this->user->budgets()->orderBy('order', 'ASC')->orderBy('name', 'ASC')->get();
|
||||
|
||||
return $this->user
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,130 +73,64 @@ class PrimaryAmountRecalculationService
|
||||
}
|
||||
}
|
||||
|
||||
private function resetGenericTables(UserGroup $userGroup): void
|
||||
public function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculateAccounts($userGroup, $currency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
public function recalculateForGroupAndCurrency(UserGroup $userGroup, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
if ('' !== (string)$piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string)$account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string)$event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
if ($limitCurrency->id === $currency->id) {
|
||||
Log::debug(sprintf('Can skip recalculation because user requested the same currencies (%s).', $limitCurrency->code));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recalculateAccountsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
}
|
||||
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null]);
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string)$autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string)$limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$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 $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
|
||||
});
|
||||
})
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (DatabaseBuilder $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$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']);
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
@@ -211,25 +145,25 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Now in calculateTransactionsForCurrency(#%d, %s, %s)', $userGroup->id, $currency->code, $limitCurrency->code));
|
||||
// custom query because of the potential size of this update.
|
||||
$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 $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)
|
||||
->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$q3->whereNot('transactions.transaction_currency_id', $currency->id)
|
||||
->whereNot('transactions.foreign_currency_id', $currency->id);
|
||||
});
|
||||
})
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (DatabaseBuilder $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
|
||||
});
|
||||
})
|
||||
// must be in the limit currency.
|
||||
->where('transactions.transaction_currency_id', $limitCurrency->id)
|
||||
->orWhere('transactions.foreign_currency_id', $limitCurrency->id)
|
||||
->get(['transactions.id']);
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
@@ -239,27 +173,6 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccounts(#%d, %s)', $userGroup->id, $groupCurrency->code));
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int)$account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
$account->touch();
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function collectAccounts(UserGroup $userGroup): Collection
|
||||
{
|
||||
return $userGroup
|
||||
@@ -275,7 +188,30 @@ class PrimaryAmountRecalculationService
|
||||
$q->orWhere('virtual_balance', '!=', '');
|
||||
}
|
||||
})
|
||||
->get();
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccounts(#%d, %s)', $userGroup->id, $groupCurrency->code));
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
$account->touch();
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,13 +225,15 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int)$account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($limitCurrency->id !== $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is NOT in limit currency %s, skip.', $account->name, $limitCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
@@ -364,38 +302,6 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
|
||||
}
|
||||
|
||||
public function recalculateForGroupAndCurrency(UserGroup $userGroup, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
if ($limitCurrency->id === $currency->id) {
|
||||
Log::debug(sprintf('Can skip recalculation because user requested the same currencies (%s).', $limitCurrency->code));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recalculateAccountsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
}
|
||||
|
||||
public function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculateAccounts($userGroup, $currency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
|
||||
{
|
||||
$set = $piggyBank->piggyBankEvents()->get();
|
||||
@@ -410,25 +316,25 @@ class PrimaryAmountRecalculationService
|
||||
*/
|
||||
private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
$converter = new ExchangeRateConverter();
|
||||
$converter = new ExchangeRateConverter();
|
||||
$converter->setUserGroup($userGroup);
|
||||
$converter->setIgnoreSettings(true);
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getPiggyBanks();
|
||||
$set = $set->filter(static fn(PiggyBank $piggyBank): bool => $currency->id !== $piggyBank->transaction_currency_id);
|
||||
$set = $repository->getPiggyBanks();
|
||||
$set = $set->filter(static fn (PiggyBank $piggyBank): bool => $currency->id !== $piggyBank->transaction_currency_id);
|
||||
foreach ($set as $piggyBank) {
|
||||
$piggyBank->encrypted = false;
|
||||
$piggyBank->save();
|
||||
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
$account->pivot->native_current_amount = null;
|
||||
if (0 !== bccomp((string)$account->pivot->current_amount, '0')) {
|
||||
if (0 !== bccomp((string) $account->pivot->current_amount, '0')) {
|
||||
$account->pivot->native_current_amount = $converter->convert(
|
||||
$piggyBank->transactionCurrency,
|
||||
$currency,
|
||||
today(),
|
||||
(string)$account->pivot->current_amount
|
||||
(string) $account->pivot->current_amount
|
||||
);
|
||||
}
|
||||
$account->pivot->save();
|
||||
@@ -437,4 +343,104 @@ class PrimaryAmountRecalculationService
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string) $autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string) $limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetGenericTables(UserGroup $userGroup): void
|
||||
{
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
{
|
||||
if ('' !== (string) $piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string) $account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string) $event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null])
|
||||
;
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -11,16 +11,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
- [Issue 11964](https://github.com/firefly-iii/firefly-iii/issues/11964) ("Left to spend" is not taking into account non-main currency withdrawals (when displaying in primary currency)) reported by @absdjfh
|
||||
- [Issue 11966](https://github.com/firefly-iii/firefly-iii/issues/11966) (Error when trying to export data in CSV (Export data via front end)) reported by @jgmm81
|
||||
- #11969
|
||||
- #11974
|
||||
- #11977
|
||||
- #11978
|
||||
- #11982
|
||||
- [Issue 11969](https://github.com/firefly-iii/firefly-iii/issues/11969) (Problem found when editing a multi-currency record, as well as details in the "Audit log entries") reported by @jgmm81
|
||||
- [PR 11974](https://github.com/firefly-iii/firefly-iii/pull/11974) (Fix typo in SMTP server comment in .env.example) reported by @NorskNoobing
|
||||
- [Discussion 11977](https://github.com/orgs/firefly-iii/discussions/11977) (CSP header `form-action 'self'` prevents form submission because it's a redirect) started by @superrio0187
|
||||
- [Issue 11978](https://github.com/firefly-iii/firefly-iii/issues/11978) (Tags not associated with any record display incorrect information) reported by @jgmm81
|
||||
- [Issue 11982](https://github.com/firefly-iii/firefly-iii/issues/11982) (Foreign currency account value in primary currency does not update after changing exchange rates) reported by @gattacus
|
||||
- Remove old `zoomLevel` / `zoom_level` database references for tags, since they are no longer queries anyway.
|
||||
|
||||
### API
|
||||
|
||||
- #11976
|
||||
- [Issue 11976](https://github.com/firefly-iii/firefly-iii/issues/11976) (New lines are removed from rule description when created using API POST) reported by @AlexRNL
|
||||
|
||||
## v6.5.6 - 2026-03-16
|
||||
|
||||
|
||||
Generated
+9
-9
@@ -5781,27 +5781,27 @@
|
||||
},
|
||||
{
|
||||
"name": "rcrowe/twigbridge",
|
||||
"version": "v0.14.6",
|
||||
"version": "v0.14.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rcrowe/TwigBridge.git",
|
||||
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45"
|
||||
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
|
||||
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
|
||||
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/03a767c8d5c1d74d5f14e9fc754619a271822663",
|
||||
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^9|^10|^11|^12",
|
||||
"illuminate/view": "^9|^10|^11|^12",
|
||||
"illuminate/support": "^9|^10|^11|^12|^13",
|
||||
"illuminate/view": "^9|^10|^11|^12|^13",
|
||||
"php": "^8.1",
|
||||
"twig/twig": "~3.21"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": "^9|^10|^11|^12",
|
||||
"laravel/framework": "^9|^10|^11|^12|^13",
|
||||
"mockery/mockery": "^1.3.1",
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.3.7 || ^10.0 || ^11.0 || ^12.0",
|
||||
"squizlabs/php_codesniffer": "^3.6"
|
||||
@@ -5847,9 +5847,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rcrowe/TwigBridge/issues",
|
||||
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.6"
|
||||
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.7"
|
||||
},
|
||||
"time": "2025-08-20T11:25:49+00:00"
|
||||
"time": "2026-03-20T16:59:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/backtrace",
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ return [
|
||||
// see cer.php for exchange rates feature flag.
|
||||
],
|
||||
'version' => 'develop/2026-03-20',
|
||||
'build_time' => 1773991310,
|
||||
'build_time' => 1774047487,
|
||||
'api_version' => '2.1.0', // field is no longer used.
|
||||
'db_version' => 28, // field is no longer used.
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -7143,9 +7143,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.8.20",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.20.tgz",
|
||||
"integrity": "sha512-xjo9+lbX/P1tQt3xpO2rfJiBppNfUnNIPKgCvNsTKsvTOCro1Qr/geXVg1N47j5ScOSaXAPq8ET93raK3Rr06A==",
|
||||
"version": "25.9.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.9.0.tgz",
|
||||
"integrity": "sha512-mJ4rVRNWOTkqh5xnaGR6iMFT5vEw3Y2MTJhcjinR/7u8cRv6dAfC0ofuePh5fVPxoh395p6JdrJTStCcNW66gg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
||||
+3
-3
@@ -658,9 +658,9 @@ Route::group(
|
||||
// transaction currency API routes that require admin rights:
|
||||
Route::group(
|
||||
[
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionCurrency',
|
||||
'prefix' => 'v1/currencies',
|
||||
'as' => 'api.v1.currencies.',
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionCurrency',
|
||||
'prefix' => 'v1/currencies',
|
||||
'as' => 'api.v1.currencies.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
|
||||
Reference in new issue
Block a user