Refactor upgrade and verify commands.

This commit is contained in:
James Cole
2019-03-23 18:58:06 +01:00
parent 1b0be2a47e
commit ce30375341
31 changed files with 909 additions and 513 deletions

View File

@@ -26,9 +26,9 @@ use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\User;
use Illuminate\Console\Command;
use Log;
use UnexpectedValueException;
/**
* Class AccountCurrencies
@@ -49,6 +49,9 @@ class AccountCurrencies extends Command
*/
protected $signature = 'firefly-iii:account-currencies {--F|force : Force the execution of this command.}';
/** @var AccountRepositoryInterface */
private $repository;
/**
* Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account.
*
@@ -56,78 +59,21 @@ class AccountCurrencies extends Command
*/
public function handle(): int
{
$this->repository = app(AccountRepositoryInterface::class);
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
return 0;
}
Log::debug('Now in updateAccountCurrencies()');
$this->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;
}
);
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified and fixed account currencies in %s seconds.', $end));
$this->markAsExecuted();
return 0;
}
@@ -152,4 +98,87 @@ class AccountCurrencies extends Command
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
}
/**
* @param Account $account
* @param TransactionCurrency $currency
*/
private function updateAccount(Account $account, TransactionCurrency $currency): void
{
$this->repository->setUser($account->user);
$accountCurrency = (int)$this->repository->getMetaValue($account, 'currency_id');
$openingBalance = $account->getOpeningBalance();
$obCurrency = (int)$openingBalance->transaction_currency_id;
// 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' => $currency->id]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $currency->code));
return;
}
// 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, $currency->code));
return;
}
// 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));
}
}
/**
*
*/
private function updateAccountCurrencies(): void
{
$defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR');
$users = User::get();
foreach ($users as $user) {
$this->updateCurrenciesForUser($user, $defaultCurrencyCode);
}
}
/**
* @param User $user
* @param string $systemCurrencyCode
*/
private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void
{
$accounts = $user->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])
->get(['accounts.*']);
// get user's currency preference:
$defaultCurrencyCode = app('preferences')->getForUser($user, 'currencyPreference', $systemCurrencyCode)->data;
if (!is_string($defaultCurrencyCode)) {
$defaultCurrencyCode = $systemCurrencyCode;
}
/** @var TransactionCurrency $defaultCurrency */
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
if (null === $defaultCurrency) {
$this->error(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode));
return;
}
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $defaultCurrency);
}
}
}

View File

