diff --git a/app/Console/Commands/Correction/TriggerCreditCalculation.php b/app/Console/Commands/Correction/TriggerCreditCalculation.php new file mode 100644 index 0000000000..ca1bab2e9e --- /dev/null +++ b/app/Console/Commands/Correction/TriggerCreditCalculation.php @@ -0,0 +1,62 @@ +processAccounts(); + return 0; + } + + private function processAccounts(): void + { + $accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + foreach ($accounts as $account) { + Log::debug(sprintf('Processing account #%d ("%s")', $account->id, $account->name)); + $this->processAccount($account); + } + } + + /** + * @param Account $account + * @return void + */ + private function processAccount(Account $account): void + { + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setAccount($account); + $object->recalculate(); + } +} diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradeLiabilities.php index 43c2cc68c5..e2c64278bf 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradeLiabilities.php @@ -145,10 +145,13 @@ class UpgradeLiabilities extends Command $this->correctOpeningBalance($account, $openingBalance); } - // add liability direction property - /** @var AccountMetaFactory $factory */ - $factory = app(AccountMetaFactory::class); - $factory->crud($account, 'liability_direction', 'debit'); + // add liability direction property (if it does not yet exist!) + $value = $repository->getMetaValue($account, 'liability_direction'); + if (null === $value) { + /** @var AccountMetaFactory $factory */ + $factory = app(AccountMetaFactory::class); + $factory->crud($account, 'liability_direction', 'debit'); + } } /** diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php b/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php new file mode 100644 index 0000000000..4a3b4cba4e --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php @@ -0,0 +1,208 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Console\Commands\Upgrade; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Services\Internal\Destroy\TransactionGroupDestroyService; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; +use FireflyIII\User; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +/** + * Class UpgradeLiabilitiesEight + */ +class UpgradeLiabilitiesEight extends Command +{ + public const CONFIG_NAME = '580_upgrade_liabilities'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Upgrade liabilities to new 5.8.0 structure.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:liabilities-580 {--F|force : Force the execution of this command.}'; + + /** + * Execute the console command. + * + * @return int + * @throws FireflyException + */ + public function handle(): int + { + $start = microtime(true); + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + $this->upgradeLiabilities(); + + // TODO uncomment me + //$this->markAsExecuted(); + + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Upgraded liabilities for 5.8.0 in %s seconds.', $end)); + + return 0; + } + + /** + * @return bool + * @throws FireflyException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + + /** + * + */ + private function upgradeLiabilities(): void + { + Log::debug('Upgrading liabilities.'); + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + $this->upgradeForUser($user); + } + } + + /** + * @param User $user + */ + private function upgradeForUser(User $user): void + { + Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); + $accounts = $user->accounts() + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + $this->upgradeLiability($account); + $service = app(CreditRecalculateService::class); + $service->setAccount($account); + $service->recalculate(); + } + } + + /** + * @param Account $account + */ + private function upgradeLiability(Account $account): void + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($account->user); + Log::debug(sprintf('Upgrade liability #%d ("%s")', $account->id, $account->name)); + + $direction = $repository->getMetaValue($account, 'liability_direction'); + if ('debit' === $direction && $this->hasBadOpening($account)) { + $this->deleteCreditTransaction($account); + $this->line(sprintf('Fixed correct bad opening for liability #%d ("%s")', $account->id, $account->name)); + } + } + + /** + * @param Account $account + * @return void + */ + private function deleteCreditTransaction(Account $account): void + { + $liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first(); + $liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $liabilityType->id) + ->first(['transaction_journals.*']); + if (null !== $liabilityJournal) { + $group = $liabilityJournal->transactionGroup; + $service = new TransactionGroupDestroyService(); + $service->destroy($group); + Log::debug(sprintf('Deleted liability credit group #%d', $group->id)); + } + } + + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * @param Account $account + * @return bool + */ + private function hasBadOpening(Account $account): bool + { + $openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); + $liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first(); + $openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $openingBalanceType->id) + ->first(['transaction_journals.*']); + if (null === $openingJournal) { + Log::debug('Account has no opening balance and can be skipped.'); + return false; + } + $liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->where('transaction_journals.transaction_type_id', $liabilityType->id) + ->first(['transaction_journals.*']); + if (null === $liabilityJournal) { + Log::debug('Account has no liability credit and can be skipped.'); + return false; + } + if (!$openingJournal->date->isSameDay($liabilityJournal->date)) { + Log::debug('Account has opening/credit not on the same day.'); + return false; + } + + return true; + } +} diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index 931fb680ee..33661f9d6c 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -39,8 +39,7 @@ use Log; */ class AccountTasker implements AccountTaskerInterface { - /** @var User */ - private $user; + private User $user; /** * @param Collection $accounts diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php index 8c177f7732..38800d8d34 100644 --- a/app/Services/Internal/Update/AccountUpdateService.php +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -95,8 +95,8 @@ class AccountUpdateService // update opening balance. $this->updateOpeningBalance($account, $data); - // update opening balance. - $this->updateCreditLiability($account, $data); + // Since 5.8.0, delete liability credit transactions, if any: + $this->deleteCreditTransaction($account); // update note: if (array_key_exists('notes', $data) && null !== $data['notes']) { @@ -305,6 +305,10 @@ class AccountUpdateService $this->deleteOBGroup($account); } } + // if cannot have an opening balance, delete it. + if(!in_array($type->type, $this->canHaveOpeningBalance, true)) { + $this->deleteOBGroup($account); + } } /** @@ -312,6 +316,7 @@ class AccountUpdateService * @param array $data * * @throws FireflyException + * @deprecated In Firefly III v5.8.0 and onwards, credit transactions for liabilities are no longer created. */ private function updateCreditLiability(Account $account, array $data): void { diff --git a/config/firefly.php b/config/firefly.php index e02260a5f6..9e35d4dae3 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -875,7 +875,7 @@ return [ 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), ], 'can_have_virtual_amounts' => [AccountType::ASSET], - 'can_have_opening_balance' => [AccountType::ASSET], + 'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], 'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], 'valid_cc_fields' => ['account_role', 'cc_monthly_payment_date', 'cc_type', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'],