Refactor upgrade and verify commands.

This commit is contained in:
James Cole 2019-03-23 08:10:59 +01:00
parent a89be86ca4
commit 1b0be2a47e
32 changed files with 1883 additions and 873 deletions

View File

@ -0,0 +1,81 @@
<?php
/**
* CorrectDatabase.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use Artisan;
use Illuminate\Console\Command;
use Schema;
/**
* Class CorrectDatabase
*/
class CorrectDatabase extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Will correct the integrity of your database, of necessary.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:correct-database';
/**
* Execute the console command.
*/
public function handle(): int
{
// if table does not exist, return false
if (!Schema::hasTable('users')) {
return 1;
}
$commands = [
'firefly-iii:fix-piggies',
'firefly-iii:create-link-types',
'firefly-iii:create-access-tokens',
'firefly-iii:remove-bills',
'firefly-iii:enable-currencies',
'firefly-iii:fix-transfer-budgets',
'firefly-iii:fix-uneven-amount',
'firefly-iii:delete-zero-amount',
'firefly-iii:delete-orphaned-transactions',
'firefly-iii:delete-empty-journals',
'firefly-iii:delete-empty-groups',
'firefly-iii:fix-account-types',
];
foreach ($commands as $command) {
$this->line(sprintf('Now executing %s', $command));
Artisan::call($command);
$result = Artisan::output();
echo $result;
}
return 0;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* VerifySkeleton.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use Illuminate\Console\Command;
/**
* Class CorrectionSkeleton
*/
class CorrectionSkeleton extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'DESCRIPTION HERE';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:CORR_COMMAND';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
//
$this->warn('Congrats, you found the skeleton command. Boo!');
return 0;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* CreateAccessTokens.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use Exception;
use FireflyIII\User;
use Illuminate\Console\Command;
/**
* Class CreateAccessTokens
*/
class CreateAccessTokens extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Creates user access tokens.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:create-access-tokens';
/**
* Execute the console command.
*
* @return int
* @throws Exception
*/
public function handle(): int
{
$count = 0;
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$pref = app('preferences')->getForUser($user, 'access_token', null);
if (null === $pref) {
$token = $user->generateAccessToken();
app('preferences')->setForUser($user, 'access_token', $token);
$this->line(sprintf('Generated access token for user %s', $user->email));
++$count;
}
}
if (0 === $count) {
$this->info('All access tokens OK!');
}
return 0;
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* CreateLinkTypes.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\LinkType;
use Illuminate\Console\Command;
/**
* Class CreateLinkTypes. Created all link types in case a migration hasn't fired.
*/
class CreateLinkTypes extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Creates all link types.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:create-link-types';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
{
//
$count = 0;
$set = [
'Related' => ['relates to', 'relates to'],
'Refund' => ['(partially) refunds', 'is (partially) refunded by'],
'Paid' => ['(partially) pays for', 'is (partially) paid for by'],
'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'],
];
foreach ($set as $name => $values) {
$link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first();
if (null === $link) {
$link = new LinkType;
$link->name = $name;
$link->outward = $values[0];
$link->inward = $values[1];
++$count;
$this->line(sprintf('Created missing link type "%s"', $name));
}
$link->editable = false;
$link->save();
}
if (0 === $count) {
$this->info('All link types OK!');
}
return 0;
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* DeleteEmptyGroups.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use Exception;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
/**
* Class DeleteEmptyGroups
*/
class DeleteEmptyGroups extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete empty transaction groups.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:delete-empty-groups';
/**
* Execute the console command.
*
* @throws Exception;
* @return mixed
*/
public function handle(): int
{
//
$groups = array_unique(TransactionJournal::get(['transaction_group_id'])->pluck('transaction_group_id')->toArray());
$count = TransactionGroup::whereNull('deleted_at')->whereNotIn('id', $groups)->count();
if (0 === $count) {
$this->info('No empty groups.');
}
if ($count > 0) {
$this->info(sprintf('Deleted %d empty groups.', $count));
TransactionGroup::whereNull('deleted_at')->whereNotIn('id', $groups)->delete();
}
return 0;
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* DeleteEmptyJournals.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use DB;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
/**
* Class DeleteEmptyJournals
*/
class DeleteEmptyJournals extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete empty and uneven transaction journals.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:delete-empty-journals';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->deleteUnevenJournals();
$this->deleteEmptyJournals();
return 0;
}
private function deleteEmptyJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id']);
foreach ($set as $entry) {
TransactionJournal::find($entry->id)->delete();
$this->info(sprintf('Deleted empty transaction #%d', $entry->id));
++$count;
}
if (0 === $count) {
$this->info('No empty transactions.');
}
}
/**
* Delete transactions and their journals if they have an uneven number of transactions.
*/
private function deleteUnevenJournals(): void
{
/**
* select count(transactions.transaction_journal_id) as the_count, transactions.transaction_journal_id from transactions
*
* where transactions.deleted_at is null
*
* group by transactions.transaction_journal_id
* having the_count in ()
*/
$set = Transaction
::whereNull('deleted_at')
->having('the_count', '!=', '2')
->groupBy('transactions.transaction_journal_id')
->get([DB::raw('COUNT(transactions.transaction_journal_id) as the_count'), 'transaction_journal_id']);
$total = 0;
foreach ($set as $row) {
$count = (int)$row->the_count;
if (1 === $count % 2) {
// uneven number, delete journal and transactions:
TransactionJournal::find((int)$row->transaction_journal_id)->delete();
Transaction::where('transaction_journal_id', (int)$row->transaction_journal_id)->delete();
$this->info(sprintf('Deleted transaction #%d because it had an uneven number of transactions.', $row->transaction_journal_id));
$total++;
}
}
if (0 === $total) {
$this->info('No uneven transactions.');
}
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* DeleteOrphanedTransactions.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use Exception;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
use stdClass;
/**
* Deletes transactions where the journal has been deleted.
*/
class DeleteOrphanedTransactions extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Deletes orphaned transactions.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:delete-orphaned-transactions';
/**
* Execute the console command.
*
* @return int
* @throws Exception
*/
public function handle(): int
{
$this->deleteOrphanedTransactions();
$this->deleteFromOrphanedAccounts();
return 0;
}
/**
*
*/
private function deleteFromOrphanedAccounts(): void
{
$set
= Transaction
::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->whereNotNull('accounts.deleted_at')
->get(['transactions.*']);
$count = 0;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
// delete journals
$journal = TransactionJournal::find((int)$transaction->transaction_journal_id);
if ($journal) {
$journal->delete();
}
Transaction::where('transaction_journal_id', (int)$transaction->transaction_journal_id)->delete();
$this->line(
sprintf('Deleted transaction #%d because account #%d was already deleted.', $transaction->transaction_journal_id, $transaction->account_id)
);
$count++;
}
if(0===$count) {
$this->info('No orphaned accounts.');
}
}
/**
* @throws Exception
*/
private function deleteOrphanedTransactions(): void
{
$count = 0;
$set = Transaction
::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereNotNull('transaction_journals.deleted_at')
->whereNull('transactions.deleted_at')
->whereNotNull('transactions.id')
->get(
[
'transaction_journals.id as journal_id',
'transactions.id as transaction_id',
]
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$transaction = Transaction::find((int)$entry->transaction_id);
$transaction->delete();
$this->info(
sprintf(
'Transaction #%d (part of deleted journal #%d) has been deleted as well.',
$entry->transaction_id,
$entry->journal_id
)
);
++$count;
}
if (0 === $count) {
$this->info('No orphaned transactions.');
}
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* DeleteZeroAmount.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\Transaction;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Exception;
/**
* Class DeleteZeroAmount
*/
class DeleteZeroAmount extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete transactions with zero amount.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:delete-zero-amount';
/**
* Execute the console command.
* @throws Exception
* @return int
*/
public function handle(): int
{
$set = Transaction::where('amount', 0)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
$set = array_unique($set);
/** @var Collection $journals */
$journals = TransactionJournal::whereIn('id', $set)->get();
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->info(sprintf('Deleted transaction #%d because the amount is zero (0.00).', $journal->id));
$journal->delete();
Transaction::where('transaction_journal_id', $journal->id)->delete();
}
if (0 === $journals->count()) {
$this->info('No zero-amount transactions.');
}
return 0;
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* EnableCurrencies.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
/**
* Class EnableCurrencies
*/
class EnableCurrencies extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Enables all currencies in use.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:enable-currencies';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
{
$found = [];
// get all meta entries
/** @var Collection $meta */
$meta = AccountMeta::where('name', 'currency_id')->groupBy('data')->get(['data']);
foreach ($meta as $entry) {
$found[] = (int)$entry->data;
}
// get all from journals:
/** @var Collection $journals */
$journals = TransactionJournal::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($journals as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
// get all from transactions
/** @var Collection $transactions */
$transactions = Transaction::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($transactions as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
// get all from budget limits
/** @var Collection $limits */
$limits = BudgetLimit::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($limits as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
$found = array_unique($found);
$this->info(sprintf('%d different currencies are currently in use.', count($found)));
$disabled = TransactionCurrency::whereIn('id', $found)->where('enabled', false)->count();
if ($disabled > 0) {
$this->info(sprintf('%d were still disabled. This has been corrected.', $disabled));
}
TransactionCurrency::whereIn('id', $found)->update(['enabled' => true]);
return 0;
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* FixAccountTypes.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
/**
* Class FixAccountTypes
*/
class FixAccountTypes extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Make sure all journals have the correct from/to account types.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-account-types';
/** @var array */
private $expected;
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->expected = config('firefly.source_dests');
$journals = TransactionJournal::get();
foreach ($journals as $journal) {
$this->inspectJournal($journal);
}
return 0;
}
private function getDestinationAccount(TransactionJournal $journal): Account
{
return $journal->transactions()->where('amount', '>', 0)->first()->account;
}
/**
* @param TransactionJournal $journal
*
* @return Account
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
return $journal->transactions()->where('amount', '<', 0)->first()->account;
}
/**
* @param TransactionJournal $journal
*/
private function inspectJournal(TransactionJournal $journal): void
{
$count = $journal->transactions()->count();
if (2 !== $count) {
$this->info(sprintf('Cannot inspect journal #%d because it does not have 2 transactions, but %d', $journal->id, $count));
return;
}
$type = $journal->transactionType->type;
$sourceAccount = $this->getSourceAccount($journal);
$sourceAccountType = $sourceAccount->accountType->type;
$destinationAccount = $this->getDestinationAccount($journal);
$destinationAccountType = $destinationAccount->accountType->type;
if (!isset($this->expected[$type])) {
$this->info(sprintf('No source/destination info for transaction type %s.', $type));
return;
}
if (!isset($this->expected[$type][$sourceAccountType])) {
$this->info(sprintf('The source of %s #%d cannot be of type "%s".', $type, $journal->id, $sourceAccountType));
$this->info(sprintf('The destination of %s #%d probably cannot be of type "%s".', $type, $journal->id, $destinationAccountType));
// TODO think of a way to fix the problem.
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!\in_array($destinationAccountType, $expectedTypes, true)) {
$this->info(sprintf('The destination of %s #%d cannot be of type "%s".', $type, $journal->id, $destinationAccountType));
// TODO think of a way to fix the problem.
}
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* FixPiggies.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Illuminate\Console\Command;
/**
* Report (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events.
*
* Class FixPiggies
*/
class FixPiggies extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes common issues with piggy banks.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-piggies';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
{
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
$set->each(
function (PiggyBankEvent $event) {
if (null === $event->transaction_journal_id) {
return true;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
if (null === $journal) {
return true;
}
$type = $journal->transactionType->type;
if (TransactionType::TRANSFER !== $type) {
$event->transaction_journal_id = null;
$event->save();
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
}
return true;
}
);
$this->line(sprintf('Verified the content of %d piggy bank events.', $set->count()));
return 0;
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* FixUnevenAmount.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use DB;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
use stdClass;
/**
* Class FixUnevenAmount
*/
class FixUnevenAmount extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fix journals with uneven amounts.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-uneven-amount';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$count = 0;
// get invalid journals
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->whereNull('deleted_at')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')]);
/** @var stdClass $entry */
foreach ($journals as $entry) {
if (0 !== bccomp((string)$entry->the_sum, '0')) {
$this->fixJournal((int)$entry->transaction_journal_id);
$count++;
}
}
if (0 === $count) {
$this->info('Amount integrity OK!');
}
return 0;
}
/**
* @param int $param
*/
private function fixJournal(int $param): void
{
// one of the transactions is bad.
$journal = TransactionJournal::find($param);
if (!$journal) {
return;
}
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
$amount = bcmul('-1', (string)$source->amount);
// fix amount of destination:
/** @var Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
$destination->amount = $amount;
$destination->save();
$this->line(sprintf('Corrected amount in transaction #%d', $param));
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* RemoveBills.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Illuminate\Console\Command;
/**
* Class RemoveBills
*/
class RemoveBills extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove bills from transactions that shouldn\'t have one.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:remove-bills';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
{
/** @var TransactionType $withdrawal */
$withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first();
$journals = TransactionJournal::whereNotNull('bill_id')->where('transaction_type_id', '!=', $withdrawal->id)->get();
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->line(sprintf('Transaction journal #%d should not be linked to bill #%d.', $journal->id, $journal->bill_id));
$journal->bill_id = null;
$journal->save();
}
if (0 === $journals->count()) {
$this->info('All transactions have correct bill information.');
}
if ($journals->count() > 0) {
$this->info('Fixed all transactions so they have correct bill information.');
}
return 0;
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* TransferBudgets.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Illuminate\Console\Command;
/**
* Class TransferBudgets
*/
class TransferBudgets extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes budgets from transfers.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-transfer-budgets';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$set = TransactionJournal::distinct()
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id')
->whereNotIn('transaction_types.type', [TransactionType::WITHDRAWAL])
->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']);
$count = 0;
/** @var TransactionJournal $entry */
foreach ($set as $entry) {
$this->info(sprintf('Transaction #%d is a %s, so has no longer a budget.', $entry->id, $entry->transactionType->type));
$entry->budgets()->sync([]);
$count++;
}
if (0 === $count) {
$this->info('No invalid budget/journal entries.');
}
return 0;
}
}

View File

@ -0,0 +1,191 @@
<?php
/**
* ReportEmptyObjects.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Integrity;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Tag;
use FireflyIII\Models\Account;
use Illuminate\Console\Command;
use stdClass;
/**
* Class ReportEmptyObjects
*/
class ReportEmptyObjects extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reports on empty database objects.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:report-empty-objects';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->reportEmptyBudgets();
$this->reportEmptyCategories();
$this->reportEmptyTags();
$this->reportAccounts();
$this->reportBudgetLimits();
return 0;
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
$entry->user_id,
$entry->email,
$entry->id,
$entry->name
);
$this->line($line);
}
}
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(
['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$name = $entry->name;
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name);
$this->line($line);
}
}
/**
* Report on budgets with no transactions or journals.
*/
private function reportEmptyBudgets(): void
{
$set = Budget::leftJoin('budget_transaction_journal', 'budgets.id', '=', 'budget_transaction_journal.budget_id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->distinct()
->whereNull('budget_transaction_journal.budget_id')
->whereNull('budgets.deleted_at')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$entry->id,
$objName
);
$this->line($line);
}
}
/**
* Report on categories with no transactions or journals.
*/
private function reportEmptyCategories(): void
{
$set = Category::leftJoin('category_transaction_journal', 'categories.id', '=', 'category_transaction_journal.category_id')
->leftJoin('users', 'categories.user_id', '=', 'users.id')
->distinct()
->whereNull('category_transaction_journal.category_id')
->whereNull('categories.deleted_at')
->get(['categories.id', 'categories.name', 'categories.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
$line = sprintf(
'User #%d (%s) has category #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$entry->id,
$objName
);
$this->line($line);
}
}
/**
*
*/
private function reportEmptyTags(): void
{
$set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id')
->leftJoin('users', 'tags.user_id', '=', 'users.id')
->distinct()
->whereNull('tag_transaction_journal.tag_id')
->whereNull('tags.deleted_at')
->get(['tags.id', 'tags.tag', 'tags.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->tag;
$line = sprintf(
'User #%d (%s) has tag #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$entry->id,
$objName
);
$this->line($line);
}
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* ReportIntegrity.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Integrity;
use Illuminate\Console\Command;
use Schema;
use Artisan;
/**
* Class ReportIntegrity
*/
class ReportIntegrity extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Will report on the integrity of your database.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:report-integrity';
/**
* Execute the console command.
*/
public function handle(): int
{
// if table does not exist, return false
if (!Schema::hasTable('users')) {
return 1;
}
$commands = [
'firefly-iii:report-empty-objects',
'firefly-iii:report-sum',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
// 'firefly-iii:',
];
foreach ($commands as $command) {
$this->line(sprintf('Now executing %s', $command));
Artisan::call($command);
$result = Artisan::output();
echo $result;
}
// $this->reportEmptyBudgets();
// $this->reportEmptyCategories();
// $this->reportObject('tag');
// $this->reportAccounts();
// $this->reportBudgetLimits();
// $this->reportSum();
// $this->reportJournals();
// $this->reportTransactions();
// $this->reportDeletedAccounts();
// $this->reportNoTransactions();
// $this->reportTransfersBudgets();
// $this->reportIncorrectJournals();
// $this->repairPiggyBanks();
// $this->createLinkTypes();
// $this->createAccessTokens();
// $this->fixDoubleAmounts(); // is a report function!
// $this->fixBadMeta();
// $this->removeBills();
// $this->enableCurrencies();
// $this->reportZeroAmount();
return 0;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* VerifySkeleton.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Integrity;
use Illuminate\Console\Command;
/**
* Class ReportSkeleton
*/
class ReportSkeleton extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'DESCRIPTION HERE';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:INT_COMMAND';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
//
$this->warn('Congrats, you found the skeleton command. Boo!');
return 0;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* ReportSum.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Integrity;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Console\Command;
/**
* Class ReportSkeleton
*/
class ReportSum extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Report on the total sum of transactions. Must be 0.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:report-sum';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->reportSum();
return 0;
}
/**
* Reports for each user when the sum of their transactions is not zero.
*/
private function reportSum(): void
{
/** @var UserRepositoryInterface $userRepository */
$userRepository = app(UserRepositoryInterface::class);
/** @var User $user */
foreach ($userRepository->all() as $user) {
$sum = (string)$user->transactions()->sum('amount');
if (0 !== bccomp($sum, '0')) {
$this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!');
}
if (0 === bccomp($sum, '0')) {
$this->info(sprintf('Amount integrity OK for user #%d', $user->id));
}
}
}
}

View File

@ -206,6 +206,7 @@ class JournalCurrencies extends Command
$accountRepos->setUser($transaction->account->user);
$currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id'));
$journal = $transaction->transactionJournal;
$currencyCode = $journal->transactionCurrency->code ?? '(nothing)';
if (null === $currency) {
return;
@ -218,7 +219,7 @@ class JournalCurrencies extends Command
$journal->id,
$journal->description,
$currency->code,
$journal->transactionCurrency->code
$currencyCode
)
);
$journal->transaction_currency_id = $currency->id;

View File

@ -24,6 +24,7 @@ namespace FireflyIII\Console\Commands\Upgrade;
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;
@ -93,7 +94,8 @@ class MigrateToGroups extends Command
}
Log::debug('---- start group migration ----');
$this->makeGroups();
$this->makeGroupsFromSplitJournals();
$this->makeGroupsFromAll();
Log::debug('---- end group migration ----');
$this->markAsMigrated();
@ -101,6 +103,19 @@ class MigrateToGroups extends Command
return 0;
}
/**
* @param TransactionJournal $journal
*/
private function giveGroup(TransactionJournal $journal): void
{
$group = new TransactionGroup;
$group->title = null;
$group->user_id = $journal->user_id;
$group->save();
$journal->transaction_group_id = $group->id;
$journal->save();
}
/**
* @return bool
*/
@ -114,12 +129,50 @@ class MigrateToGroups extends Command
return false; // @codeCoverageIgnore
}
/**
* Gives all journals without a group a group.
*/
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);
}
}
if (0 === $orphanedJournals->count()) {
$this->info('No need to convert transactions.');
}
}
/**
*
* @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 */
foreach ($splitJournals as $journal) {
$this->makeMultiGroup($journal);
}
}
if (0 === $splitJournals->count()) {
$this->info('Found no split transactions. Nothing to do.');
}
}
/**
* @param TransactionJournal $journal
*
* @throws Exception
*/
private function makeGroup(TransactionJournal $journal): void
private function makeMultiGroup(TransactionJournal $journal): void
{
// double check transaction count.
if ($journal->transactions->count() <= 2) {
@ -204,33 +257,13 @@ class MigrateToGroups extends Command
Log::debug('Done calling transaction journal factory');
// delete the old transaction journal.
//$this->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())));
$this->line(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray())));
}
/**
*
* @throws Exception
*/
private function makeGroups(): void
{
$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 */
foreach ($splitJournals as $journal) {
$this->makeGroup($journal);
}
}
if (0 === $splitJournals->count()) {
$this->info('Found no split journals. Nothing to do.');
}
}
/**
*
*/

