mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-04-29 02:53:05 +00:00
Rewrite calculation service for #11982
- moved reset methods to this class - Add method `recalculateForGroupAndCurrency` that accepts a $limitCurrency as third argument. - Add `recalculateAccountsForCurrency` and `calculateTransactionsForCurrency` `recalculateForGroupAndCurrency` can recalculate any foreign amount back to the primary currency (just like the class was already capable of) BUT limits the action to foreign amounts in $limitCurrency. This greatly reduces the rework necessary. This means the method can be safely called when changing currency exchange rates. Not all methods in `recalculateForGroupAndCurrency` actually care about the given $limitCurrency but the methods that don't aren't doing much anyway, so it's OK to recalculate everything even though its not necessary.
This commit is contained in:
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Services\Internal\Recalculate;
|
||||
|
||||
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
|
||||
use FireflyIII\Handlers\Observer\TransactionObserver;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AutoBudget;
|
||||
@@ -36,14 +37,16 @@ use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\Builder as DatabaseBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -57,36 +60,176 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var UserGroupRepositoryInterface $repository */
|
||||
$repository = app(UserGroupRepositoryInterface::class);
|
||||
Preferences::mark();
|
||||
|
||||
/** @var UserGroup $userGroup */
|
||||
foreach ($repository->getAll() as $userGroup) {
|
||||
Log::debug('Resetting primary currency amounts for all objects.');
|
||||
$this->resetGenericTables($userGroup);
|
||||
$this->resetPiggyBanks($userGroup);
|
||||
$this->resetBudgets($userGroup);
|
||||
$this->resetTransactions($userGroup);
|
||||
Log::debug('Have now reset all primary amounts to NULL.');
|
||||
$this->recalculateForGroup($userGroup);
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
$transaction?->touch();
|
||||
}
|
||||
TransactionObserver::$recalculate = true;
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
private function calculateTransactionsForCurrency(UserGroup $userGroup, TransactionCurrency $currency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
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);
|
||||
});
|
||||
})
|
||||
// must be in the limit currency.
|
||||
->where('transactions.transaction_currency_id', $limitCurrency->id)
|
||||
->orWhere('transactions.foreign_currency_id', $limitCurrency->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);
|
||||
@@ -98,11 +241,28 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
* TODO this routine must filter on accounts that are NOT in the userGroup's currency.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup): void
|
||||
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
|
||||
{
|
||||
$set = $userGroup
|
||||
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
|
||||
->accounts()
|
||||
->where(static function (EloquentBuilder $q): void {
|
||||
$q->whereNotNull('virtual_balance');
|
||||
@@ -116,10 +276,31 @@ class PrimaryAmountRecalculationService
|
||||
}
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccountsForCurrency(UserGroup $userGroup, TransactionCurrency $groupCurrency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccountsForCurrency(#%d, %s, %s)', $userGroup->id, $groupCurrency->code, $limitCurrency->code));
|
||||
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$account->touch();
|
||||
$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));
|
||||
// TODO it is bad form to call an event from an event but OK.
|
||||
event(new UpdatedExistingAccount($account, []));
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
@@ -183,14 +364,31 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
|
||||
}
|
||||
|
||||
private function recalculateForGroup(UserGroup $userGroup): void
|
||||
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));
|
||||
$this->recalculateAccounts($userGroup);
|
||||
|
||||
// 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);
|
||||
@@ -212,25 +410,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();
|
||||
|
||||
Reference in New Issue
Block a user