From e2a20dd63dc592e33f69fcb4582d760fb6ddf9ae Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 21 Dec 2024 11:20:48 +0100 Subject: [PATCH] Command to recalculate foreign amounts. --- .../Correction/CorrectionSkeleton.php.stub | 1 + .../Correction/RecalculateNativeAmounts.php | 219 ++++++++++++++++++ app/Factory/PiggyBankFactory.php | 11 +- app/Handlers/Observer/PiggyBankObserver.php | 7 +- app/Handlers/Observer/TransactionObserver.php | 2 +- app/Support/Navigation.php | 22 +- ..._12_19_061003_add_native_amount_column.php | 2 +- 7 files changed, 245 insertions(+), 19 deletions(-) create mode 100644 app/Console/Commands/Correction/RecalculateNativeAmounts.php diff --git a/app/Console/Commands/Correction/CorrectionSkeleton.php.stub b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub index 801dd0d845..8f620c0d22 100644 --- a/app/Console/Commands/Correction/CorrectionSkeleton.php.stub +++ b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub @@ -10,6 +10,7 @@ use Illuminate\Console\Command; */ class CorrectionSkeleton extends Command { + use ShowsFriendlyMessages; protected $description = 'DESCRIPTION HERE'; protected $signature = 'firefly-iii:CORR_COMMAND'; diff --git a/app/Console/Commands/Correction/RecalculateNativeAmounts.php b/app/Console/Commands/Correction/RecalculateNativeAmounts.php new file mode 100644 index 0000000000..90ca01c508 --- /dev/null +++ b/app/Console/Commands/Correction/RecalculateNativeAmounts.php @@ -0,0 +1,219 @@ + ['native_amount', 'native_foreign_amount'], // works + + + Log::debug('Will update all native amounts. This may take some time.'); + $this->friendlyWarning('Recalculating native amounts for all objects. This may take some time!'); + /** @var UserGroupRepositoryInterface $repository */ + $repository = app(UserGroupRepositoryInterface::class); + /** @var UserGroup $userGroup */ + foreach ($repository->getAll() as $userGroup) { + $this->recalculateForGroup($userGroup); + } + + + return 0; + } + + private function recalculateForGroup(UserGroup $userGroup): void + { + Log::debug(sprintf('Now recalculating for user group #%d', $userGroup->id)); + $this->recalculateAccounts($userGroup); + + // do a check with the group's currency so we can skip some stuff. + $currency = app('amount')->getDefaultCurrencyByUserGroup($userGroup); + + $this->recalculatePiggyBanks($userGroup, $currency); + $this->recalculateBudgets($userGroup, $currency); + $this->recalculateAvailableBudgets($userGroup, $currency); + $this->recalculateBills($userGroup, $currency); + $this->calculateTransactions($userGroup, $currency); + + } + + private function recalculateAccounts(UserGroup $userGroup): void + { + $set = $userGroup->accounts()->where(function (EloquentBuilder $q) { + $q->whereNotNull('virtual_balance'); + $q->orWhere('virtual_balance', '!=', ''); + })->get(); + /** @var Account $account */ + foreach ($set as $account) { + $account->touch(); + } + Log::debug(sprintf('Recalculated %d accounts', $set->count())); + } + + private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void + { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $set = $repository->getPiggyBanks(); + $set = $set->filter( + static function (PiggyBank $piggyBank) use ($currency) { + return $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($account->pivot->current_amount, '0')) { + $account->pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $currency, today(), $account->pivot->current_amount); + } + $account->pivot->save(); + } + $this->recalculatePiggyBankEvents($piggyBank); + } + Log::debug(sprintf('Recalculated %d piggy banks.', $set->count())); + + } + + private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void + { + $set = $piggyBank->piggyBankEvents()->get(); + $set->each( + static function (PiggyBankEvent $event) { + $event->touch(); + } + ); + Log::debug(sprintf('Recalculated %d piggy bank events.', $set->count())); + } + + private function recalculateBudgets(UserGroup $userGroup, TransactionCurrency $currency): void + { + $set = $userGroup->budgets()->get(); + /** @var Budget $budget */ + foreach ($set as $budget) { + $this->recalculateBudgetLimits($budget, $currency); + $this->recalculateAutoBudgets($budget, $currency); + } + Log::debug(sprintf('Recalculated %d budgets.', $set->count())); + } + + private function recalculateBudgetLimits(Budget $budget, TransactionCurrency $currency): void + { + $set = $budget->budgetlimits()->where('transaction_currency_id', '!=', $currency->id)->get(); + /** @var BudgetLimit $limit */ + foreach ($set as $limit) { + Log::debug(sprintf('Will now touch BL #%d', $limit->id)); + $limit->touch(); + Log::debug(sprintf('Done with touch BL #%d', $limit->id)); + } + Log::debug(sprintf('Recalculated %d budget limits.', $set->count())); + } + + private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void + { + $set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get(); + /** @var AutoBudget $autoBudget */ + foreach ($set as $autoBudget) { + $autoBudget->touch(); + } + Log::debug(sprintf('Recalculated %d auto budgets.', $set->count())); + } + + private function recalculateBills(UserGroup $userGroup, TransactionCurrency $currency): void + { + $set = $userGroup->bills()->where('transaction_currency_id', '!=', $currency->id)->get(); + /** @var Bill $bill */ + foreach ($set as $bill) { + $bill->touch(); + } + Log::debug(sprintf('Recalculated %d bills.', $set->count())); + } + + private function recalculateAvailableBudgets(UserGroup $userGroup, TransactionCurrency $currency): void + { + Log::debug('Start with available budgets.'); + $set = $userGroup->availableBudgets()->where('transaction_currency_id', '!=', $currency->id)->get(); + /** @var AvailableBudget $budget */ + foreach ($set as $budget) { + $budget->touch(); + } + Log::debug(sprintf('Recalculated %d available budgets.', $set->count())); + } + + 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 $q) use ($currency) { + $q->whereNot('transactions.transaction_currency_id', $currency->id) + ->orWhereNot('transactions.foreign_currency_id', $currency->id); + }) + ->get(['transactions.id']); + foreach ($set as $item) { + // here we are. + $transaction = Transaction::find($item->id); + $transaction->touch(); + } + Log::debug(sprintf('Recalculated %d transactions.', $set->count())); + } +} diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index 1cce498936..a5ddedb090 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -88,7 +88,7 @@ class PiggyBankFactory try { /** @var PiggyBank $piggyBank */ - $piggyBank = PiggyBank::create($piggyBankData); + $piggyBank = PiggyBank::createQuietly($piggyBankData); } catch (QueryException $e) { app('log')->error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyBankData); @@ -103,7 +103,6 @@ class PiggyBankFactory $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); - $piggyBank->save(); } } // try also with ID @@ -112,10 +111,12 @@ class PiggyBankFactory $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); - $piggyBank->save(); } } - + Log::debug('Touch piggy bank'); + $piggyBank->encrypted = false; + $piggyBank->save(); + $piggyBank->touch(); return $piggyBank; } @@ -184,7 +185,7 @@ class PiggyBankFactory $order = $data['order']; } $piggyBank->order = $order; - $piggyBank->save(); + $piggyBank->saveQuietly(); return $piggyBank; } diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 32c07b82c2..4f57ed2917 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -48,7 +48,12 @@ class PiggyBankObserver private function updateNativeAmount(PiggyBank $piggyBank): void { - $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($piggyBank->accounts()->first()?->user->userGroup); + $group =$piggyBank->accounts()->first()?->user->userGroup; + if(null === $group) { + Log::debug(sprintf('No account(s) yet for piggy bank #%d.', $piggyBank->id)); + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($group); if(null === $userCurrency) { return; } diff --git a/app/Handlers/Observer/TransactionObserver.php b/app/Handlers/Observer/TransactionObserver.php index ed0eda2fce..23b0fac316 100644 --- a/app/Handlers/Observer/TransactionObserver.php +++ b/app/Handlers/Observer/TransactionObserver.php @@ -74,7 +74,7 @@ class TransactionObserver $transaction->native_amount = $converter->convert($transaction->transactionCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->amount); } // then foreign amount - if ($transaction->foreignCurrency?->id !== $userCurrency->id && null !== $transaction->foreign_amount) { + if ($transaction->foreignCurrency?->id !== $userCurrency->id && null !== $transaction->foreign_amount && null !== $transaction->foreignCurrency) { $converter = new ExchangeRateConverter(); $converter->setIgnoreSettings(true); $transaction->native_foreign_amount = $converter->convert($transaction->foreignCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->foreign_amount); diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index f048ae0f68..b195f17f0a 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -161,7 +161,7 @@ class Navigation public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon { $date = clone $theDate; - Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); + //Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); $functionMap = [ '1D' => 'startOfDay', 'daily' => 'startOfDay', @@ -186,25 +186,25 @@ class Navigation if (array_key_exists($repeatFreq, $functionMap)) { $function = $functionMap[$repeatFreq]; - Log::debug(sprintf('Function is ->%s()', $function)); +// Log::debug(sprintf('Function is ->%s()', $function)); if (array_key_exists($function, $parameterMap)) { - Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); +// Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); +// Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } $date->{$function}(); // @phpstan-ignore-line - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); +// Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { $skipTo = $date->month > 7 ? 6 : 0; $date->startOfYear()->addMonths($skipTo); - Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); +// Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); +// Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } @@ -220,13 +220,13 @@ class Navigation default => null, }; if (null !== $result) { - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); +// Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $result; } if ('custom' === $repeatFreq) { - Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); +// Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); return $date; // the date is already at the start. } @@ -238,7 +238,7 @@ class Navigation public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon { $currentEnd = clone $end; - Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); + //Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); $functionMap = [ '1D' => 'endOfDay', @@ -327,7 +327,7 @@ class Navigation if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } - Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String())); +// Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String())); return $currentEnd; } diff --git a/database/migrations/2024_12_19_061003_add_native_amount_column.php b/database/migrations/2024_12_19_061003_add_native_amount_column.php index 9d486328d4..6dc827577b 100644 --- a/database/migrations/2024_12_19_061003_add_native_amount_column.php +++ b/database/migrations/2024_12_19_061003_add_native_amount_column.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { private array $tables = [ - // !!! this array is also in PreferencesEventHandler + // !!! this array is also in PreferencesEventHandler + RecalculateNativeAmountsCommand 'accounts' => ['native_virtual_balance'], // works. 'account_piggy_bank' => ['native_current_amount'], // works 'auto_budgets' => ['native_amount'], // works