Basic code for tracking liabilities.

This commit is contained in:
James Cole 2018-08-04 17:30:47 +02:00
parent f0d3ca5d53
commit 8dbc846314
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
17 changed files with 161 additions and 43 deletions

View File

@ -32,6 +32,7 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Services\Internal\Support\AccountServiceTrait; use FireflyIII\Services\Internal\Support\AccountServiceTrait;
use FireflyIII\User; use FireflyIII\User;
use Log;
/** /**
* Factory to create or return accounts. * Factory to create or return accounts.
@ -80,8 +81,9 @@ class AccountFactory
'iban' => $data['iban'], 'iban' => $data['iban'],
]; ];
// remove virtual balance when not an asset account: // remove virtual balance when not an asset account or a liability
if ($type->type !== AccountType::ASSET) { $canHaveVirtual = [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD];
if (!\in_array($type->type, $canHaveVirtual, true)) {
$databaseData['virtual_balance'] = '0'; $databaseData['virtual_balance'] = '0';
} }
@ -93,7 +95,7 @@ class AccountFactory
$return = Account::create($databaseData); $return = Account::create($databaseData);
$this->updateMetaData($return, $data); $this->updateMetaData($return, $data);
if ($type->type === AccountType::ASSET) { if (\in_array($type->type, $canHaveVirtual, true)) {
if ($this->validIBData($data)) { if ($this->validIBData($data)) {
$this->updateIB($return, $data); $this->updateIB($return, $data);
} }
@ -188,9 +190,12 @@ class AccountFactory
$result = AccountType::find($accountTypeId); $result = AccountType::find($accountTypeId);
} }
if (null === $result) { if (null === $result) {
/** @var string $type */ Log::debug(sprintf('No account type found by ID, continue search for "%s".', $accountType));
$type = (string)config('firefly.accountTypeByIdentifier.' . $accountType); /** @var array $types */
$result = AccountType::whereType($type)->first(); $types = config('firefly.accountTypeByIdentifier.' . $accountType) ?? [];
if (\count($types) > 0) {
$result = AccountType::whereIn('types', $types)->first();
}
if (null === $result && null !== $accountType) { if (null === $result && null !== $accountType) {
// try as full name: // try as full name:
$result = AccountType::whereType($accountType)->first(); $result = AccountType::whereType($accountType)->first();

View File

@ -78,6 +78,26 @@ class CreateController extends Controller
$roles[$role] = (string)trans('firefly.account_role_' . $role); $roles[$role] = (string)trans('firefly.account_role_' . $role);
} }
// types of liability:
$debt = $this->repository->getAccountTypeByType(AccountType::DEBT);
$loan = $this->repository->getAccountTypeByType(AccountType::LOAN);
$mortgage = $this->repository->getAccountTypeByType(AccountType::MORTGAGE);
$creditCard = $this->repository->getAccountTypeByType(AccountType::CREDITCARD);
$liabilityTypes = [
$debt->id => (string)trans('firefly.account_type_' . AccountType::DEBT),
$loan->id => (string)trans('firefly.account_type_' . AccountType::LOAN),
$mortgage->id => (string)trans('firefly.account_type_' . AccountType::MORTGAGE),
$creditCard->id => (string)trans('firefly.account_type_' . AccountType::CREDITCARD),
];
asort($liabilityTypes);
// interest calculation periods:
$interestPeriods = [
'daily' => (string)trans('firefly.interest_calc_daily'),
'monthly' => (string)trans('firefly.interest_calc_monthly'),
'yearly' => (string)trans('firefly.interest_calc_yearly'),
];
// pre fill some data // pre fill some data
$request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id]); $request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id]);
@ -87,7 +107,7 @@ class CreateController extends Controller
} }
$request->session()->forget('accounts.create.fromStore'); $request->session()->forget('accounts.create.fromStore');
return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'roles')); return view('accounts.create', compact('subTitleIcon', 'what','interestPeriods', 'subTitle', 'roles', 'liabilityTypes'));
} }
@ -100,6 +120,7 @@ class CreateController extends Controller
*/ */
public function store(AccountFormRequest $request) public function store(AccountFormRequest $request)
{ {
$data = $request->getAccountData(); $data = $request->getAccountData();
$account = $this->repository->store($data); $account = $this->repository->store($data);
$request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name])); $request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name]));

View File

