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

View File

@ -78,6 +78,26 @@ class CreateController extends Controller
$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
$request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id]);
@ -87,7 +107,7 @@ class CreateController extends Controller
}
$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)
{
$data = $request->getAccountData();
$account = $this->repository->store($data);
$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);
$subTitle = (string)trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]);
$accountList = app('expandedform')->makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type]));
$what = $typeName;
unset($accountList[$account->id]);
// put previous url in session
$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 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
*
@ -373,6 +385,7 @@ class AccountRepository implements AccountRepositoryInterface
* Returns the date of the very first transaction in this account.
*
* @param Account $account
*
* @return TransactionJournal|null
*/
public function oldestJournal(Account $account): ?TransactionJournal

View File

@ -24,17 +24,18 @@ namespace FireflyIII\Repositories\Account;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Interface AccountRepositoryInterface.
*/
interface AccountRepositoryInterface
{
/**
* Moved here from account CRUD.
*
@ -87,6 +88,15 @@ interface AccountRepositoryInterface
*/
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
*
@ -164,6 +174,7 @@ interface AccountRepositoryInterface
* Returns the date of the very first transaction in this account.
*
* @param Account $account
*
* @return TransactionJournal|null
*/
public function oldestJournal(Account $account): ?TransactionJournal;

View File

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

View File

@ -528,6 +528,34 @@ class ExpandedForm
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 $name
@ -772,6 +800,7 @@ class ExpandedForm
return $html;
}
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param string $name
* @param string $view

View File

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

View File

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

View File

@ -27,6 +27,7 @@ use Carbon\Carbon;
use Exception;
use Illuminate\Validation\Validator;
use InvalidArgumentException;
use Log;
/**
* Trait RecurrenceValidation
@ -187,6 +188,7 @@ trait RecurrenceValidation
try {
Carbon::createFromFormat('Y-m-d', $moment);
} 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'));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -17,9 +17,17 @@
</div>
<div class="box-body">
{{ ExpandedForm.text('name') }}
{% if what == 'asset' %}
{% if what == 'asset' or what=='liabilities' %}
{{ ExpandedForm.currencyList('currency_id', null, {helpText:'account_default_currency'|_}) }}
{% 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>
@ -40,9 +48,10 @@
{{ ExpandedForm.amountNoCurrency('openingBalance') }}
{{ 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') }}
{% endif %}
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
</div>

View File

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