Clean up liability overview.

This commit is contained in:
James Cole
2022-12-27 07:01:13 +01:00
parent eff631276e
commit bb11e61034
9 changed files with 139 additions and 88 deletions

View File

@@ -206,7 +206,7 @@ class AccountFactory
if ('' === (string) $databaseData['virtual_balance']) { if ('' === (string) $databaseData['virtual_balance']) {
$databaseData['virtual_balance'] = null; $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)) { if (!in_array($type->type, $this->canHaveVirtual, true)) {
$databaseData['virtual_balance'] = null; $databaseData['virtual_balance'] = null;
} }
@@ -217,14 +217,14 @@ class AccountFactory
$data = $this->cleanMetaDataArray($account, $data); $data = $this->cleanMetaDataArray($account, $data);
$this->storeMetaData($account, $data); $this->storeMetaData($account, $data);
// create opening balance // create opening balance (only asset accounts)
try { try {
$this->storeOpeningBalance($account, $data); $this->storeOpeningBalance($account, $data);
} catch (FireflyException $e) { } catch (FireflyException $e) {
Log::error($e->getMessage()); Log::error($e->getMessage());
} }
// create credit liability data (if relevant) // create credit liability data (only liabilities)
try { try {
$this->storeCreditLiability($account, $data); $this->storeCreditLiability($account, $data);
} catch (FireflyException $e) { } catch (FireflyException $e) {
@@ -352,16 +352,17 @@ class AccountFactory
$accountType = $account->accountType->type; $accountType = $account->accountType->type;
$direction = $this->accountRepository->getMetaValue($account, 'liability_direction'); $direction = $this->accountRepository->getMetaValue($account, 'liability_direction');
$valid = config('firefly.valid_liabilities'); $valid = config('firefly.valid_liabilities');
if (in_array($accountType, $valid, true) && 'credit' === $direction) { if (in_array($accountType, $valid, true)) {
Log::debug('Is a liability with credit direction.'); Log::debug('Is a liability with credit ("i am owed") direction.');
if ($this->validOBData($data)) { if ($this->validOBData($data)) {
Log::debug('Has valid CL data.'); Log::debug('Has valid CL data.');
$openingBalance = $data['opening_balance']; $openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date']; $openingBalanceDate = $data['opening_balance_date'];
$this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); // store credit transaction.
$this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
} }
if (!$this->validOBData($data)) { 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); $this->deleteCreditTransaction($account);
} }
} }

View File