@ -69,12 +69,13 @@ class DeleteController extends Controller
$typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type); $typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type);
$subTitle = (string)trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]); $subTitle = (string)trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]);
$accountList = app('expandedform')->makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type])); $accountList = app('expandedform')->makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type]));
$what = $typeName;
unset($accountList[$account->id]); unset($accountList[$account->id]);
// put previous url in session // put previous url in session
$this->rememberPreviousUri('accounts.delete.uri'); $this->rememberPreviousUri('accounts.delete.uri');
return view('accounts.delete', compact('account', 'subTitle', 'accountList')); return view('accounts.delete', compact('account', 'subTitle', 'accountList', 'what'));
} }
/** /**

View File

@ -168,6 +168,18 @@ class AccountRepository implements AccountRepositoryInterface
return $this->user->accounts()->find($accountId); return $this->user->accounts()->find($accountId);
} }
/**
* Return account type or null if not found.
*
* @param string $type
*
* @return AccountType|null
*/
public function getAccountTypeByType(string $type): ?AccountType
{
return AccountType::whereType($type)->first();
}
/** /**
* @param array $accountIds * @param array $accountIds
* *
@ -373,6 +385,7 @@ class AccountRepository implements AccountRepositoryInterface
* Returns the date of the very first transaction in this account. * Returns the date of the very first transaction in this account.
* *
* @param Account $account * @param Account $account
*
* @return TransactionJournal|null * @return TransactionJournal|null
*/ */
public function oldestJournal(Account $account): ?TransactionJournal public function oldestJournal(Account $account): ?TransactionJournal

View File