View File

@ -34,7 +34,7 @@ class UpgradeDatabase extends Command
*
* @var string
*/
protected $description = 'Executes all upgrade commands.';
protected $description = 'Upgrades the database to the latest version.';
/**
* The name and signature of the console command.
*

View File

@ -40,7 +40,7 @@ class UpgradeSkeleton extends Command
*
* @var string
*/
protected $signature = 'firefly-iii:SKELETON {--F|force : Force the execution of this command.}';
protected $signature = 'firefly-iii:UPGRSKELETON {--F|force : Force the execution of this command.}';
/**
* Execute the console command.

View File

@ -1,714 +0,0 @@
<?php
/**
* VerifyDatabase.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** @noinspection PhpDynamicAsStaticMethodCallInspection */
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use DB;
use Exception;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Category;
use FireflyIII\Models\LinkType;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Log;
use Schema;
use stdClass;
/**
* Class VerifyDatabase.
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @codeCoverageIgnore
*/
class VerifyDatabase extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Will verify your database.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly:verify';
/**
* Execute the console command.
*/
public function handle(): int
{
// if table does not exist, return false
if (!Schema::hasTable('users')) {
return 1;
}
$this->reportEmptyBudgets();
$this->reportEmptyCategories();
$this->reportObject('tag');
$this->reportAccounts();
$this->reportBudgetLimits();
$this->reportSum();
$this->reportJournals();
$this->reportTransactions();
$this->reportDeletedAccounts();
$this->reportNoTransactions();
$this->reportTransfersBudgets();
$this->reportIncorrectJournals();
$this->repairPiggyBanks();
$this->createLinkTypes();
$this->createAccessTokens();
$this->fixDoubleAmounts();
$this->fixBadMeta();
$this->removeBills();
$this->enableCurrencies();
$this->reportZeroAmount();
return 0;
}
/**
* Create user access tokens, if not present already.
*/
private function createAccessTokens(): void
{
$count = 0;
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$pref = app('preferences')->getForUser($user, 'access_token', null);
if (null === $pref) {
$token = $user->generateAccessToken();
app('preferences')->setForUser($user, 'access_token', $token);
$this->line(sprintf('Generated access token for user %s', $user->email));
++$count;
}
}
if (0 === $count) {
$this->info('All access tokens OK!');
}
}
/**
* Create default link types if necessary.
*/
private function createLinkTypes(): void
{
$count = 0;
$set = [
'Related' => ['relates to', 'relates to'],
'Refund' => ['(partially) refunds', 'is (partially) refunded by'],
'Paid' => ['(partially) pays for', 'is (partially) paid for by'],
'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'],
];
foreach ($set as $name => $values) {
$link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first();
if (null === $link) {
$link = new LinkType;
$link->name = $name;
$link->outward = $values[0];
$link->inward = $values[1];
++$count;
}
$link->editable = false;
$link->save();
}
if (0 === $count) {
$this->info('All link types OK!');
}
}
/**
* Will make sure that all currencies in use are actually enabled.
*/
private function enableCurrencies(): void
{
$found = [];
// get all meta entries
/** @var Collection $meta */
$meta = AccountMeta::where('name', 'currency_id')->groupBy('data')->get(['data']);
foreach ($meta as $entry) {
$found[] = (int)$entry->data;
}
// get all from journals:
/** @var Collection $journals */
$journals = TransactionJournal::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($journals as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
// get all from transactions
/** @var Collection $transactions */
$transactions = Transaction::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($transactions as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
// get all from budget limits
/** @var Collection $limits */
$limits = BudgetLimit::groupBy('transaction_currency_id')->get(['transaction_currency_id']);
foreach ($limits as $entry) {
$found[] = (int)$entry->transaction_currency_id;
}
$found = array_unique($found);
TransactionCurrency::whereIn('id', $found)->update(['enabled' => true]);
}
/**
* Fix the situation where the matching transactions of a journal somehow have non-matching categories or budgets.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
private function fixBadMeta(): void
{
// categories
$set = Transaction
::leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id')
->whereNull('transactions.deleted_at')
->get(['transactions.id', 'transaction_journal_id', 'identifier', 'category_transaction.category_id', 'category_transaction.id as ct_id']);
$results = [];
foreach ($set as $obj) {
$key = $obj->transaction_journal_id . '-' . $obj->identifier;
$category = (int)$obj->category_id;
// value exists and is not category:
if (isset($results[$key]) && $results[$key] !== $category) {
$this->error(
sprintf(
'Transaction #%d referred to the wrong category. Was category #%d but is fixed to be category #%d.', $obj->transaction_journal_id,
$category, $results[$key]
)
);
DB::table('category_transaction')->where('id', $obj->ct_id)->update(['category_id' => $results[$key]]);
}
// value does not exist:
if ($category > 0 && !isset($results[$key])) {
$results[$key] = $category;
}
}
// budgets
$set = Transaction
::leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id')
->whereNull('transactions.deleted_at')
->get(['transactions.id', 'transaction_journal_id', 'identifier', 'budget_transaction.budget_id', 'budget_transaction.id as ct_id']);
$results = [];
foreach ($set as $obj) {
$key = $obj->transaction_journal_id . '-' . $obj->identifier;
$budget = (int)$obj->budget_id;
// value exists and is not budget:
if (isset($results[$key]) && $results[$key] !== $budget) {
$this->error(
sprintf(
'Transaction #%d referred to the wrong budget. Was budget #%d but is fixed to be budget #%d.', $obj->transaction_journal_id, $budget,
$results[$key]
)
);
DB::table('budget_transaction')->where('id', $obj->ct_id)->update(['budget_id' => $results[$key]]);
}
// value does not exist:
if ($budget > 0 && !isset($results[$key])) {
$results[$key] = $budget;
}
}
}
/**
* Makes sure amounts are stored correctly.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
private function fixDoubleAmounts(): void
{
$count = 0;
// get invalid journals
$errored = [];
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')]);
/** @var stdClass $entry */
foreach ($journals as $entry) {
if (0 !== bccomp((string)$entry->the_sum, '0')) {
$errored[] = $entry->transaction_journal_id;
}
}
foreach ($errored as $journalId) {
// select and update:
$res = Transaction::whereNull('deleted_at')->where('transaction_journal_id', $journalId)->groupBy('amount')->get([DB::raw('MIN(id) as first_id')]);
$ids = $res->pluck('first_id')->toArray();
DB::table('transactions')->whereIn('id', $ids)->update(['amount' => DB::raw('amount * -1')]);
++$count;
// report about it
/** @var TransactionJournal $journal */
$journal = TransactionJournal::find($journalId);
if (null === $journal) {
continue;
}
if (TransactionType::OPENING_BALANCE === $journal->transactionType->type) {
$this->error(
sprintf(
'Transaction #%d was stored incorrectly. One of your asset accounts may show the wrong balance. Please visit /transactions/show/%d to verify the opening balance.',
$journalId, $journalId
)
);
}
if (TransactionType::OPENING_BALANCE !== $journal->transactionType->type) {
$this->error(
sprintf(
'Transaction #%d was stored incorrectly. Could be that the transaction shows the wrong amount. Please visit /transactions/show/%d to verify the opening balance.',
$journalId, $journalId
)
);
}
}
if (0 === $count) {
$this->info('Amount integrity OK!');
}
}
/**
* Removes bills from journals that should not have bills.
*/
private function removeBills(): void
{
/** @var TransactionType $withdrawal */
$withdrawal = TransactionType::where('type', TransactionType::WITHDRAWAL)->first();
$journals = TransactionJournal::whereNotNull('bill_id')
->where('transaction_type_id', '!=', $withdrawal->id)->get();
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->line(sprintf('Transaction journal #%d should not be linked to bill #%d.', $journal->id, $journal->bill_id));
$journal->bill_id = null;
$journal->save();
}
}
/**
* Eeport (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events.
*/
private function repairPiggyBanks(): void
{
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
$set->each(
function (PiggyBankEvent $event) {
if (null === $event->transaction_journal_id) {
return true;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
if (null === $journal) {
return true;
}
$type = $journal->transactionType->type;
if (TransactionType::TRANSFER !== $type) {
$event->transaction_journal_id = null;
$event->save();
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
}
return true;
}
);
}
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(
['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$name = $entry->name;
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name);
$this->line($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
$entry->user_id,
$entry->email,
$entry->id,
$entry->name
);
$this->line($line);
}
}
/**
* Reports on deleted accounts that still have not deleted transactions or journals attached to them.
*/
private function reportDeletedAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->whereNotNull('accounts.deleted_at')
->whereNotNull('transactions.id')
->where(
function (Builder $q) {
$q->whereNull('transactions.deleted_at');
$q->orWhereNull('transaction_journals.deleted_at');
}
)
->get(
['accounts.id as account_id', 'accounts.deleted_at as account_deleted_at', 'transactions.id as transaction_id',
'transactions.deleted_at as transaction_deleted_at', 'transaction_journals.id as journal_id',
'transaction_journals.deleted_at as journal_deleted_at',]
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$date = $entry->transaction_deleted_at ?? $entry->journal_deleted_at;
$this->error(
'Error: Account #' . $entry->account_id . ' should have been deleted, but has not.' .
' Find it in the table called "accounts" and change the "deleted_at" field to: "' . $date . '"'
);
}
}
/**
* Report on budgets with no transactions or journals.
*/
private function reportEmptyBudgets(): void
{
$set = Budget::leftJoin('budget_transaction_journal', 'budgets.id', '=', 'budget_transaction_journal.budget_id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->distinct()
->whereNull('budget_transaction_journal.budget_id')
->whereNull('budgets.deleted_at')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
// also count the transactions:
$countTransactions = DB::table('budget_transaction')->where('budget_id', $entry->id)->count();
if (0 === $countTransactions) {
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$entry->id,
$objName
);
$this->line($line);
}
}
}
/**
* Report on categories with no transactions or journals.
*/
private function reportEmptyCategories(): void
{
$set = Category::leftJoin('category_transaction_journal', 'categories.id', '=', 'category_transaction_journal.category_id')
->leftJoin('users', 'categories.user_id', '=', 'users.id')
->distinct()
->whereNull('category_transaction_journal.category_id')
->whereNull('categories.deleted_at')
->get(['categories.id', 'categories.name', 'categories.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
// also count the transactions:
$countTransactions = DB::table('category_transaction')->where('category_id', $entry->id)->count();
if (0 === $countTransactions) {
$line = sprintf(
'User #%d (%s) has category #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$entry->id,
$objName
);
$this->line($line);
}
}
}
/**
* Report on journals with bad account types linked to them.
*/
private function reportIncorrectJournals(): void
{
$configuration = [
// a withdrawal can not have revenue account:
TransactionType::WITHDRAWAL => [AccountType::REVENUE],
// deposit cannot have an expense account:
TransactionType::DEPOSIT => [AccountType::EXPENSE],
// transfer cannot have either:
TransactionType::TRANSFER => [AccountType::EXPENSE, AccountType::REVENUE],
];
foreach ($configuration as $transactionType => $accountTypes) {
$set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id')
->leftJoin('users', 'users.id', '=', 'transaction_journals.user_id')
->where('transaction_types.type', $transactionType)
->whereIn('account_types.type', $accountTypes)
->whereNull('transaction_journals.deleted_at')
->get(
['transaction_journals.id', 'transaction_journals.user_id', 'users.email', 'account_types.type as a_type',
'transaction_types.type',]
);
foreach ($set as $entry) {
$this->error(
sprintf(
'Transaction journal #%d (user #%d, %s) is of type "%s" but ' .
'is linked to a "%s". The transaction journal should be recreated.',
$entry->id,
$entry->user_id,
$entry->email,
$entry->type,
$entry->a_type
)
);
}
}
}
/**
* Any deleted transaction journals that have transactions that are NOT deleted:.
*/
private function reportJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereNotNull('transaction_journals.deleted_at')// USE THIS
->whereNull('transactions.deleted_at')
->whereNotNull('transactions.id')
->get(
[
'transaction_journals.id as journal_id',
'transaction_journals.description',
'transaction_journals.deleted_at as journal_deleted',
'transactions.id as transaction_id',
'transactions.deleted_at as transaction_deleted_at',]
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$this->error(
'Error: Transaction #' . $entry->transaction_id . ' should have been deleted, but has not.' .
' Find it in the table called "transactions" and change the "deleted_at" field to: "' . $entry->journal_deleted . '"'
);
++$count;
}
if (0 === $count) {
$this->info('No orphaned transactions!');
}
}
/**
* Report on journals without transactions.
*/
private function reportNoTransactions(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id']);
foreach ($set as $entry) {
$this->error(
'Error: Journal #' . $entry->id . ' has zero transactions. Open table "transaction_journals" and delete the entry with id #' . $entry->id
);
++$count;
}
if (0 === $count) {
$this->info('No orphaned journals!');
}
}
/**
* Report on things with no linked journals.
*
* @param string $name
*/
private function reportObject(string $name): void
{
$plural = str_plural($name);
$class = sprintf('FireflyIII\Models\%s', ucfirst($name));
$field = 'tag' === $name ? 'tag' : 'name';
/** @noinspection PhpUndefinedMethodInspection */
$set = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id')
->leftJoin('users', $plural . '.user_id', '=', 'users.id')
->distinct()
->whereNull($name . '_transaction_journal.' . $name . '_id')
->whereNull($plural . '.deleted_at')
->get([$plural . '.id', $plural . '.' . $field . ' as name', $plural . '.user_id', 'users.email']);
/** @var stdClass $entry */
foreach ($set as $entry) {
$objName = $entry->name;
$line = sprintf(
'User #%d (%s) has %s #%d ("%s") which has no transactions.',
$entry->user_id,
$entry->email,
$name,
$entry->id,
$objName
);
$this->line($line);
}
}
/**
* Reports for each user when the sum of their transactions is not zero.
*/
private function reportSum(): void
{
/** @var UserRepositoryInterface $userRepository */
$userRepository = app(UserRepositoryInterface::class);
/** @var User $user */
foreach ($userRepository->all() as $user) {
$sum = (string)$user->transactions()->sum('amount');
if (0 !== bccomp($sum, '0')) {
$this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!');
}
if (0 === bccomp($sum, '0')) {
$this->info(sprintf('Amount integrity OK for user #%d', $user->id));
}
}
}
/**
* Reports on deleted transactions that are connected to a not deleted journal.
*/
private function reportTransactions(): void
{
$set = Transaction::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereNotNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->get(
['transactions.id as transaction_id', 'transactions.deleted_at as transaction_deleted', 'transaction_journals.id as journal_id',
'transaction_journals.deleted_at',]
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$this->error(
'Error: Transaction journal #' . $entry->journal_id . ' should have been deleted, but has not.' .
' Find it in the table called "transaction_journals" and change the "deleted_at" field to: "' . $entry->transaction_deleted . '"'
);
}
}
/**
* Report on transfers that have budgets.
*/
private function reportTransfersBudgets(): void
{
$set = TransactionJournal::distinct()
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id')
->whereIn('transaction_types.type', [TransactionType::TRANSFER, TransactionType::DEPOSIT])
->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']);
/** @var TransactionJournal $entry */
foreach ($set as $entry) {
$this->error(
sprintf(
'Error: Transaction journal #%d is a %s, but has a budget. Edit it without changing anything, so the budget will be removed.',
$entry->id,
$entry->transactionType->type
)
);
}
}
/**
* Collect all journals with empty amount.
*/
private function reportZeroAmount(): void
{
$set = Transaction::where('amount', 0)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
$set = array_unique($set);
/** @var Collection $journals */
$journals = TransactionJournal::whereIn('id', $set)->get();
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$message = sprintf(
'Transaction "%s" (#%d), owned by user %s, has amount zero (0.00). It should be deleted.', $journal->description,
$journal->id, $journal->user->email
);
$this->error($message);
}
}
}

