diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index aeb9b62b3b..8c06e1e63c 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -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(); diff --git a/app/Http/Controllers/Account/CreateController.php b/app/Http/Controllers/Account/CreateController.php index 10da6e9c81..f78ac9e166 100644 --- a/app/Http/Controllers/Account/CreateController.php +++ b/app/Http/Controllers/Account/CreateController.php @@ -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])); diff --git a/app/Http/Controllers/Account/DeleteController.php b/app/Http/Controllers/Account/DeleteController.php index 700a0660c7..3ec08fe917 100644 --- a/app/Http/Controllers/Account/DeleteController.php +++ b/app/Http/Controllers/Account/DeleteController.php @@ -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')); } /** diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index e865b10dbc..29616566f1 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -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 diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 09a0981209..8f88884e1a 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -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; diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php index a5bf55b646..6877ba13bb 100644 --- a/app/Services/Internal/Support/AccountServiceTrait.php +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -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 diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 0370df5f54..51609ca6c1 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -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 diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index 74198da564..e00093a7c9 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -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; } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 1034b1ef63..c44ac55640 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -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) { diff --git a/app/Validation/RecurrenceValidation.php b/app/Validation/RecurrenceValidation.php index 98ad0d0342..ad6614a0db 100644 --- a/app/Validation/RecurrenceValidation.php +++ b/app/Validation/RecurrenceValidation.php @@ -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')); } } diff --git a/config/firefly.php b/config/firefly.php index c996c32c76..69f9fd000c 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -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 diff --git a/config/twigbridge.php b/config/twigbridge.php index fa0557b7c9..7d3c3f0091 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -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' => [ diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index eaca551c4c..72edf342b2 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -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', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index c2dfc104eb..2007f077a9 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -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)', diff --git a/resources/views/accounts/create.twig b/resources/views/accounts/create.twig index 0fe42b070c..dc32ab6e77 100644 --- a/resources/views/accounts/create.twig +++ b/resources/views/accounts/create.twig @@ -17,9 +17,17 @@
{{ 'save_transactions_by_moving'|_ }}
diff --git a/resources/views/form/percentage.twig b/resources/views/form/percentage.twig new file mode 100644 index 0000000000..b5e73b7b91 --- /dev/null +++ b/resources/views/form/percentage.twig @@ -0,0 +1,12 @@ +