@ -24,17 +24,18 @@ namespace FireflyIII\Repositories\Account;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
/** /**
* Interface AccountRepositoryInterface. * Interface AccountRepositoryInterface.
*/ */
interface AccountRepositoryInterface interface AccountRepositoryInterface
{ {
/** /**
* Moved here from account CRUD. * Moved here from account CRUD.
* *
@ -87,6 +88,15 @@ interface AccountRepositoryInterface
*/ */
public function findNull(int $accountId): ?Account; public function findNull(int $accountId): ?Account;
/**
* Return account type or null if not found.
*
* @param string $type
*
* @return AccountType|null
*/
public function getAccountTypeByType(string $type): ?AccountType;
/** /**
* @param array $accountIds * @param array $accountIds
* *
@ -164,6 +174,7 @@ interface AccountRepositoryInterface
* Returns the date of the very first transaction in this account. * Returns the date of the very first transaction in this account.
* *
* @param Account $account * @param Account $account
*
* @return TransactionJournal|null * @return TransactionJournal|null
*/ */
public function oldestJournal(Account $account): ?TransactionJournal; public function oldestJournal(Account $account): ?TransactionJournal;

View File

@ -49,7 +49,7 @@ trait AccountServiceTrait
/** @var array */ /** @var array */
public $validCCFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC']; public $validCCFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC'];
/** @var array */ /** @var array */
public $validFields = ['accountNumber', 'currency_id', 'BIC']; public $validFields = ['accountNumber', 'currency_id', 'BIC','interest','interest_period'];
/** /**
* @param Account $account * @param Account $account

View File

@ -528,6 +528,34 @@ class ExpandedForm
return $html; return $html;
} }
/**
* Function to render a percentage.
*
* @param string $name
* @param mixed $value
* @param array $options
*
* @return string
*
*/
public function percentage(string $name, $value = null, array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
unset($options['placeholder']);
try {
$html = view('form.percentage', compact('classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
Log::debug(sprintf('Could not render percentage(): %s', $e->getMessage()));
$html = 'Could not render percentage.';
}
return $html;
}
/** /**
* @param string $type * @param string $type
* @param string $name * @param string $name
@ -772,6 +800,7 @@ class ExpandedForm
return $html; return $html;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param string $name * @param string $name
* @param string $view * @param string $view

View File

@ -69,7 +69,9 @@ class Translation extends Twig_Extension
'journalLinkTranslation', 'journalLinkTranslation',
function (string $direction, string $original) { function (string $direction, string $original) {
$key = sprintf('firefly.%s_%s', $original, $direction); $key = sprintf('firefly.%s_%s', $original, $direction);
return $key;
$translation = trans($key); $translation = trans($key);
if ($key === $translation) { if ($key === $translation) {
return $original; return $original;
} }

View File

@ -38,7 +38,9 @@ use FireflyIII\TransactionRules\Triggers\TriggerInterface;
use FireflyIII\User; use FireflyIII\User;
use Google2FA; use Google2FA;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Collection;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
use Log;
/** /**
* Class FireflyValidator. * Class FireflyValidator.
@ -195,12 +197,12 @@ class FireflyValidator extends Validator
* *
* @return bool * @return bool
*/ */
public function validateMore($attribute, $value, $parameters): bool public function validateLess($attribute, $value, $parameters): bool
{ {
/** @var mixed $compare */ /** @var mixed $compare */
$compare = $parameters[0] ?? '0'; $compare = $parameters[0] ?? '0';
return bccomp((string)$value, (string)$compare) > 0; return bccomp((string)$value, (string)$compare) < 0;
} }
/** /**
@ -211,15 +213,14 @@ class FireflyValidator extends Validator
* *
* @return bool * @return bool
*/ */
public function validateLess($attribute, $value, $parameters): bool public function validateMore($attribute, $value, $parameters): bool
{ {
/** @var mixed $compare */ /** @var mixed $compare */
$compare = $parameters[0] ?? '0'; $compare = $parameters[0] ?? '0';
return bccomp((string)$value, (string)$compare) < 0; return bccomp((string)$value, (string)$compare) > 0;
} }
/** /**
* @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.UnusedFormalParameter)
* *
@ -291,7 +292,7 @@ class FireflyValidator extends Validator
case 'link_to_bill': case 'link_to_bill':
/** @var BillRepositoryInterface $repository */ /** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class); $repository = app(BillRepositoryInterface::class);
$bill = $repository->findByName((string)$value); $bill = $repository->findByName($value);
return null !== $bill; return null !== $bill;
case 'invalid': case 'invalid':
@ -468,7 +469,7 @@ class FireflyValidator extends Validator
if ((int)$accountId > 0) { if ((int)$accountId > 0) {
// exclude current account from check. // exclude current account from check.
$query->where('account_meta.account_id', '!=', (int)$accountId); $query->where('account_meta.account_id', '!=', $accountId);
} }
$set = $query->get(['account_meta.*']); $set = $query->get(['account_meta.*']);
@ -499,9 +500,7 @@ class FireflyValidator extends Validator
public function validateUniqueObjectForUser($attribute, $value, $parameters): bool public function validateUniqueObjectForUser($attribute, $value, $parameters): bool
{ {
$value = $this->tryDecrypt($value); $value = $this->tryDecrypt($value);
// exclude? [$table, $field] = $parameters;
$table = $parameters[0];
$field = $parameters[1];
$exclude = (int)($parameters[2] ?? 0.0); $exclude = (int)($parameters[2] ?? 0.0);
/* /*
@ -630,7 +629,7 @@ class FireflyValidator extends Validator
try { try {
$value = Crypt::decrypt($value); $value = Crypt::decrypt($value);
} catch (DecryptException $e) { } catch (DecryptException $e) {
// do not care. Log::debug(sprintf('Could not decrypt. %s', $e->getMessage()));
} }
return $value; return $value;
@ -717,11 +716,14 @@ class FireflyValidator extends Validator
*/ */
private function validateByAccountTypeString(string $value, array $parameters, string $type): bool private function validateByAccountTypeString(string $value, array $parameters, string $type): bool
{ {
$search = Config::get('firefly.accountTypeByIdentifier.' . $type); /** @var array $search */
$accountType = AccountType::whereType($search)->first(); $search = Config::get('firefly.accountTypeByIdentifier.' . $type);
$ignore = (int)($parameters[0] ?? 0.0); /** @var Collection $accountTypes */
$accountTypes = AccountType::whereIn('type', $search)->get();
$set = auth()->user()->accounts()->where('account_type_id', $accountType->id)->where('id', '!=', $ignore)->get(); $ignore = (int)($parameters[0] ?? 0.0);
$accountTypeIds = $accountTypes->pluck('id')->toArray();
/** @var Collection $set */
$set = auth()->user()->accounts()->whereIn('account_type_id', $accountTypeIds)->where('id', '!=', $ignore)->get();
/** @var Account $entry */ /** @var Account $entry */
foreach ($set as $entry) { foreach ($set as $entry) {
if ($entry->name === $value) { if ($entry->name === $value) {

View File

@ -27,6 +27,7 @@ use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
use InvalidArgumentException; use InvalidArgumentException;
use Log;
/** /**
* Trait RecurrenceValidation * Trait RecurrenceValidation
@ -187,6 +188,7 @@ trait RecurrenceValidation
try { try {
Carbon::createFromFormat('Y-m-d', $moment); Carbon::createFromFormat('Y-m-d', $moment);
} catch (InvalidArgumentException|Exception $e) { } catch (InvalidArgumentException|Exception $e) {
Log::debug(sprintf('Invalid argument for Carbon: %s', $e->getMessage()));
$validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment'));
} }
} }

View File

@ -176,10 +176,11 @@ return [
], ],
'subTitlesByIdentifier' => 'subTitlesByIdentifier' =>
[ [
'asset' => 'Asset accounts', 'asset' => 'Asset accounts',
'expense' => 'Expense accounts', 'expense' => 'Expense accounts',
'revenue' => 'Revenue accounts', 'revenue' => 'Revenue accounts',
'cash' => 'Cash accounts', 'cash' => 'Cash accounts',
'liabilities' => 'Liabilities',
], ],
'subIconsByIdentifier' => 'subIconsByIdentifier' =>
[ [
@ -194,6 +195,7 @@ return [
'Revenue account' => 'fa-download', 'Revenue account' => 'fa-download',
'import' => 'fa-download', 'import' => 'fa-download',
'Import account' => 'fa-download', 'Import account' => 'fa-download',
'liabilities' => 'fa-ticket',
], ],
'accountTypesByIdentifier' => 'accountTypesByIdentifier' =>
[ [
@ -205,13 +207,14 @@ return [
], ],
'accountTypeByIdentifier' => 'accountTypeByIdentifier' =>
[ [
'asset' => 'Asset account', 'asset' => ['Asset account'],
'expense' => 'Expense account', 'expense' => ['Expense account'],
'revenue' => 'Revenue account', 'revenue' => ['Revenue account'],
'opening' => 'Initial balance account', 'opening' => ['Initial balance account'],
'initial' => 'Initial balance account', 'initial' => ['Initial balance account'],
'import' => 'Import account', 'import' => ['Import account'],
'reconcile' => 'Reconciliation account', 'reconcile' => ['Reconciliation account'],
'liabilities' => ['Loan', 'Debt', 'Mortgage', 'Credit card'],
], ],
'shortNamesByFullName' => 'shortNamesByFullName' =>
[ [
@ -222,6 +225,10 @@ return [
'Beneficiary account' => 'expense', 'Beneficiary account' => 'expense',
'Revenue account' => 'revenue', 'Revenue account' => 'revenue',
'Cash account' => 'cash', 'Cash account' => 'cash',
'Credit card' => 'liabilities',
'Loan' => 'liabilities',
'Debt' => 'liabilities',
'Mortgage' => 'liabilities',
], ],
'languages' => [ 'languages' => [
// completed languages // completed languages

View File

@ -189,7 +189,7 @@ return [
'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', 'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location',
'file', 'staticText', 'password', 'nonSelectableAmount', 'file', 'staticText', 'password', 'nonSelectableAmount',
'number', 'assetAccountList','amountNoCurrency','currencyList','ruleGroupList','assetAccountCheckList','ruleGroupListWithEmpty', 'number', 'assetAccountList','amountNoCurrency','currencyList','ruleGroupList','assetAccountCheckList','ruleGroupListWithEmpty',
'piggyBankList','currencyListEmpty','activeAssetAccountList' 'piggyBankList','currencyListEmpty','activeAssetAccountList','percentage'
], ],
], ],
'Form' => [ 'Form' => [

View File

@ -591,7 +591,6 @@ return [
'source_or_dest_invalid' => 'Cannot find the correct transaction details. Conversion is not possible.', 'source_or_dest_invalid' => 'Cannot find the correct transaction details. Conversion is not possible.',
// create new stuff: // create new stuff:
'create_new_withdrawal' => 'Create new withdrawal', 'create_new_withdrawal' => 'Create new withdrawal',
'create_new_deposit' => 'Create new deposit', 'create_new_deposit' => 'Create new deposit',
@ -689,6 +688,7 @@ return [
'delete_asset_account' => 'Delete asset account ":name"', 'delete_asset_account' => 'Delete asset account ":name"',
'delete_expense_account' => 'Delete expense account ":name"', 'delete_expense_account' => 'Delete expense account ":name"',
'delete_revenue_account' => 'Delete revenue account ":name"', 'delete_revenue_account' => 'Delete revenue account ":name"',
'delete_liabilities_account' => 'Delete liability ":name"',
'asset_deleted' => 'Successfully deleted asset account ":name"', 'asset_deleted' => 'Successfully deleted asset account ":name"',
'expense_deleted' => 'Successfully deleted expense account ":name"', 'expense_deleted' => 'Successfully deleted expense account ":name"',
'revenue_deleted' => 'Successfully deleted revenue account ":name"', 'revenue_deleted' => 'Successfully deleted revenue account ":name"',
@ -756,6 +756,9 @@ return [
'already_cleared_transactions' => 'Already cleared transactions (:count)', 'already_cleared_transactions' => 'Already cleared transactions (:count)',
'submitted_end_balance' => 'Submitted end balance', 'submitted_end_balance' => 'Submitted end balance',
'initial_balance_description' => 'Initial balance for ":account"', 'initial_balance_description' => 'Initial balance for ":account"',
'interest_calc_daily' => 'Per day',
'interest_calc_monthly' => 'Per month',
'interest_calc_yearly' => 'Per year',
// categories: // categories:
'new_category' => 'New category', 'new_category' => 'New category',

View File

@ -86,6 +86,7 @@ return [
'remember_me' => 'Remember me', 'remember_me' => 'Remember me',
'liability_type_id' => 'Liability type', 'liability_type_id' => 'Liability type',
'interest' => 'Interest', 'interest' => 'Interest',
'interest_period' => 'Interest period',
'source_account_asset' => 'Source account (asset account)', 'source_account_asset' => 'Source account (asset account)',
'destination_account_expense' => 'Destination account (expense account)', 'destination_account_expense' => 'Destination account (expense account)',

View File

@ -17,9 +17,17 @@
</div> </div>
<div class="box-body"> <div class="box-body">
{{ ExpandedForm.text('name') }} {{ ExpandedForm.text('name') }}
{% if what == 'asset' %} {% if what == 'asset' or what=='liabilities' %}
{{ ExpandedForm.currencyList('currency_id', null, {helpText:'account_default_currency'|_}) }} {{ ExpandedForm.currencyList('currency_id', null, {helpText:'account_default_currency'|_}) }}
{% endif %} {% endif %}
{% if what == 'liabilities' %}
{{ ExpandedForm.select('liability_type_id', liabilityTypes) }}
{{ ExpandedForm.amountNoCurrency('openingBalance', null, {label:'debt_start_amount'|_, helpText: 'debt_start_amount_help'|_}) }}
{{ ExpandedForm.date('openingBalanceDate', null, {label:'debt_start_date'|_}) }}
{{ ExpandedForm.percentage('interest') }}
{{ ExpandedForm.select('interest_period', interestPeriods) }}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -40,9 +48,10 @@
{{ ExpandedForm.amountNoCurrency('openingBalance') }} {{ ExpandedForm.amountNoCurrency('openingBalance') }}
{{ ExpandedForm.date('openingBalanceDate') }} {{ ExpandedForm.date('openingBalanceDate') }}
{{ ExpandedForm.select('accountRole', roles,null,{'helpText' : 'asset_account_role_help'|_}) }} {{ ExpandedForm.select('accountRole', roles,null,{helpText : 'asset_account_role_help'|_}) }}
{{ ExpandedForm.amountNoCurrency('virtualBalance') }} {{ ExpandedForm.amountNoCurrency('virtualBalance') }}
{% endif %} {% endif %}
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }} {{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
</div> </div>

View File

@ -33,7 +33,7 @@
{% endif %} {% endif %}
</p> </p>
{% endif %} {% endif %}
{% if account.transactions.count > 0 %} {% if account.transactions.count > 0 and account.accountType.type == 'Asset account' %}
<p class="text-success"> <p class="text-success">
{{ 'save_transactions_by_moving'|_ }} {{ 'save_transactions_by_moving'|_ }}
</p> </p>

View File

@ -0,0 +1,12 @@
<div class="{{ classes }}" id="{{ name }}_holder">
<label for="{{ options.id }}" class="col-sm-4 control-label">{{ label }}</label>
<div class="col-sm-8">
<div class="input-group">
{{ Form.input('number', name, value, options) }}
<div class="input-group-addon">%</div>
</div>
{% include 'form/help' %}
{% include 'form/feedback' %}
</div>
</div>