View File

@ -154,26 +154,8 @@ class TransactionFactory
Log::debug(sprintf('Now in getAccount(%s)', $direction));
Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true)));
// expected type of source account, in order of preference
$array = [
'source' => [
TransactionType::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionType::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE,
AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION],
TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
],
'destination' => [
TransactionType::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionType::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
],
];
/** @var array $array */
$array = config('firefly.expected_source_types');
$expectedTypes = $array[$direction];
unset($array);
@ -295,38 +277,7 @@ class TransactionFactory
public function makeDramaOverAccountTypes(Account $source, Account $destination): void
{
// if the source is X, then Y is allowed as destination.
$combinations = [
TransactionType::WITHDRAWAL => [
AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH],
AccountType::LOAN => [AccountType::EXPENSE],
AccountType::DEBT => [AccountType::EXPENSE],
AccountType::MORTGAGE => [AccountType::EXPENSE],
],
TransactionType::DEPOSIT => [
AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::CASH => [AccountType::ASSET],
AccountType::LOAN => [AccountType::ASSET],
AccountType::DEBT => [AccountType::ASSET],
AccountType::MORTGAGE => [AccountType::ASSET],
],
TransactionType::TRANSFER => [
AccountType::ASSET => [AccountType::ASSET],
AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
],
TransactionType::OPENING_BALANCE => [
AccountType::ASSET => [AccountType::INITIAL_BALANCE],
AccountType::LOAN => [AccountType::INITIAL_BALANCE],
AccountType::DEBT => [AccountType::INITIAL_BALANCE],
AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE],
AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
],
TransactionType::RECONCILIATION => [
AccountType::RECONCILIATION => [AccountType::ASSET],
AccountType::ASSET => [AccountType::RECONCILIATION],
],
];
$combinations = config('firefly.source_dests');
$sourceType = $source->accountType->type;
$destType = $destination->accountType->type;
$journalType = $this->journal->transactionType->type;