@@ -231,7 +231,7 @@ class BoxController extends Controller
/** @var AccountRepositoryInterface $accountRepository */ /** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class); $accountRepository = app(AccountRepositoryInterface::class);
$allAccounts = $accountRepository->getActiveAccountsByType( $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())); Log::debug(sprintf('Found %d accounts.', $allAccounts->count()));

View File

@@ -364,7 +364,7 @@ class AccountRepository implements AccountRepositoryInterface
{ {
$journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id) ->where('transactions.account_id', $account->id)
->transactionTypes([TransactionType::OPENING_BALANCE]) ->transactionTypes([TransactionType::OPENING_BALANCE, TransactionType::LIABILITY_CREDIT])
->first(['transaction_journals.*']); ->first(['transaction_journals.*']);
if (null === $journal) { if (null === $journal) {
return null; return null;
@@ -388,7 +388,7 @@ class AccountRepository implements AccountRepositoryInterface
{ {
$journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id) ->where('transactions.account_id', $account->id)
->transactionTypes([TransactionType::OPENING_BALANCE]) ->transactionTypes([TransactionType::OPENING_BALANCE, TransactionType::LIABILITY_CREDIT])
->first(['transaction_journals.*']); ->first(['transaction_journals.*']);
if (null === $journal) { if (null === $journal) {
return null; return null;

View File

@@ -407,13 +407,22 @@ trait AccountServiceTrait
* @return TransactionGroup * @return TransactionGroup
* @throws FireflyException * @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__)); Log::debug(sprintf('Now in %s', __METHOD__));
if (0 === bccomp($openingBalance, '0')) { if (0 === bccomp($openingBalance, '0')) {
Log::debug('Amount is zero, so will not update liability credit group.'); Log::debug('Amount is zero, so will not update liability credit/debit group.');
throw new FireflyException('Amount for update liability credit was unexpectedly 0.'); 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: // create if not exists:
@@ -468,6 +477,23 @@ trait AccountServiceTrait
} }
$language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data; $language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data;
// 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); $amount = app('steam')->positive($openingBalance);
// get or grab currency: // get or grab currency:
@@ -484,10 +510,10 @@ trait AccountServiceTrait
[ [
'type' => 'Liability credit', 'type' => 'Liability credit',
'date' => $openingBalanceDate, 'date' => $openingBalanceDate,
'source_id' => null, 'source_id' => $sourceId,
'source_name' => trans('firefly.liability_credit_description', ['account' => $account->name], $language), 'source_name' => $sourceName,
'destination_id' => $account->id, 'destination_id' => $destId,
'destination_name' => null, 'destination_name' => $destName,
'user' => $account->user_id, 'user' => $account->user_id,
'currency_id' => $currency->id, 'currency_id' => $currency->id,
'order' => 0, 'order' => 0,

View File

@@ -62,9 +62,11 @@ class CreditRecalculateService
return; return;
} }
if (null !== $this->group && null === $this->account) { if (null !== $this->group && null === $this->account) {
Log::debug('Have to handle a group.');
$this->processGroup(); $this->processGroup();
} }
if (null !== $this->account && null === $this->group) { if (null !== $this->account && null === $this->group) {
Log::debug('Have to handle an account.');
// work based on account. // work based on account.
$this->processAccount(); $this->processAccount();
} }
@@ -213,7 +215,6 @@ class CreditRecalculateService
} }
$factory->crud($account, 'current_debt', $leftOfDebt); $factory->crud($account, 'current_debt', $leftOfDebt);
Log::debug(sprintf('Done with %s(#%d)', __METHOD__, $account->id)); 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)); Log::debug(sprintf('Processing group #%d, journal #%d of type "%s"', $journal->id, $groupId, $type));
// it's a withdrawal into this liability (from asset). // it's a withdrawal into this liability (from asset).
// if it's a credit, we don't care, because sending more money // if it's a credit ("I am owed"), this increases the amount due,
// to a credit-liability doesn't increase the amount (yet) // because we're lending person X more money
if ( if (
$type === TransactionType::WITHDRAWAL $type === TransactionType::WITHDRAWAL
&& (int)$account->id === (int)$transaction->account_id && (int)$account->id === (int)$transaction->account_id
&& 1 === bccomp($usedAmount, '0') && 1 === bccomp($usedAmount, '0')
&& 'credit' === $direction && '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; return $amount;
} }

View File

@@ -324,7 +324,7 @@ class AccountUpdateService
$openingBalance = $data['opening_balance']; $openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date']; $openingBalanceDate = $data['opening_balance_date'];
if ('credit' === $direction) { if ('credit' === $direction) {
$this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); $this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
} }
} }

View File

@@ -43,35 +43,34 @@ trait LiabilityValidation
Log::debug('Now in validateLCDestination', $array); Log::debug('Now in validateLCDestination', $array);
$result = null; $result = null;
$accountId = array_key_exists('id', $array) ? $array['id'] : null; $accountId = array_key_exists('id', $array) ? $array['id'] : null;
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
$validTypes = config('firefly.valid_liabilities'); $validTypes = config('firefly.valid_liabilities');
if (null === $accountId) { // if the ID is not null the source account should be a dummy account of the type liability credit.
$this->sourceError = (string) trans('validation.lc_destination_need_data'); // the ID of the destination must belong to a liability.
$result = false; 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.'); if (null !== $accountName && '' !== $accountName) {
$search = $this->accountRepository->find($accountId); 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, 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;
} }
// the source resulted in an account, AND it's of a valid type. Log::error('Destination ID is null, but destination name is also NULL.');
if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { return false;
Log::debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name));
$this->source = $search;
$result = true;
}
return $result ?? 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
* *
@@ -79,13 +78,33 @@ trait LiabilityValidation
*/ */
protected function validateLCSource(array $array): 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; $accountName = array_key_exists('name', $array) ? $array['name'] : null;
$result = true; $result = true;
Log::debug('Now in validateLCDestination', $array);
if ('' === $accountName || null === $accountName) { if ('' === $accountName || null === $accountName) {
Log::error('Array must have a name, is not the case, return false.');
$result = false; $result = false;
} }
if (true === $result) { if (true === $result) {
Log::error('Array has a name, return true.');
// set the source to be a (dummy) revenue account. // set the source to be a (dummy) revenue account.
$account = new Account(); $account = new Account();
$accountType = AccountType::whereType(AccountType::LIABILITY_CREDIT)->first(); $accountType = AccountType::whereType(AccountType::LIABILITY_CREDIT)->first();

View File

@@ -749,7 +749,7 @@ return [
'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3),
], ],
'can_have_virtual_amounts' => [AccountType::ASSET], '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_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_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'], 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'],

View File

@@ -15,7 +15,9 @@
<th>{{ trans('list.interest') }} ({{ trans('list.interest_period') }})</th> <th>{{ trans('list.interest') }} ({{ trans('list.interest_period') }})</th>
{% endif %} {% endif %}
<th class="hidden-sm hidden-xs">{{ trans('form.account_number') }}</th> <th class="hidden-sm hidden-xs">{{ trans('form.account_number') }}</th>
{% if objectType != 'liabilities' %}
<th style="text-align: right;">{{ trans('list.currentBalance') }}</th> <th style="text-align: right;">{{ trans('list.currentBalance') }}</th>
{% endif %}
{% if objectType == 'liabilities' %} {% if objectType == 'liabilities' %}
<th style="text-align: right;"> <th style="text-align: right;">
{{ trans('firefly.left_in_debt') }} {{ trans('firefly.left_in_debt') }}
@@ -61,11 +63,13 @@
<td>{{ account.interest }}% ({{ account.interestPeriod|lower }})</td> <td>{{ account.interest }}% ({{ account.interestPeriod|lower }})</td>
{% endif %} {% endif %}
<td class="hidden-sm hidden-xs">{{ account.iban }}{% if account.iban == '' %}{{ accountGetMetaField(account, 'account_number') }}{% endif %}</td> <td class="hidden-sm hidden-xs">{{ account.iban }}{% if account.iban == '' %}{{ accountGetMetaField(account, 'account_number') }}{% endif %}</td>
{% if objectType != 'liabilities' %}
<td style="text-align: right;"> <td style="text-align: right;">
<span style="margin-right:5px;"> <span style="margin-right:5px;">
{{ formatAmountByAccount(account, account.endBalance) }} {{ formatAmountByAccount(account, account.endBalance) }}
</span> </span>
</td> </td>
{% endif %}
{% if objectType == 'liabilities' %} {% if objectType == 'liabilities' %}
<td style="text-align: right;"> <td style="text-align: right;">
{% if '-' != account.current_debt %} {% if '-' != account.current_debt %}