diff --git a/app/Console/Commands/Upgrade/AccountCurrencies.php b/app/Console/Commands/Upgrade/AccountCurrencies.php new file mode 100644 index 0000000000..d56d8e8702 --- /dev/null +++ b/app/Console/Commands/Upgrade/AccountCurrencies.php @@ -0,0 +1,155 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountMeta; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use Illuminate\Console\Command; +use Log; +use UnexpectedValueException; + +/** + * Class AccountCurrencies + */ +class AccountCurrencies extends Command +{ + public const CONFIG_NAME = '4780_account_currencies'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Give all accounts proper currency info.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:account-currencies {--F|force : Force the execution of this command.}'; + + /** + * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account. + * + * @return int + */ + public function handle(): int + { + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + + Log::debug('Now in updateAccountCurrencies()'); + + $defaultConfig = (string)config('firefly.default_currency', 'EUR'); + Log::debug(sprintf('System default currency is "%s"', $defaultConfig)); + + $accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']); + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $accounts->each( + function (Account $account) use ($repository, $defaultConfig) { + $repository->setUser($account->user); + // get users preference, fall back to system pref. + + // expand and debug routine. + $defaultCurrencyCode = app('preferences')->getForUser($account->user, 'currencyPreference', $defaultConfig)->data; + Log::debug(sprintf('Default currency code is "%s"', var_export($defaultCurrencyCode, true))); + if (!is_string($defaultCurrencyCode)) { + $defaultCurrencyCode = $defaultConfig; + Log::debug(sprintf('Default currency code is not a string, now set to "%s"', $defaultCurrencyCode)); + } + $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); + $accountCurrency = (int)$repository->getMetaValue($account, 'currency_id'); + $openingBalance = $account->getOpeningBalance(); + $obCurrency = (int)$openingBalance->transaction_currency_id; + + if (null === $defaultCurrency) { + throw new UnexpectedValueException(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode)); + } + Log::debug( + sprintf('Found default currency #%d (%s) while searching for "%s"', $defaultCurrency->id, $defaultCurrency->code, $defaultCurrencyCode) + ); + + // both 0? set to default currency: + if (0 === $accountCurrency && 0 === $obCurrency) { + AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete(); + AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]); + $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); + + return true; + } + + // account is set to 0, opening balance is not? + if (0 === $accountCurrency && $obCurrency > 0) { + AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]); + $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); + + return true; + } + + // do not match and opening balance id is not null. + if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) { + // update opening balance: + $openingBalance->transaction_currency_id = $accountCurrency; + $openingBalance->save(); + $this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); + + return true; + } + + return true; + } + ); + + $this->markAsExecuted(); + + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } +} diff --git a/app/Console/Commands/Upgrade/BackToJournals.php b/app/Console/Commands/Upgrade/BackToJournals.php new file mode 100644 index 0000000000..e13c0ef679 --- /dev/null +++ b/app/Console/Commands/Upgrade/BackToJournals.php @@ -0,0 +1,168 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; + +/** + * Class BackToJournals + */ +class BackToJournals extends Command +{ + public const CONFIG_NAME = '4780_back_to_journals'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Move meta data back to journals, not individual transactions.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:back-to-journals {--F|force : Force the execution of this command.}'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + if (!$this->isMigrated()) { + $this->error('Please run firefly-iii:migrate-to-groups first.'); + } + if ($this->isExecuted() && true !== $this->option('force')) { + $this->info('This command has been executed already.'); + + return 0; + } + if (true === $this->option('force')) { + $this->warn('Forcing the command.'); + } + + $this->migrateAll(); + + $this->info('Updated category and budget info for all journals.'); + $this->markAsExecuted(); + + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + /** + * @return bool + */ + private function isMigrated(): bool + { + $configVar = app('fireflyconfig')->get(MigrateToGroups::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * + */ + private function migrateAll(): void + { + $journals = TransactionJournal::get(); + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + $this->migrateCategoriesForJournal($journal); + $this->migrateBudgetsForJournal($journal); + } + } + + /** + * @param TransactionJournal $journal + */ + private function migrateBudgetsForJournal(TransactionJournal $journal): void + { + // grab category from first transaction + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Budget $budget */ + $budget = $transaction->budgets()->first(); + if (null !== $budget) { + // sync to journal: + $journal->budgets()->sync([(int)$budget->id]); + + // remove from transactions: + $journal->transactions()->each( + function (Transaction $transaction) { + $transaction->budgets()->sync([]); + } + ); + } + } + + /** + * @param TransactionJournal $journal + */ + private function migrateCategoriesForJournal(TransactionJournal $journal): void + { + // grab category from first transaction + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + /** @var Category $category */ + $category = $transaction->categories()->first(); + if (null !== $category) { + // sync to journal: + $journal->categories()->sync([(int)$category->id]); + + // remove from transactions: + $journal->transactions()->each( + function (Transaction $transaction) { + $transaction->categories()->sync([]); + } + ); + } + } +} diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php index 91e6bb3415..856057ebaa 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -21,17 +21,13 @@ namespace FireflyIII\Console\Commands\Upgrade; -use Carbon\Carbon; -use DB; use Exception; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Services\Internal\Destroy\JournalDestroyService; use Illuminate\Console\Command; -use Illuminate\Support\Collection; use Log; /** @@ -43,6 +39,7 @@ use Log; */ class MigrateToGroups extends Command { + public const CONFIG_NAME = '4780_migrated_to_groups'; /** * The console command description. * @@ -54,7 +51,7 @@ class MigrateToGroups extends Command * * @var string */ - protected $signature = 'firefly:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; + protected $signature = 'firefly-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; /** @var TransactionJournalFactory */ private $journalFactory; @@ -62,6 +59,9 @@ class MigrateToGroups extends Command /** @var JournalRepositoryInterface */ private $journalRepository; + /** @var JournalDestroyService */ + private $service; + /** * Create a new command instance. * @@ -72,80 +72,7 @@ class MigrateToGroups extends Command parent::__construct(); $this->journalFactory = app(TransactionJournalFactory::class); $this->journalRepository = app(JournalRepositoryInterface::class); - } - - /** - * @param TransactionJournal $journal - * @param Transaction $transaction - * - * @return int - */ - public function getBudgetId(TransactionJournal $journal, Transaction $transaction): int - { - $budgetId = 0; - if (null !== $transaction->budgets()->first()) { - $budgetId = (int)$transaction->budgets()->first()->id; // done! - Log::debug(sprintf('Transaction #%d has a reference to budget #%d so will use that one.', $transaction->id, $budgetId)); - } - if (0 === $budgetId && $journal->budgets()->first()) { - $budgetId = (int)$journal->budgets()->first()->id; // also done! - Log::debug( - sprintf('Transaction #%d has NO budget, but journal #%d has budget #%d so will use that one.', $transaction->id, $journal->id, $budgetId) - ); - } - Log::debug(sprintf('Final budget ID for journal #%d and transaction #%d is %d', $journal->id, $transaction->id, $budgetId)); - - return $budgetId; - - } - - /** - * @param TransactionJournal $journal - * @param Transaction $transaction - * - * @return int - */ - public function getCategoryId(TransactionJournal $journal, Transaction $transaction): int - { - $categoryId = 0; - if (null !== $transaction->categories()->first()) { - $categoryId = (int)$transaction->categories()->first()->id; // done! - Log::debug(sprintf('Transaction #%d has a reference to category #%d so will use that one.', $transaction->id, $categoryId)); - } - if (0 === $categoryId && $journal->categories()->first()) { - $categoryId = (int)$journal->categories()->first()->id; // also done! - Log::debug( - sprintf('Transaction #%d has NO category, but journal #%d has category #%d so will use that one.', $transaction->id, $journal->id, $categoryId) - ); - } - Log::debug(sprintf('Final category ID for journal #%d and transaction #%d is %d', $journal->id, $transaction->id, $categoryId)); - - return $categoryId; - - } - - /** - * @param TransactionJournal $journal - * @param Transaction $transaction - * - * @return Transaction - * @throws FireflyException - */ - public function getOpposingTransaction(TransactionJournal $journal, Transaction $transaction): Transaction - { - /** @var Transaction $opposing */ - $opposing = $journal->transactions()->where('amount', $transaction->amount * -1)->where('identifier', $transaction->identifier)->first(); - if (null === $opposing) { - $message = sprintf( - 'Could not convert journal #%d ("%s") because transaction #%d has no opposite entry in the database. This requires manual intervention beyond the capabilities of this script. Please open an issue on GitHub.', - $journal->id, $journal->description, $transaction->id - ); - $this->error($message); - throw new FireflyException($message); - } - Log::debug(sprintf('Found opposing transaction #%d for transaction #%d (both part of journal #%d)', $opposing->id, $transaction->id, $journal->id)); - - return $opposing; + $this->service = app(JournalDestroyService::class); } /** @@ -158,6 +85,8 @@ class MigrateToGroups extends Command { if ($this->isMigrated() && true !== $this->option('force')) { $this->info('Database already seems to be migrated.'); + + return 0; } if (true === $this->option('force')) { $this->warn('Forcing the migration.'); @@ -177,13 +106,12 @@ class MigrateToGroups extends Command */ private function isMigrated(): bool { - $configName = 'migrated_to_groups_4780'; - $configVar = app('fireflyconfig')->get($configName, false); + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { return (bool)$configVar->data; } - return false; + return false; // @codeCoverageIgnore } /** @@ -204,9 +132,6 @@ class MigrateToGroups extends Command $this->journalRepository->setUser($journal->user); $this->journalFactory->setUser($journal->user); - /** @var JournalDestroyService $service */ - $service = app(JournalDestroyService::class); - $data = [ // mandatory fields. 'type' => strtolower($journal->transactionType->type), @@ -222,12 +147,21 @@ class MigrateToGroups extends Command /** @var Transaction $transaction */ foreach ($transactions as $transaction) { Log::debug(sprintf('Now going to add transaction #%d to the array.', $transaction->id)); - $budgetId = $this->getBudgetId($journal, $transaction); - $categoryId = $this->getCategoryId($journal, $transaction); - $opposingTr = $this->getOpposingTransaction($journal, $transaction); - $tArray = [ + $budgetId = $this->journalRepository->getJournalBudgetId($journal); + $categoryId = $this->journalRepository->getJournalCategoryId($journal); + $opposingTr = $this->journalRepository->findOpposingTransaction($transaction); - // currency and foreign currency + if (null === $opposingTr) { + $this->error( + sprintf( + 'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.', + $journal->id, $transaction->id + ) + ); + continue; + } + + $tArray = [ 'currency_id' => $transaction->transaction_currency_id, 'foreign_currency_id' => $transaction->foreign_currency_id, 'amount' => $transaction->amount, @@ -239,7 +173,7 @@ class MigrateToGroups extends Command 'category_id' => $categoryId, 'bill_id' => $journal->bill_id, 'notes' => $this->journalRepository->getNoteText($journal), - 'tags' => $journal->tags->pluck('tag')->toArray(), + 'tags' => $this->journalRepository->getTags($journal), 'internal_reference' => $this->journalRepository->getMetaField($journal, 'internal-reference'), 'sepa-cc' => $this->journalRepository->getMetaField($journal, 'sepa-cc'), 'sepa-ct-op' => $this->journalRepository->getMetaField($journal, 'sepa-ct-op'), @@ -270,7 +204,7 @@ class MigrateToGroups extends Command Log::debug('Done calling transaction journal factory'); // delete the old transaction journal. - //$service->destroy($journal); + //$this->service->destroy($journal); // report on result: Log::debug(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); @@ -283,16 +217,8 @@ class MigrateToGroups extends Command */ private function makeGroups(): void { - // grab all split transactions: - $all = Transaction::groupBy('transaction_journal_id')->get(['transaction_journal_id', DB::raw('COUNT(transaction_journal_id) as result')]); - /** @var Collection $filtered */ - $filtered = $all->filter( - function (Transaction $transaction) { - return (int)$transaction->result > 2; - } - ); - $journalIds = array_unique($filtered->pluck('transaction_journal_id')->toArray()); - $splitJournals = TransactionJournal::whereIn('id', $journalIds)->get(); + $splitJournals = $this->journalRepository->getSplitJournals(); + if ($splitJournals->count() > 0) { $this->info(sprintf('Going to un-split %d transaction(s). This could take some time.', $splitJournals->count())); /** @var TransactionJournal $journal */ @@ -300,6 +226,9 @@ class MigrateToGroups extends Command $this->makeGroup($journal); } } + if (0 === $splitJournals->count()) { + $this->info('Found no split journals. Nothing to do.'); + } } /** @@ -307,7 +236,7 @@ class MigrateToGroups extends Command */ private function markAsMigrated(): void { - app('fireflyconfig')->set('migrated_to_groups_4780', true); + app('fireflyconfig')->set(self::CONFIG_NAME, true); } } diff --git a/app/Console/Commands/Upgrade/TransactionIdentifier.php b/app/Console/Commands/Upgrade/TransactionIdentifier.php new file mode 100644 index 0000000000..3bcd0927d7 --- /dev/null +++ b/app/Console/Commands/Upgrade/TransactionIdentifier.php @@ -0,0 +1,160 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use DB; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; +use Illuminate\Database\QueryException; +use Log; +use Schema; + +/** + * Class TransactionIdentifier + */ +class TransactionIdentifier extends Command +{ + public const CONFIG_NAME = '4780_transaction_identifier'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Fixes transaction identifiers.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:transaction-identifiers {--F|force : Force the execution of this command.}'; + + /** + * This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier + * to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example. + * + * In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5. + * + * When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would + * think. So each set gets a number (1,2,3) to keep them apart. + * + * @return int + */ + public function handle(): int + { + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + + // if table does not exist, return false + if (!Schema::hasTable('transaction_journals')) { + return 0; + } + $subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->groupBy(['transaction_journals.id']) + ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]); + /** @noinspection PhpStrictTypeCheckingInspection */ + $result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived')) + ->mergeBindings($subQuery->getQuery()) + ->where('t_count', '>', 2) + ->select(['id', 't_count']); + $journalIds = array_unique($result->pluck('id')->toArray()); + + foreach ($journalIds as $journalId) { + $this->updateJournalidentifiers((int)$journalId); + } + + $this->markAsExecuted(); + + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * grab all positive transactiosn from this journal that are not deleted. for each one, grab the negative opposing one + * which has 0 as an identifier and give it the same identifier. + * + * @param int $journalId + */ + private function updateJournalidentifiers(int $journalId): void + { + $identifier = 0; + $processed = []; + $transactions = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->get(); + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + // find opposing: + $amount = bcmul((string)$transaction->amount, '-1'); + + try { + /** @var Transaction $opposing */ + $opposing = Transaction::where('transaction_journal_id', $journalId) + ->where('amount', $amount)->where('identifier', '=', 0) + ->whereNotIn('id', $processed) + ->first(); + } catch (QueryException $e) { + Log::error($e->getMessage()); + $this->error('Firefly III could not find the "identifier" field in the "transactions" table.'); + $this->error(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version'))); + $this->error('Please run "php artisan migrate" to add this field to the table.'); + $this->info('Then, run "php artisan firefly:upgrade-database" to try again.'); + + return; + } + if (null !== $opposing) { + // give both a new identifier: + $transaction->identifier = $identifier; + $opposing->identifier = $identifier; + $transaction->save(); + $opposing->save(); + $processed[] = $transaction->id; + $processed[] = $opposing->id; + } + ++$identifier; + } + + } +} diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradeDatabase.php new file mode 100644 index 0000000000..d1dd3b52fc --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradeDatabase.php @@ -0,0 +1,79 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use Artisan; +use Illuminate\Console\Command; + +/** + * Class UpgradeDatabase + */ +class UpgradeDatabase extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Executes all upgrade commands.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:upgrade-database {--F|force : Force all upgrades.}'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $commands = [ + 'firefly-iii:transaction-identifiers', + 'firefly-iii:account-currencies', + 'firefly-iii:migrate-to-groups', + 'firefly-iii:back-to-journals', + ]; + $args = []; + if ($this->option('force')) { + $args = ['--force' => true]; + } + foreach ($commands as $command) { + $this->line(sprintf('Now executing %s', $command)); + Artisan::call($command, $args); + $result = Artisan::output(); + echo $result; + } + } +} diff --git a/app/Console/Commands/Upgrade/UpgradeSkeleton.php b/app/Console/Commands/Upgrade/UpgradeSkeleton.php new file mode 100644 index 0000000000..c6629fdb2c --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradeSkeleton.php @@ -0,0 +1,83 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use Illuminate\Console\Command; + +/** + * Class UpgradeSkeleton + */ +class UpgradeSkeleton extends Command +{ + public const CONFIG_NAME = '4780_some_name'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'SOME DESCRIPTION'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:SKELETON {--F|force : Force the execution of this command.}'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + + //$this->markAsExecuted(); + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } +} diff --git a/app/Console/Commands/UpgradeDatabaseX.php b/app/Console/Commands/UpgradeDatabaseX.php index 8b5ef9267e..ac5bcc4114 100644 --- a/app/Console/Commands/UpgradeDatabaseX.php +++ b/app/Console/Commands/UpgradeDatabaseX.php @@ -1,7 +1,7 @@ setTransactionIdentifier(); $this->updateAccountCurrencies(); $this->createNewTypes(); $this->line('Updating currency information..'); @@ -277,112 +276,8 @@ class UpgradeDatabase extends Command } } - /** - * This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier - * to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example. - * - * In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5. - * - * When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would - * think. So each set gets a number (1,2,3) to keep them apart. - */ - public function setTransactionIdentifier(): void - { - // if table does not exist, return false - if (!Schema::hasTable('transaction_journals')) { - return; - } - $subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNull('transaction_journals.deleted_at') - ->whereNull('transactions.deleted_at') - ->groupBy(['transaction_journals.id']) - ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]); - /** @noinspection PhpStrictTypeCheckingInspection */ - $result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived')) - ->mergeBindings($subQuery->getQuery()) - ->where('t_count', '>', 2) - ->select(['id', 't_count']); - $journalIds = array_unique($result->pluck('id')->toArray()); - foreach ($journalIds as $journalId) { - $this->updateJournalidentifiers((int)$journalId); - } - } - - /** - * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function updateAccountCurrencies(): void - { - Log::debug('Now in updateAccountCurrencies()'); - - $defaultConfig = (string)config('firefly.default_currency', 'EUR'); - Log::debug(sprintf('System default currency is "%s"', $defaultConfig)); - - $accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']); - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $accounts->each( - function (Account $account) use ($repository, $defaultConfig) { - $repository->setUser($account->user); - // get users preference, fall back to system pref. - - // expand and debug routine. - $defaultCurrencyCode = app('preferences')->getForUser($account->user, 'currencyPreference', $defaultConfig)->data; - Log::debug(sprintf('Default currency code is "%s"', var_export($defaultCurrencyCode, true))); - if (!is_string($defaultCurrencyCode)) { - $defaultCurrencyCode = $defaultConfig; - Log::debug(sprintf('Default currency code is not a string, now set to "%s"', $defaultCurrencyCode)); - } - $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); - $accountCurrency = (int)$repository->getMetaValue($account, 'currency_id'); - $openingBalance = $account->getOpeningBalance(); - $obCurrency = (int)$openingBalance->transaction_currency_id; - - if (null === $defaultCurrency) { - throw new UnexpectedValueException(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode)); - } - Log::debug( - sprintf('Found default currency #%d (%s) while searching for "%s"', $defaultCurrency->id, $defaultCurrency->code, $defaultCurrencyCode) - ); - - // both 0? set to default currency: - if (0 === $accountCurrency && 0 === $obCurrency) { - AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete(); - AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]); - $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); - - return true; - } - - // account is set to 0, opening balance is not? - if (0 === $accountCurrency && $obCurrency > 0) { - AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]); - $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); - - return true; - } - - // do not match and opening balance id is not null. - if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) { - // update opening balance: - $openingBalance->transaction_currency_id = $accountCurrency; - $openingBalance->save(); - $this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); - - return true; - } - - return true; - } - ); - - } /** * This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for @@ -638,50 +533,7 @@ class UpgradeDatabase extends Command } - /** - * grab all positive transactiosn from this journal that are not deleted. for each one, grab the negative opposing one - * which has 0 as an identifier and give it the same identifier. - * - * @param int $journalId - */ - private function updateJournalidentifiers(int $journalId): void - { - $identifier = 0; - $processed = []; - $transactions = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->get(); - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - // find opposing: - $amount = bcmul((string)$transaction->amount, '-1'); - try { - /** @var Transaction $opposing */ - $opposing = Transaction::where('transaction_journal_id', $journalId) - ->where('amount', $amount)->where('identifier', '=', 0) - ->whereNotIn('id', $processed) - ->first(); - } catch (QueryException $e) { - Log::error($e->getMessage()); - $this->error('Firefly III could not find the "identifier" field in the "transactions" table.'); - $this->error(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version'))); - $this->error('Please run "php artisan migrate" to add this field to the table.'); - $this->info('Then, run "php artisan firefly:upgrade-database" to try again.'); - - return; - } - if (null !== $opposing) { - // give both a new identifier: - $transaction->identifier = $identifier; - $opposing->identifier = $identifier; - $transaction->save(); - $opposing->save(); - $processed[] = $transaction->id; - $processed[] = $opposing->id; - } - ++$identifier; - } - - } /** * This method makes sure that the tranaction uses the same currency as the source account does.