@@ -21,6 +21,7 @@
namespace FireflyIII\Console\Commands\Upgrade;
use DB;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Transaction;
@@ -53,6 +54,7 @@ class BackToJournals extends Command
*/
public function handle(): int
{
$start = microtime(true);
if (!$this->isMigrated()) {
$this->error('Please run firefly-iii:migrate-to-groups first.');
}
@@ -66,13 +68,35 @@ class BackToJournals extends Command
}
$this->migrateAll();
$this->info('Updated category and budget info for all journals.');
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Updated category and budget info for all transaction journals in %s seconds.', $end));
$this->markAsExecuted();
return 0;
}
/**
* @return array
*/
private function getIdsForBudgets(): array
{
$transactions = DB::table('budget_transaction')->distinct()->get(['transaction_id'])->pluck('transaction_id')->toArray();
return DB::table('transactions')->whereIn('transactions.id', $transactions)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(
);
}
/**
* @return array
*/
private function getIdsForCategories(): array
{
$transactions = DB::table('category_transaction')->distinct()->get(['transaction_id'])->pluck('transaction_id')->toArray();
return DB::table('transactions')->whereIn('transactions.id', $transactions)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(
);
}
/**
* @return bool
*/
@@ -112,10 +136,24 @@ class BackToJournals extends Command
*/
private function migrateAll(): void
{
$journals = TransactionJournal::get();
$this->migrateBudgets();
$this->migrateCategories();
// empty tables
DB::table('budget_transaction')->delete();
DB::table('categories_transaction')->delete();
}
/**
*
*/
private function migrateBudgets(): void
{
$journalIds = $this->getIdsForBudgets();
$journals = TransactionJournal::whereIn('id', $journalIds)->with(['transactions', 'budgets', 'transactions.budgets'])->get();
$this->line(sprintf('Check %d transaction journals for budget info.', $journals->count()));
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->migrateCategoriesForJournal($journal);
$this->migrateBudgetsForJournal($journal);
}
}
@@ -127,19 +165,38 @@ class BackToJournals extends Command
{
// grab category from first transaction
/** @var Transaction $transaction */
$transaction = $journal->transactions()->first();
$transaction = $journal->transactions->first();
if (null === $transaction) {
$this->info(sprintf('Transaction journal #%d has no transactions. Will be fixed later.', $journal->id));
return;
}
/** @var Budget $budget */
$budget = $transaction->budgets()->first();
if (null !== $budget) {
$budget = $transaction->budgets->first();
/** @var Budget $journalBudget */
$journalBudget = $journal->budgets->first();
if (null !== $budget && null !== $journalBudget && $budget->id !== $journalBudget->id) {
// sync to journal:
$journal->budgets()->sync([(int)$budget->id]);
}
// remove from transactions:
$journal->transactions()->each(
function (Transaction $transaction) {
$transaction->budgets()->sync([]);
}
);
// budget in transaction overrules journal.
if (null === $budget && null !== $journalBudget) {
$journal->budgets()->sync([]);
}
}
/**
*
*/
private function migrateCategories(): void
{
$journalIds = $this->getIdsForCategories();
$journals = TransactionJournal::whereIn('id', $journalIds)->with(['transactions', 'categories', 'transactions.categories'])->get();
$this->line(sprintf('Check %d transaction journals for category info.', $journals->count()));
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->migrateCategoriesForJournal($journal);
}
}
@@ -150,19 +207,24 @@ class BackToJournals extends Command
{
// grab category from first transaction
/** @var Transaction $transaction */
$transaction = $journal->transactions()->first();
$transaction = $journal->transactions->first();
if (null === $transaction) {
$this->info(sprintf('Transaction journal #%d has no transactions. Will be fixed later.', $journal->id));
return;
}
/** @var Category $category */
$category = $transaction->categories()->first();
if (null !== $category) {
$category = $transaction->categories->first();
/** @var Category $journalCategory */
$journalCategory = $journal->categories->first();
if (null !== $category && null !== $journalCategory && $category->id !== $journalCategory->id) {
// sync to journal:
$journal->categories()->sync([(int)$category->id]);
}
// remove from transactions:
$journal->transactions()->each(
function (Transaction $transaction) {
$transaction->categories()->sync([]);
}
);
// category in transaction overrules journal.
if (null === $category && null !== $journalCategory) {
$journal->categories()->sync([]);
}
}
}

View File

