diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index 25f1552532..5bf31bd99e 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -206,7 +206,7 @@ class AccountFactory if ('' === (string) $databaseData['virtual_balance']) { $databaseData['virtual_balance'] = null; } - // remove virtual balance when not an asset account or a liability + // remove virtual balance when not an asset account if (!in_array($type->type, $this->canHaveVirtual, true)) { $databaseData['virtual_balance'] = null; } @@ -217,14 +217,14 @@ class AccountFactory $data = $this->cleanMetaDataArray($account, $data); $this->storeMetaData($account, $data); - // create opening balance + // create opening balance (only asset accounts) try { $this->storeOpeningBalance($account, $data); } catch (FireflyException $e) { Log::error($e->getMessage()); } - // create credit liability data (if relevant) + // create credit liability data (only liabilities) try { $this->storeCreditLiability($account, $data); } catch (FireflyException $e) { @@ -352,16 +352,17 @@ class AccountFactory $accountType = $account->accountType->type; $direction = $this->accountRepository->getMetaValue($account, 'liability_direction'); $valid = config('firefly.valid_liabilities'); - if (in_array($accountType, $valid, true) && 'credit' === $direction) { - Log::debug('Is a liability with credit direction.'); + if (in_array($accountType, $valid, true)) { + Log::debug('Is a liability with credit ("i am owed") direction.'); if ($this->validOBData($data)) { Log::debug('Has valid CL data.'); $openingBalance = $data['opening_balance']; $openingBalanceDate = $data['opening_balance_date']; - $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + // store credit transaction. + $this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate); } if (!$this->validOBData($data)) { - Log::debug('Has NOT valid CL data.'); + Log::debug('Does NOT have valid CL data, deletr any CL transaction.'); $this->deleteCreditTransaction($account); } } diff --git a/app/Http/Controllers/Json/BoxController.php b/app/Http/Controllers/Json/BoxController.php index c7f64a05cf..9fec18bb89 100644 --- a/app/Http/Controllers/Json/BoxController.php +++ b/app/Http/Controllers/Json/BoxController.php @@ -231,7 +231,7 @@ class BoxController extends Controller /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); $allAccounts = $accountRepository->getActiveAccountsByType( - [AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD] + [AccountType::DEFAULT, AccountType::ASSET] ); Log::debug(sprintf('Found %d accounts.', $allAccounts->count())); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index d39282aa16..6dc64a5187 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -364,7 +364,7 @@ class AccountRepository implements AccountRepositoryInterface { $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transactions.account_id', $account->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->transactionTypes([TransactionType::OPENING_BALANCE, TransactionType::LIABILITY_CREDIT]) ->first(['transaction_journals.*']); if (null === $journal) { return null; @@ -388,7 +388,7 @@ class AccountRepository implements AccountRepositoryInterface { $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transactions.account_id', $account->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->transactionTypes([TransactionType::OPENING_BALANCE, TransactionType::LIABILITY_CREDIT]) ->first(['transaction_journals.*']); if (null === $journal) { return null; diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php index ff11004697..5b075a9aaf 100644 --- a/app/Services/Internal/Support/AccountServiceTrait.php +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -51,7 +51,7 @@ trait AccountServiceTrait protected AccountRepositoryInterface $accountRepository; /** - * @param null|string $iban + * @param null|string $iban * * @return null|string */ @@ -76,7 +76,7 @@ trait AccountServiceTrait /** * Returns true if the data in the array is submitted but empty. * - * @param array $data + * @param array $data * * @return bool */ @@ -104,8 +104,8 @@ trait AccountServiceTrait * * TODO this method treats expense accounts and liabilities the same way (tries to save interest) * - * @param Account $account - * @param array $data + * @param Account $account + * @param array $data * */ public function updateMetaData(Account $account, array $data): void @@ -155,14 +155,14 @@ trait AccountServiceTrait $data[$field] = 1; } - $factory->crud($account, $field, (string) $data[$field]); + $factory->crud($account, $field, (string)$data[$field]); } } } /** - * @param Account $account - * @param string $note + * @param Account $account + * @param string $note * * @codeCoverageIgnore * @return bool @@ -195,13 +195,13 @@ trait AccountServiceTrait /** * Verify if array contains valid data to possibly store or update the opening balance. * - * @param array $data + * @param array $data * * @return bool */ public function validOBData(array $data): bool { - $data['opening_balance'] = (string) ($data['opening_balance'] ?? ''); + $data['opening_balance'] = (string)($data['opening_balance'] ?? ''); if ('' !== $data['opening_balance'] && 0 === bccomp($data['opening_balance'], '0')) { $data['opening_balance'] = ''; } @@ -217,8 +217,8 @@ trait AccountServiceTrait } /** - * @param Account $account - * @param array $data + * @param Account $account + * @param array $data * * @return TransactionGroup * @throws FireflyException @@ -311,7 +311,7 @@ trait AccountServiceTrait /** * Delete TransactionGroup with liability credit in it. * - * @param Account $account + * @param Account $account */ protected function deleteCreditTransaction(Account $account): void { @@ -329,7 +329,7 @@ trait AccountServiceTrait /** * Returns the credit transaction group, or NULL if it does not exist. * - * @param Account $account + * @param Account $account * * @return TransactionGroup|null */ @@ -343,7 +343,7 @@ trait AccountServiceTrait /** * Delete TransactionGroup with opening balance in it. * - * @param Account $account + * @param Account $account */ protected function deleteOBGroup(Account $account): void { @@ -362,7 +362,7 @@ trait AccountServiceTrait /** * Returns the opening balance group, or NULL if it does not exist. * - * @param Account $account + * @param Account $account * * @return TransactionGroup|null */ @@ -372,8 +372,8 @@ trait AccountServiceTrait } /** - * @param int $currencyId - * @param string $currencyCode + * @param int $currencyId + * @param string $currencyCode * * @return TransactionCurrency * @throws FireflyException @@ -400,20 +400,29 @@ trait AccountServiceTrait /** * Create the opposing "credit liability" transaction for credit liabilities. * - * @param Account $account - * @param string $openingBalance - * @param Carbon $openingBalanceDate + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate * * @return TransactionGroup * @throws FireflyException */ - protected function updateCreditTransaction(Account $account, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup + protected function updateCreditTransaction(Account $account, string $direction, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup { Log::debug(sprintf('Now in %s', __METHOD__)); if (0 === bccomp($openingBalance, '0')) { - Log::debug('Amount is zero, so will not update liability credit group.'); - throw new FireflyException('Amount for update liability credit was unexpectedly 0.'); + Log::debug('Amount is zero, so will not update liability credit/debit group.'); + throw new FireflyException('Amount for update liability credit/debit was unexpectedly 0.'); + } + // if direction is "debit" (i owe this debt), amount is negative. + // which means the liability will have a negative balance which the user must fill. + $openingBalance = app('steam')->negative($openingBalance); + + // if direction is "credit" (I am owed this debt), amount is positive. + // which means the liability will have a positive balance which is drained when its paid back into any asset. + if ('credit' === $direction) { + $openingBalance = app('steam')->positive($openingBalance); } // create if not exists: @@ -451,9 +460,9 @@ trait AccountServiceTrait } /** - * @param Account $account - * @param string $openingBalance - * @param Carbon $openingBalanceDate + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate * * @return TransactionGroup * @throws FireflyException @@ -468,7 +477,24 @@ trait AccountServiceTrait } $language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data; - $amount = app('steam')->positive($openingBalance); + + // set source and/or destination based on whether the amount is positive or negative. + // first, assume the amount is positive and go from there: + // if amount is positive ("I am owed this debt"), source is special account, destination is the liability. + $sourceId = null; + $sourceName = trans('firefly.liability_credit_description', ['account' => $account->name], $language); + $destId = $account->id; + $destName = null; + if(-1 === bccomp($openingBalance, '0')) { + // amount is negative, reverse it + $sourceId = $account->id; + $sourceName = null; + $destId = null; + $destName = trans('firefly.liability_credit_description', ['account' => $account->name], $language); + } + + // amount must be positive for the transaction to work. + $amount = app('steam')->positive($openingBalance); // get or grab currency: $currency = $this->accountRepository->getAccountCurrency($account); @@ -484,10 +510,10 @@ trait AccountServiceTrait [ 'type' => 'Liability credit', 'date' => $openingBalanceDate, - 'source_id' => null, - 'source_name' => trans('firefly.liability_credit_description', ['account' => $account->name], $language), - 'destination_id' => $account->id, - 'destination_name' => null, + 'source_id' => $sourceId, + 'source_name' => $sourceName, + 'destination_id' => $destId, + 'destination_name' => $destName, 'user' => $account->user_id, 'currency_id' => $currency->id, 'order' => 0, @@ -526,7 +552,7 @@ trait AccountServiceTrait /** * TODO refactor to "getfirstjournal" * - * @param TransactionGroup $group + * @param TransactionGroup $group * * @return TransactionJournal * @throws FireflyException @@ -545,8 +571,8 @@ trait AccountServiceTrait /** * TODO Rename to getOpposingTransaction * - * @param TransactionJournal $journal - * @param Account $account + * @param TransactionJournal $journal + * @param Account $account * * @return Transaction * @throws FireflyException @@ -563,8 +589,8 @@ trait AccountServiceTrait } /** - * @param TransactionJournal $journal - * @param Account $account + * @param TransactionJournal $journal + * @param Account $account * * @return Transaction * @throws FireflyException @@ -584,9 +610,9 @@ trait AccountServiceTrait * Update or create the opening balance group. * Since opening balance and date can still be empty strings, it may fail. * - * @param Account $account - * @param string $openingBalance - * @param Carbon $openingBalanceDate + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate * * @return TransactionGroup * @throws FireflyException @@ -646,9 +672,9 @@ trait AccountServiceTrait } /** - * @param Account $account - * @param string $openingBalance - * @param Carbon $openingBalanceDate + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate * * @return TransactionGroup * @throws FireflyException diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index a47a9d8610..a9786f8e98 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -62,9 +62,11 @@ class CreditRecalculateService return; } if (null !== $this->group && null === $this->account) { + Log::debug('Have to handle a group.'); $this->processGroup(); } if (null !== $this->account && null === $this->group) { + Log::debug('Have to handle an account.'); // work based on account. $this->processAccount(); } @@ -213,7 +215,6 @@ class CreditRecalculateService } $factory->crud($account, 'current_debt', $leftOfDebt); - Log::debug(sprintf('Done with %s(#%d)', __METHOD__, $account->id)); } @@ -252,16 +253,16 @@ class CreditRecalculateService Log::debug(sprintf('Processing group #%d, journal #%d of type "%s"', $journal->id, $groupId, $type)); // it's a withdrawal into this liability (from asset). - // if it's a credit, we don't care, because sending more money - // to a credit-liability doesn't increase the amount (yet) + // if it's a credit ("I am owed"), this increases the amount due, + // because we're lending person X more money if ( $type === TransactionType::WITHDRAWAL && (int)$account->id === (int)$transaction->account_id && 1 === bccomp($usedAmount, '0') && 'credit' === $direction ) { - Log::debug(sprintf('Is withdrawal into credit liability #%d, does not influence the amount due.', $transaction->account_id)); - + $amount = bcadd($amount, app('steam')->positive($usedAmount)); + Log::debug(sprintf('Is withdrawal (%s) into credit liability #%d, will increase amount due to %s.', $transaction->account_id, $usedAmount, $amount)); return $amount; } diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php index 09df1a6b78..57029d206e 100644 --- a/app/Services/Internal/Update/AccountUpdateService.php +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -324,7 +324,7 @@ class AccountUpdateService $openingBalance = $data['opening_balance']; $openingBalanceDate = $data['opening_balance_date']; if ('credit' === $direction) { - $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + $this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate); } } diff --git a/app/Validation/Account/LiabilityValidation.php b/app/Validation/Account/LiabilityValidation.php index 30a23ebedf..feb8c155dc 100644 --- a/app/Validation/Account/LiabilityValidation.php +++ b/app/Validation/Account/LiabilityValidation.php @@ -34,58 +34,77 @@ use Log; trait LiabilityValidation { /** - * @param array $array + * @param array $array * * @return bool */ protected function validateLCDestination(array $array): bool { Log::debug('Now in validateLCDestination', $array); - $result = null; - $accountId = array_key_exists('id', $array) ? $array['id'] : null; - $validTypes = config('firefly.valid_liabilities'); + $result = null; + $accountId = array_key_exists('id', $array) ? $array['id'] : null; + $accountName = array_key_exists('name', $array) ? $array['name'] : null; + $validTypes = config('firefly.valid_liabilities'); - if (null === $accountId) { - $this->sourceError = (string) trans('validation.lc_destination_need_data'); - $result = false; + // if the ID is not null the source account should be a dummy account of the type liability credit. + // the ID of the destination must belong to a liability. + if (null !== $accountId) { + if (AccountType::LIABILITY_CREDIT !== $this?->source?->accountType?->type) { + Log::error('Source account is not a liability.'); + return false; + } + $result = $this->findExistingAccount($validTypes, $array); + if (null === $result) { + Log::error('Destination account is not a liability.'); + return false; + } + return true; } - Log::debug('Destination ID is not null.'); - $search = $this->accountRepository->find($accountId); - - // the source resulted in an account, but it's not of a valid type. - if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { - $message = sprintf('User submitted only an ID (#%d), which is a "%s", so this is not a valid destination.', $accountId, $search->accountType->type); - Log::debug($message); - $this->sourceError = $message; - $result = false; + if (null !== $accountName && '' !== $accountName) { + Log::debug('Destination ID is null, now we can assume the destination is a (new) liability credit account.'); + return true; } - // the source resulted in an account, AND it's of a valid type. - if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { - Log::debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name)); - $this->source = $search; - $result = true; - } - - return $result ?? false; + Log::error('Destination ID is null, but destination name is also NULL.'); + return false; } /** - * Source of an liability credit must be a liability. + * Source of a liability credit must be a liability or liability credit account. * - * @param array $array + * @param array $array * * @return bool */ protected function validateLCSource(array $array): bool { + Log::debug('Now in validateLCSource', $array); + // if the array has an ID and ID is not null, try to find it and check type. + // this account must be a liability + $accountId = array_key_exists('id', $array) ? $array['id'] : null; + if (null !== $accountId) { + Log::debug('Source ID is not null, assume were looking for a liability.'); + // find liability credit: + $result = $this->findExistingAccount(config('firefly.valid_liabilities'), $array); + if (null === $result) { + Log::error('Did not find a liability account, return false.'); + return false; + } + Log::debug(sprintf('Return true, found #%d ("%s")', $result->id, $result->name)); + $this->source = $result; + return true; + } + + // if array has name and is not null, return true. $accountName = array_key_exists('name', $array) ? $array['name'] : null; - $result = true; - Log::debug('Now in validateLCDestination', $array); + + $result = true; if ('' === $accountName || null === $accountName) { + Log::error('Array must have a name, is not the case, return false.'); $result = false; } if (true === $result) { + Log::error('Array has a name, return true.'); // set the source to be a (dummy) revenue account. $account = new Account(); $accountType = AccountType::whereType(AccountType::LIABILITY_CREDIT)->first(); diff --git a/config/firefly.php b/config/firefly.php index 3c51ed54df..eb9dc83f29 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -749,7 +749,7 @@ return [ 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), ], 'can_have_virtual_amounts' => [AccountType::ASSET], - 'can_have_opening_balance' => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], + 'can_have_opening_balance' => [AccountType::ASSET], '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'], diff --git a/resources/views/list/accounts.twig b/resources/views/list/accounts.twig index 4108e9625a..0790643c45 100644 --- a/resources/views/list/accounts.twig +++ b/resources/views/list/accounts.twig @@ -15,7 +15,9 @@ {{ trans('list.interest') }} ({{ trans('list.interest_period') }}) {% endif %} {{ trans('form.account_number') }} - {{ trans('list.currentBalance') }} + {% if objectType != 'liabilities' %} + {{ trans('list.currentBalance') }} + {% endif %} {% if objectType == 'liabilities' %} {{ trans('firefly.left_in_debt') }} @@ -61,11 +63,13 @@ {{ account.interest }}% ({{ account.interestPeriod|lower }}) {% endif %} {{ account.iban }}{% if account.iban == '' %}{{ accountGetMetaField(account, 'account_number') }}{% endif %} + {% if objectType != 'liabilities' %} {{ formatAmountByAccount(account, account.endBalance) }} + {% endif %} {% if objectType == 'liabilities' %} {% if '-' != account.current_debt %}