View File

@ -129,7 +129,9 @@ class TransactionJournalFactory
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions)));
/** Get basic fields */
$currency = $this->currencyRepository->findCurrency($transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code']);
$currency = $this->currencyRepository->findCurrency(
$transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code']
);
$foreignCurrency = $this->findForeignCurrency($transaction);
$bill = $this->billRepository->findBill($transaction['bill'], (int)$transaction['bill_id'], $transaction['bill_name']);
@ -189,9 +191,8 @@ class TransactionJournalFactory
$collection->push($journal);
}
if ($collection->count() > 1) {
$this->storeGroup($collection, $data['group_title']);
}
$this->storeGroup($collection, $data['group_title']);
return $collection;

View File

@ -26,6 +26,7 @@ use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -103,11 +104,11 @@ class TransactionGroup extends Model
/**
* @codeCoverageIgnore
* @return BelongsToMany
* @return HasMany
*/
public function transactionJournals(): BelongsToMany
public function transactionJournals(): HasMany
{
return $this->belongsToMany(TransactionJournal::class,'group_journals');
return $this->hasMany(TransactionJournal::class);
}
/**

View File

@ -39,43 +39,45 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class TransactionJournal.
*
* @property User $user
* @property int $bill_id
* @property Collection $categories
* @property bool $completed
* @property string $description
* @property int $transaction_type_id
* @property int transaction_currency_id
* @property TransactionCurrency $transactionCurrency
* @property Collection $tags
* @property int user_id
* @property Collection transactions
* @property int transaction_count
* @property Carbon interest_date
* @property Carbon book_date
* @property Carbon process_date
* @property bool encrypted
* @property int order
* @property int budget_id
* @property string period_marker
* @property Carbon $date
* @property string $transaction_type_type
* @property int $id
* @property TransactionType $transactionType
* @property Collection budgets
* @property Bill $bill
* @property Collection transactionJournalMeta
* @property User $user
* @property int $bill_id
* @property Collection $categories
* @property bool $completed
* @property string $description
* @property int $transaction_type_id
* @property int transaction_currency_id
* @property TransactionCurrency $transactionCurrency
* @property Collection $tags
* @property int user_id
* @property Collection transactions
* @property int transaction_count
* @property Carbon interest_date
* @property Carbon book_date
* @property Carbon process_date
* @property bool encrypted
* @property int order
* @property int budget_id
* @property string period_marker
* @property Carbon $date
* @property string $transaction_type_type
* @property int $id
* @property TransactionType $transactionType
* @property Collection budgets
* @property Bill $bill
* @property Collection transactionJournalMeta
* @property TransactionGroup transactionGroup
* @property int transaction_group_id
* @SuppressWarnings (PHPMD.TooManyPublicMethods)
* @SuppressWarnings (PHPMD.CouplingBetweenObjects)
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int $tag_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int $tag_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalLink[] $sourceJournalLinks
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $transactionGroups
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $transactionGroups
* @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\TransactionJournal after(\Carbon\Carbon $date)
* @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\TransactionJournal before(\Carbon\Carbon $date)
* @method static bool|null forceDelete()
@ -362,11 +364,11 @@ class TransactionJournal extends Model
/**
* @codeCoverageIgnore
* @return BelongsToMany
* @return BelongsTo
*/
public function transactionGroups(): BelongsToMany
public function transactionGroup(): BelongsToMany
{
return $this->belongsToMany(TransactionGroup::class,'group_journals');
return $this->belongsTo(TransactionGroup::class);
}
/**

View File

@ -482,6 +482,16 @@ class JournalRepository implements JournalRepositoryInterface
return $amount;
}
/**
* Return all journals without a group, used in an upgrade routine.
*
* @return Collection
*/
public function getJournalsWithoutGroup(): Collection
{
return TransactionJournal::whereNull('transaction_group_id')->get();
}
/**
* @param TransactionJournalLink $link
*

View File

@ -39,7 +39,6 @@ use Illuminate\Support\MessageBag;
*/
interface JournalRepositoryInterface
{
/**
* @param TransactionJournal $journal
* @param TransactionType $type
@ -57,9 +56,6 @@ interface JournalRepositoryInterface
*/
public function countTransactions(TransactionJournal $journal): int;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* Deletes a journal.
*
@ -69,6 +65,9 @@ interface JournalRepositoryInterface
*/
public function destroy(TransactionJournal $journal): bool;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* Find a journal by its hash.
*
@ -205,6 +204,13 @@ interface JournalRepositoryInterface
*/
public function getJournalTotal(TransactionJournal $journal): string;
/**
* Return all journals without a group, used in an upgrade routine.
*
* @return Collection
*/
public function getJournalsWithoutGroup(): Collection;
/**
* @param TransactionJournalLink $link
*

View File

@ -24,6 +24,8 @@
declare(strict_types=1);
use FireflyIII\Export\Exporter\CsvExporter;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionType as TransactionTypeModel;
use FireflyIII\Services\Currency\FixerIOv2;
use FireflyIII\Services\Currency\RatesApiIOv1;
use FireflyIII\TransactionRules\Actions\AddTag;
@ -278,7 +280,6 @@ return [
'zh_CN' => ['name_locale' => 'Chinese Simplified', 'name_english' => 'Chinese Simplified'], // 99%
//'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'], // 71%
'nb_NO' => ['name_locale' => 'Norsk', 'name_english' => 'Norwegian'],
//'ca_ES' => ['name_locale' => 'Catalan', 'name_english' => 'Catalan'], // 0%
@ -474,17 +475,73 @@ return [
],
'test-triggers' => [
'test-triggers' => [
'limit' => 10,
'range' => 200,
],
'default_currency' => 'EUR',
'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category',
'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
'default_currency' => 'EUR',
'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category',
'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
// tag notes has_attachments
'cer_providers' => [
'cer_providers' => [
'fixer' => FixerIOv2::class,
'ratesapi' => RatesApiIOv1::class,
],
// expected source types for each transaction type, in order of preference.
'expected_source_types' => [
'source' => [
TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE,
AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
],
'destination' => [
TransactionTypeModel::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionTypeModel::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
],
],
// allowed source / destination accounts.
'source_dests' => [
TransactionTypeModel::WITHDRAWAL => [
AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH],
AccountType::LOAN => [AccountType::EXPENSE],
AccountType::DEBT => [AccountType::EXPENSE],
AccountType::MORTGAGE => [AccountType::EXPENSE],
],
TransactionTypeModel::DEPOSIT => [
AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::CASH => [AccountType::ASSET],
AccountType::LOAN => [AccountType::ASSET],
AccountType::DEBT => [AccountType::ASSET],
AccountType::MORTGAGE => [AccountType::ASSET],
],
TransactionTypeModel::TRANSFER => [
AccountType::ASSET => [AccountType::ASSET],
AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
],
TransactionTypeModel::OPENING_BALANCE => [
AccountType::ASSET => [AccountType::INITIAL_BALANCE],
AccountType::LOAN => [AccountType::INITIAL_BALANCE],
AccountType::DEBT => [AccountType::INITIAL_BALANCE],
AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE],
AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
],
TransactionTypeModel::RECONCILIATION => [
AccountType::RECONCILIATION => [AccountType::ASSET],
AccountType::ASSET => [AccountType::RECONCILIATION],
],
],
];

View File

@ -1,26 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
class ChangesForV480 extends Migration
{
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
//
}
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
}
}

View File

@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
* Class ChangesForV480
*/
class ChangesForV480 extends Migration
{
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table(
'transaction_journals',
function (Blueprint $table) {
// drop transaction_group_id + foreign key.
// cannot drop foreign keys in SQLite:
if ('sqlite' !== config('database.default')) {
$table->dropForeign('transaction_journals_transaction_group_id_foreign');
}
$table->dropColumn('transaction_group_id');
}
);
}
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table(
'transaction_journals',
function (Blueprint $table) {
$table->integer('transaction_currency_id', false, true)->nullable()->change();
// add column "group_id" after "transaction_type_id"
$table->integer('transaction_group_id', false, true)
->nullable()->default(null)->after('transaction_type_id');
// add foreign key for "transaction_group_id"
$table->foreign('transaction_group_id')->references('id')->on('transaction_groups')->onDelete('cascade');
}
);
}
}