@@ -54,12 +54,13 @@ class BudgetLimitCurrency extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
return 0;
}
$count = 0;
$budgetLimits = BudgetLimit::get();
/** @var BudgetLimit $budgetLimit */
foreach ($budgetLimits as $budgetLimit) {
@@ -75,10 +76,16 @@ class BudgetLimitCurrency extends Command
$this->line(
sprintf('Budget limit #%d (part of budget "%s") now has a currency setting (%s).', $budgetLimit->id, $budget->name, $currency->name)
);
$count++;
}
}
}
}
if (0 === $count) {
$this->info('All budget limits are correct.');
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified budget limits in %s seconds.', $end));
$this->markAsExecuted();

View File

@@ -57,6 +57,7 @@ class CCLiabilities extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -77,6 +78,11 @@ class CCLiabilities extends Command
if ($accounts->count() > 0) {
$this->info('Credit card liability types are no longer supported and have been converted to generic debts. See: http://bit.ly/FF3-credit-cards');
}
if (0 === $accounts->count()) {
$this->info('No incorrectly stored credit card liabilities.');
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified credit card liabilities in %s seconds', $end));
$this->markAsExecuted();
return 0;

View File

@@ -26,13 +26,13 @@ namespace FireflyIII\Console\Commands\Upgrade;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Log;
/**
@@ -54,6 +54,14 @@ class JournalCurrencies extends Command
* @var string
*/
protected $signature = 'firefly-iii:journal-currencies {--F|force : Force the execution of this command.}';
/** @var array */
private $accountCurrencies;
/** @var AccountRepositoryInterface */
private $accountRepos;
/** @var CurrencyRepositoryInterface */
private $currencyRepos;
/** @var JournalRepositoryInterface */
private $journalRepos;
/**
* Execute the console command.
@@ -62,6 +70,12 @@ class JournalCurrencies extends Command
*/
public function handle(): int
{
$this->accountCurrencies = [];
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
$this->journalRepos = app(JournalRepositoryInterface::class);
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -69,106 +83,76 @@ class JournalCurrencies extends Command
}
$this->updateTransferCurrencies();
$this->updateOtherCurrencies();
$this->updateOtherJournalsCurrencies();
$this->markAsExecuted();
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified and fixed transaction currencies in %s seconds.', $end));
return 0;
}
/**
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
* @param Account $account
*
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @return TransactionCurrency|null
*/
public function updateOtherCurrencies(): void
private function getCurrency(Account $account): ?TransactionCurrency
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
/** @var AccountRepositoryInterface $accountRepos */
$accountRepos = app(AccountRepositoryInterface::class);
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereIn('transaction_types.type', [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
->get(['transaction_journals.*']);
$accountId = $account->id;
if (isset($this->accountCurrencies[$accountId]) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (isset($this->accountCurrencies[$accountId]) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currencyId = (int)$this->accountRepos->getMetaValue($account, 'currency_id');
$result = $this->currencyRepos->findNull($currencyId);
if (null === $result) {
$this->accountCurrencies[$accountId] = 0;
$set->each(
function (TransactionJournal $journal) use ($repository, $accountRepos) {
// get the transaction with the asset account in it:
/** @var Transaction $transaction */
$transaction = $journal->transactions()
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->first(['transactions.*']);
if (null === $transaction) {
return;
}
$accountRepos->setUser($journal->user);
/** @var Account $account */
$account = $transaction->account;
$currency = $repository->findNull((int)$accountRepos->getMetaValue($account, 'currency_id'));
if (null === $currency) {
return;
}
$transactions = $journal->transactions()->get();
$transactions->each(
function (Transaction $transaction) use ($currency) {
if (null === $transaction->transaction_currency_id) {
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
return null;
}
$this->accountCurrencies[$accountId] = $result;
return $result;
// when mismatch in transaction:
if (!((int)$transaction->transaction_currency_id === (int)$currency->id)) {
$transaction->foreign_currency_id = (int)$transaction->transaction_currency_id;
$transaction->foreign_amount = $transaction->amount;
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
}
);
// also update the journal, of course:
$journal->transaction_currency_id = $currency->id;
$journal->save();
}
);
}
/**
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
* like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
* @param TransactionJournal $transfer
*
* A transfer always has the
*
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
* transactions.
* @return Transaction|null
*/
public function updateTransferCurrencies(): void
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)
->get(['transaction_journals.*']);
return $transfer->transactions->firstWhere('amount', '>', 0);
}
$set->each(
function (TransactionJournal $transfer) {
// select all "source" transactions:
/** @var Collection $transactions */
$transactions = $transfer->transactions()->where('amount', '<', 0)->get();
$transactions->each(
function (Transaction $transaction) {
$this->updateTransactionCurrency($transaction);
$this->updateJournalCurrency($transaction);
}
);
/**
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getFirstAssetTransaction(TransactionJournal $journal): ?Transaction
{
$result = $journal->transactions->first(
function (Transaction $transaction) {
return AccountType::ASSET === $transaction->account->accountType->type;
}
);
return $result;
}
/**
* @param TransactionJournal $transfer
*
* @return Transaction|null
*/
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
return $transfer->transactions->firstWhere('amount', '<', 0);
}
/**
@@ -195,17 +179,12 @@ class JournalCurrencies extends Command
/**
* This method makes sure that the transaction journal uses the currency given in the transaction.
*
* @param Transaction $transaction
* @param TransactionJournal $journal
* @param Transaction $transaction
*/
private function updateJournalCurrency(Transaction $transaction): void
private function updateJournalCurrency(TransactionJournal $journal, Transaction $transaction): void
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
/** @var AccountRepositoryInterface $accountRepos */
$accountRepos = app(AccountRepositoryInterface::class);
$accountRepos->setUser($transaction->account->user);
$currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id'));
$journal = $transaction->transactionJournal;
$currency = $this->getCurrency($transaction->account);
$currencyCode = $journal->transactionCurrency->code ?? '(nothing)';
if (null === $currency) {
@@ -225,11 +204,71 @@ class JournalCurrencies extends Command
$journal->transaction_currency_id = $currency->id;
$journal->save();
}
}
/**
* This method makes sure that the tranaction uses the same currency as the source account does.
* @param TransactionJournal $journal
*/
private function updateOtherJournalCurrency(TransactionJournal $journal): void
{
$transaction = $this->getFirstAssetTransaction($journal);
if (null === $transaction) {
return;
}
/** @var Account $account */
$account = $transaction->account;
$currency = $this->getCurrency($account);
if (null === $currency) {
return;
}
$journal->transactions->each(
function (Transaction $transaction) use ($currency) {
if (null === $transaction->transaction_currency_id) {
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
// when mismatch in transaction:
if (!((int)$transaction->transaction_currency_id === (int)$currency->id)) {
$transaction->foreign_currency_id = (int)$transaction->transaction_currency_id;
$transaction->foreign_amount = $transaction->amount;
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
}
);
// also update the journal, of course:
$journal->transaction_currency_id = $currency->id;
$journal->save();
}
/**
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
*
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
private function updateOtherJournalsCurrencies(): void
{
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNotIn('transaction_types.type', [TransactionType::TRANSFER])
->with(['transactions', 'transactions.account', 'transactions.account.accountType'])
->get(['transaction_journals.*']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateOtherJournalCurrency($journal);
}
}
/**
* This method makes sure that the transaction uses the same currency as the source account does.
* If not, the currency is updated to include a reference to its original currency as the "foreign" currency.
*
* The transaction that is sent to this function MUST be the source transaction (amount negative).
@@ -242,116 +281,108 @@ class JournalCurrencies extends Command
*
* @param Transaction $transaction
*/
private function updateTransactionCurrency(Transaction $transaction): void
private function updateTransactionCurrency(TransactionJournal $journal, Transaction $source, Transaction $destination): void
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
/** @var AccountRepositoryInterface $accountRepos */
$accountRepos = app(AccountRepositoryInterface::class);
/** @var JournalRepositoryInterface $journalRepos */
$journalRepos = app(JournalRepositoryInterface::class);
$user = $journal->user;
$sourceAccount = $source->account;
$destAccount = $destination->account;
$this->accountRepos->setUser($user);
$this->journalRepos->setUser($user);
$this->currencyRepos->setUser($user);
$accountRepos->setUser($transaction->account->user);
$journalRepos->setUser($transaction->account->user);
$currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id'));
if (null === $currency) {
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $transaction->account->id, $transaction->account->name));
$sourceAccountCurrency = $this->getCurrency($sourceAccount);
$destAccountCurrency = $this->getCurrency($destAccount);
if (null === $sourceAccountCurrency) {
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $sourceAccount->id, $sourceAccount->name));
return;
}
// has no currency ID? Must have, so fill in using account preference:
if (null === $transaction->transaction_currency_id) {
$transaction->transaction_currency_id = (int)$currency->id;
Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $transaction->id, $currency->code));
$transaction->save();
if (null === $source->transaction_currency_id) {
$source->transaction_currency_id = (int)$sourceAccountCurrency->id;
Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $source->id, $sourceAccountCurrency->code));
$source->save();
}
// does not match the source account (see above)? Can be fixed
// when mismatch in transaction and NO foreign amount is set:
if (!((int)$transaction->transaction_currency_id === (int)$currency->id) && null === $transaction->foreign_amount) {
if (!((int)$source->transaction_currency_id === (int)$sourceAccountCurrency->id) && null === $source->foreign_amount) {
Log::debug(
sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$transaction->id,
$transaction->transaction_currency_id,
$currency->id,
$transaction->amount
$source->id,
$source->transaction_currency_id,
$sourceAccountCurrency->id,
$source->amount
)
);
$transaction->transaction_currency_id = (int)$currency->id;
$transaction->save();
$source->transaction_currency_id = (int)$sourceAccountCurrency->id;
$source->save();
}
// grab opposing transaction:
/** @var TransactionJournal $journal */
$journal = $transaction->transactionJournal;
/** @var Transaction $opposing */
$opposing = $journal->transactions()->where('amount', '>', 0)->where('identifier', $transaction->identifier)->first();
$opposingCurrency = $repository->findNull((int)$accountRepos->getMetaValue($opposing->account, 'currency_id'));
if (null === $opposingCurrency) {
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $opposing->account->id, $opposing->account->name));
if (null === $destAccountCurrency) {
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $destAccount->id, $destAccount->name));
return;
}
// if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions:
if ((int)$opposingCurrency->id === (int)$currency->id) {
if ((int)$destAccountCurrency->id === (int)$sourceAccountCurrency->id) {
// update both transactions to match:
$transaction->foreign_amount = null;
$transaction->foreign_currency_id = null;
$opposing->foreign_amount = null;
$opposing->foreign_currency_id = null;
$opposing->transaction_currency_id = $currency->id;
$transaction->save();
$opposing->save();
$source->foreign_amount = null;
$source->foreign_currency_id = null;
$destination->foreign_amount = null;
$destination->foreign_currency_id = null;
$source->save();
$destination->save();
Log::debug(
sprintf(
'Currency for account "%s" is %s, and currency for account "%s" is also
%s, so %s #%d (#%d and #%d) has been verified to be to %s exclusively.',
$opposing->account->name, $opposingCurrency->code,
$transaction->account->name, $transaction->transactionCurrency->code,
$destAccount->name, $destAccountCurrency->code,
$sourceAccount->name, $sourceAccountCurrency->code,
$journal->transactionType->type, $journal->id,
$transaction->id, $opposing->id, $currency->code
$source->id, $destination->id, $sourceAccountCurrency->code
)
);
return;
}
// if destination account currency is different, both transactions must have this currency as foreign currency id.
if (!((int)$opposingCurrency->id === (int)$currency->id)) {
$transaction->foreign_currency_id = $opposingCurrency->id;
$opposing->foreign_currency_id = $opposingCurrency->id;
$transaction->save();
$opposing->save();
Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $transaction->id, $opposing->id));
if (!((int)$destAccountCurrency->id === (int)$sourceAccountCurrency->id)) {
$source->foreign_currency_id = $destAccountCurrency->id;
$destination->foreign_currency_id = $destAccountCurrency->id;
$source->save();
$destination->save();
Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $source->id, $destination->id));
}
// if foreign amount of one is null and the other is not, use this to restore:
if (null === $transaction->foreign_amount && null !== $opposing->foreign_amount) {
$transaction->foreign_amount = bcmul((string)$opposing->foreign_amount, '-1');
$transaction->save();
Log::debug(sprintf('Restored foreign amount of transaction (1) #%d to %s', $transaction->id, $transaction->foreign_amount));
if (null === $source->foreign_amount && null !== $destination->foreign_amount) {
$source->foreign_amount = bcmul((string)$destination->foreign_amount, '-1');
$source->save();
Log::debug(sprintf('Restored foreign amount of source transaction (1) #%d to %s', $source->id, $source->foreign_amount));
}
// if foreign amount of one is null and the other is not, use this to restore (other way around)
if (null === $opposing->foreign_amount && null !== $transaction->foreign_amount) {
$opposing->foreign_amount = bcmul((string)$transaction->foreign_amount, '-1');
$opposing->save();
Log::debug(sprintf('Restored foreign amount of transaction (2) #%d to %s', $opposing->id, $opposing->foreign_amount));
if (null === $destination->foreign_amount && null !== $destination->foreign_amount) {
$destination->foreign_amount = bcmul((string)$destination->foreign_amount, '-1');
$destination->save();
Log::debug(sprintf('Restored foreign amount of destination transaction (2) #%d to %s', $destination->id, $destination->foreign_amount));
}
// when both are zero, try to grab it from journal:
if (null === $opposing->foreign_amount && null === $transaction->foreign_amount) {
$foreignAmount = $journalRepos->getMetaField($journal, 'foreign_amount');
if (null === $source->foreign_amount && null === $destination->foreign_amount) {
$foreignAmount = $this->journalRepos->getMetaField($journal, 'foreign_amount');
if (null === $foreignAmount) {
Log::debug(sprintf('Journal #%d has missing foreign currency data, forced to do 1:1 conversion :(.', $transaction->transaction_journal_id));
$transaction->foreign_amount = bcmul((string)$transaction->amount, '-1');
$opposing->foreign_amount = bcmul((string)$opposing->amount, '-1');
$transaction->save();
$opposing->save();
Log::debug(sprintf('Journal #%d has missing foreign currency data, forced to do 1:1 conversion :(.', $source->transaction_journal_id));
$source->foreign_amount = $source->amount;
$destination->foreign_amount = $destination->amount;
$source->save();
$destination->save();
return;
}
@@ -359,15 +390,61 @@ class JournalCurrencies extends Command
Log::debug(
sprintf(
'Journal #%d has missing foreign currency info, try to restore from meta-data ("%s").',
$transaction->transaction_journal_id,
$source->transaction_journal_id,
$foreignAmount
)
);
$transaction->foreign_amount = bcmul($foreignPositive, '-1');
$opposing->foreign_amount = $foreignPositive;
$transaction->save();
$opposing->save();
$source->foreign_amount = bcmul($foreignPositive, '-1');
$destination->foreign_amount = $foreignPositive;
$source->save();
$destination->save();
}
}
/**
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
* like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
*
* A transfer always has the
*
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
* transactions.
*/
private function updateTransferCurrencies(): void
{
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)
->with(['user', 'transactionType', 'transactionCurrency', 'transactions', 'transactions.account'])
->get(['transaction_journals.*']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateTransferCurrency($journal);
}
}
/**
* @param TransactionJournal $transfer
*/
private function updateTransferCurrency(TransactionJournal $transfer): void
{
$sourceTransaction = $this->getSourceTransaction($transfer);
$destTransaction = $this->getDestinationTransaction($transfer);
if (null === $sourceTransaction) {
$this->info(sprintf('Source transaction for journal #%d is null.', $transfer->id));
return;
}
if (null === $destTransaction) {
$this->info(sprintf('Destination transaction for journal #%d is null.', $transfer->id));
return;
}
$this->updateTransactionCurrency($transfer, $sourceTransaction, $destTransaction);
$this->updateJournalCurrency($transfer, $sourceTransaction);
}
}

View File

@@ -55,6 +55,7 @@ class MigrateAttachments extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -84,7 +85,8 @@ class MigrateAttachments extends Command
Log::debug(sprintf('Migrated attachment #%s description to note #%d', $att->id, $note->id));
}
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Migrated attachment notes in %s seconds.', $end));
$this->markAsExecuted();
return 0;

View File

@@ -56,6 +56,7 @@ class MigrateNotes extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -82,7 +83,8 @@ class MigrateNotes extends Command
Log::error(sprintf('Could not delete old meta entry #%d: %s', $meta->id, $e->getMessage()));
}
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Migrated notes in %s seconds.', $end));
$this->markAsExecuted();
return 0;

View File

@@ -21,14 +21,15 @@
namespace FireflyIII\Console\Commands\Upgrade;
use DB;
use Exception;
use FireflyIII\Factory\TransactionJournalFactory;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
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;
/**
@@ -84,6 +85,7 @@ class MigrateToGroups extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isMigrated() && true !== $this->option('force')) {
$this->info('Database already seems to be migrated.');
@@ -95,9 +97,14 @@ class MigrateToGroups extends Command
Log::debug('---- start group migration ----');
$this->makeGroupsFromSplitJournals();
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Migrate split journals to groups in %s seconds.', $end));
$start = microtime(true);
$this->makeGroupsFromAll();
Log::debug('---- end group migration ----');
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Migrate all journals to groups in %s seconds.', $end));
$this->markAsMigrated();
return 0;
@@ -105,15 +112,42 @@ class MigrateToGroups extends Command
/**
* @param TransactionJournal $journal
* @param Transaction $transaction
*
* @return Transaction|null
*/
private function giveGroup(TransactionJournal $journal): void
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$group = new TransactionGroup;
$group->title = null;
$group->user_id = $journal->user_id;
$group->save();
$journal->transaction_group_id = $group->id;
$journal->save();
$set = $journal->transactions->filter(
function (Transaction $subject) use ($transaction) {
return $transaction->amount * -1 === (float)$subject->amount && $transaction->identifier === $subject->identifier;
}
);
return $set->first();
}
/**
* @param TransactionJournal $journal
*
* @return Collection
*/
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
return $journal->transactions->filter(
function (Transaction $transaction) {
return $transaction->amount > 0;
}
);
}
/**
* @param array $array
*/
private function giveGroup(array $array): void
{
$groupId = DB::table('transaction_groups')->insertGetId(['title' => null, 'user_id' => $array['user_id']]);
DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
}
/**
@@ -135,26 +169,26 @@ class MigrateToGroups extends Command
private function makeGroupsFromAll(): void
{
$orphanedJournals = $this->journalRepository->getJournalsWithoutGroup();
if ($orphanedJournals->count() > 0) {
Log::debug(sprintf('Going to convert %d transactions. Please hold..', $orphanedJournals->count()));
/** @var TransactionJournal $journal */
foreach ($orphanedJournals as $journal) {
$this->giveGroup($journal);
$count = count($orphanedJournals);
if ($count > 0) {
Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $count));
$this->line(sprintf('Going to convert %d transaction journals. Please hold..', $count));
/** @var array $journal */
foreach ($orphanedJournals as $array) {
$this->giveGroup($array);
}
}
if (0 === $orphanedJournals->count()) {
$this->info('No need to convert transactions.');
if (0 === $count) {
$this->info('No need to convert transaction journals.');
}
}
/**
*
* @throws Exception
*/
private function makeGroupsFromSplitJournals(): void
{
$splitJournals = $this->journalRepository->getSplitJournals();
if ($splitJournals->count() > 0) {
$this->info(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
/** @var TransactionJournal $journal */
@@ -163,7 +197,7 @@ class MigrateToGroups extends Command
}
}
if (0 === $splitJournals->count()) {
$this->info('Found no split transactions. Nothing to do.');
$this->info('Found no split transaction journals. Nothing to do.');
}
}
@@ -185,7 +219,7 @@ class MigrateToGroups extends Command
$this->journalRepository->setUser($journal->user);
$this->journalFactory->setUser($journal->user);
$data = [
$data = [
// mandatory fields.
'type' => strtolower($journal->transactionType->type),
'date' => $journal->date,
@@ -193,16 +227,40 @@ class MigrateToGroups extends Command
'group_title' => $journal->description,
'transactions' => [],
];
$destTransactions = $this->getDestinationTransactions($journal);
$budgetId = $this->journalRepository->getJournalBudgetId($journal);
$categoryId = $this->journalRepository->getJournalCategoryId($journal);
$notes = $this->journalRepository->getNoteText($journal);
$tags = $this->journalRepository->getTags($journal);
$internalRef = $this->journalRepository->getMetaField($journal, 'internal-reference');
$sepaCC = $this->journalRepository->getMetaField($journal, 'sepa-cc');
$sepaCtOp = $this->journalRepository->getMetaField($journal, 'sepa-ct-op');
$sepaCtId = $this->journalRepository->getMetaField($journal, 'sepa-ct-id');
$sepaDb = $this->journalRepository->getMetaField($journal, 'sepa-db');
$sepaCountry = $this->journalRepository->getMetaField($journal, 'sepa-country');
$sepaEp = $this->journalRepository->getMetaField($journal, 'sepa-ep');
$sepaCi = $this->journalRepository->getMetaField($journal, 'sepa-ci');
$sepaBatchId = $this->journalRepository->getMetaField($journal, 'sepa-batch-id');
$externalId = $this->journalRepository->getMetaField($journal, 'external-id');
$originalSource = $this->journalRepository->getMetaField($journal, 'original-source');
$recurrenceId = $this->journalRepository->getMetaField($journal, 'recurrence_id');
$bunq = $this->journalRepository->getMetaField($journal, 'bunq_payment_id');
$hash = $this->journalRepository->getMetaField($journal, 'importHash');
$hashTwo = $this->journalRepository->getMetaField($journal, 'importHashV2');
$interestDate = $this->journalRepository->getMetaDate($journal, 'interest_date');
$bookDate = $this->journalRepository->getMetaDate($journal, 'book_date');
$processDate = $this->journalRepository->getMetaDate($journal, 'process_date');
$dueDate = $this->journalRepository->getMetaDate($journal, 'due_date');
$paymentDate = $this->journalRepository->getMetaDate($journal, 'payment_date');
$invoiceDate = $this->journalRepository->getMetaDate($journal, 'invoice_date');
$transactions = $journal->transactions()->where('amount', '>', 0)->get();
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $transactions->count()));
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
foreach ($destTransactions as $transaction) {
Log::debug(sprintf('Now going to add transaction #%d to the array.', $transaction->id));
$budgetId = $this->journalRepository->getJournalBudgetId($journal);
$categoryId = $this->journalRepository->getJournalCategoryId($journal);
$opposingTr = $this->journalRepository->findOpposingTransaction($transaction);
$opposingTr = $this->findOpposingTransaction($journal, $transaction);
if (null === $opposingTr) {
$this->error(
@@ -225,29 +283,29 @@ class MigrateToGroups extends Command
'budget_id' => $budgetId,
'category_id' => $categoryId,
'bill_id' => $journal->bill_id,
'notes' => $this->journalRepository->getNoteText($journal),
'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'),
'sepa-ct-id' => $this->journalRepository->getMetaField($journal, 'sepa-ct-id'),
'sepa-db' => $this->journalRepository->getMetaField($journal, 'sepa-db'),
'sepa-country' => $this->journalRepository->getMetaField($journal, 'sepa-country'),
'sepa-ep' => $this->journalRepository->getMetaField($journal, 'sepa-ep'),
'sepa-ci' => $this->journalRepository->getMetaField($journal, 'sepa-ci'),
'sepa-batch-id' => $this->journalRepository->getMetaField($journal, 'sepa-batch-id'),
'external_id' => $this->journalRepository->getMetaField($journal, 'external-id'),
'original-source' => $this->journalRepository->getMetaField($journal, 'original-source'),
'recurrence_id' => $this->journalRepository->getMetaField($journal, 'recurrence_id'),
'bunq_payment_id' => $this->journalRepository->getMetaField($journal, 'bunq_payment_id'),
'importHash' => $this->journalRepository->getMetaField($journal, 'importHash'),
'importHashV2' => $this->journalRepository->getMetaField($journal, 'importHashV2'),
'interest_date' => $this->journalRepository->getMetaDate($journal, 'interest_date'),
'book_date' => $this->journalRepository->getMetaDate($journal, 'book_date'),
'process_date' => $this->journalRepository->getMetaDate($journal, 'process_date'),
'due_date' => $this->journalRepository->getMetaDate($journal, 'due_date'),
'payment_date' => $this->journalRepository->getMetaDate($journal, 'payment_date'),
'invoice_date' => $this->journalRepository->getMetaDate($journal, 'invoice_date'),
'notes' => $notes,
'tags' => $tags,
'internal_reference' => $internalRef,
'sepa-cc' => $sepaCC,
'sepa-ct-op' => $sepaCtOp,
'sepa-ct-id' => $sepaCtId,
'sepa-db' => $sepaDb,
'sepa-country' => $sepaCountry,
'sepa-ep' => $sepaEp,
'sepa-ci' => $sepaCi,
'sepa-batch-id' => $sepaBatchId,
'external_id' => $externalId,
'original-source' => $originalSource,
'recurrence_id' => $recurrenceId,
'bunq_payment_id' => $bunq,
'importHash' => $hash,
'importHashV2' => $hashTwo,
'interest_date' => $interestDate,
'book_date' => $bookDate,
'process_date' => $processDate,
'due_date' => $dueDate,
'payment_date' => $paymentDate,
'invoice_date' => $invoiceDate,
];
$data['transactions'][] = $tArray;
@@ -260,8 +318,8 @@ class MigrateToGroups extends Command
$this->service->destroy($journal);
// report on result:
Log::debug(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray())));
$this->line(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray())));
Log::debug(sprintf('Migrated journal #%d into these journals: #%s', $journal->id, implode(', #', $result->pluck('id')->toArray())));
$this->line(sprintf('Migrated journal #%d into these journals: #%s', $journal->id, implode(', #', $result->pluck('id')->toArray())));
}
/**

View File

@@ -64,6 +64,8 @@ class MigrateToRules extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -111,11 +113,13 @@ class MigrateToRules extends Command
// loop bills.
$order = 1;
$count = 0;
/** @var Collection $collection */
$collection = $user->bills()->get();
/** @var Bill $bill */
foreach ($collection as $bill) {
if ('MIGRATED_TO_RULES' !== $bill->match) {
$count++;
$rule = Rule::create(
[
'user_id' => $user->id,
@@ -211,8 +215,15 @@ class MigrateToRules extends Command
$bill->save();
}
}
if ($count > 0) {
$this->info(sprintf('Migrated %d bills for user %s', $count, $user->email));
}
if (0 === $count) {
$this->info(sprintf('Bills are correct for user %s.', $user->email));
}
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified and fixed bills in %s seconds.', $end));
$this->markAsExecuted();
return 0;

View File

@@ -61,6 +61,7 @@ class TransactionIdentifier extends Command
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
@@ -86,7 +87,8 @@ class TransactionIdentifier extends Command
foreach ($journalIds as $journalId) {
$this->updateJournalidentifiers((int)$journalId);
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verified and fixed transaction identifiers in %s seconds.', $end));
$this->markAsExecuted();
return 0;

View File

@@ -21,6 +21,8 @@
namespace FireflyIII\Console\Commands\Upgrade;
set_time_limit(0);
use Artisan;
use Illuminate\Console\Command;