From 083749e8fea9c733787737e097ccd72600a2ff51 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 07:56:50 +0200 Subject: [PATCH 01/30] Allow user to submit direction. --- .../V1/Requests/Models/Account/StoreRequest.php | 14 ++++++++------ .../V1/Requests/Models/Account/UpdateRequest.php | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/Api/V1/Requests/Models/Account/StoreRequest.php b/app/Api/V1/Requests/Models/Account/StoreRequest.php index deb9634225..6659baf155 100644 --- a/app/Api/V1/Requests/Models/Account/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Account/StoreRequest.php @@ -83,7 +83,8 @@ class StoreRequest extends FormRequest if ('liability' === $data['account_type_name'] || 'liabilities' === $data['account_type_name']) { $data['opening_balance'] = bcmul($this->string('liability_amount'), '-1'); $data['opening_balance_date'] = $this->date('liability_start_date'); - $data['account_type_name'] = $this->string('liability_type'); + $data['account_type_name'] = $this->string('liability_type'); + $data['liability_direction'] = $this->string('liability_direction'); $data['account_type_id'] = null; } @@ -118,11 +119,12 @@ class StoreRequest extends FormRequest 'account_role' => sprintf('in:%s|required_if:type,asset', $accountRoles), 'credit_card_type' => sprintf('in:%s|required_if:account_role,ccAsset', $ccPaymentTypes), 'monthly_payment_date' => 'date' . '|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull', - 'liability_type' => 'required_if:type,liability|in:loan,debt,mortgage', - 'liability_amount' => 'required_if:type,liability|min:0|numeric', - 'liability_start_date' => 'required_if:type,liability|date', - 'interest' => 'required_if:type,liability|between:0,100|numeric', - 'interest_period' => 'required_if:type,liability|in:daily,monthly,yearly', + 'liability_type' => 'required_if:type,liability|required_if:type,liabilities|in:loan,debt,mortgage', + 'liability_amount' => 'required_if:type,liability|required_if:type,liabilities|min:0|numeric', + 'liability_direction' => 'required_if:type,liability|required_if:type,liabilities|in:credit,debit', + 'liability_start_date' => 'required_if:type,liability|required_if:type,liabilities|date', + 'interest' => 'required_if:type,liability|required_if:type,liabilities|between:0,100|numeric', + 'interest_period' => 'required_if:type,liability|required_if:type,liabilities|in:daily,monthly,yearly', 'notes' => 'min:0|max:65536', ]; diff --git a/app/Api/V1/Requests/Models/Account/UpdateRequest.php b/app/Api/V1/Requests/Models/Account/UpdateRequest.php index 4b30887327..c53f70168a 100644 --- a/app/Api/V1/Requests/Models/Account/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Account/UpdateRequest.php @@ -68,6 +68,7 @@ class UpdateRequest extends FormRequest 'order' => ['order', 'integer'], 'currency_id' => ['currency_id', 'integer'], 'currency_code' => ['currency_code', 'string'], + 'liability_direction' => ['liability_direction', 'string'] ]; $data = $this->getAllData($fields); $data = $this->appendLocationData($data, null); @@ -112,6 +113,7 @@ class UpdateRequest extends FormRequest 'credit_card_type' => sprintf('in:%s|nullable|required_if:account_role,ccAsset', $ccPaymentTypes), 'monthly_payment_date' => 'date' . '|nullable|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull', 'liability_type' => 'required_if:type,liability|in:loan,debt,mortgage', + 'liability_direction' => 'required_if:type,liability|in:credit,debit', 'interest' => 'required_if:type,liability|between:0,100|numeric', 'interest_period' => 'required_if:type,liability|in:daily,monthly,yearly', 'notes' => 'min:0|max:65536', From 3a3cec4f9ab9a68c93efa9e696791159a46eae5c Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 07:57:04 +0200 Subject: [PATCH 02/30] Store direction. --- app/Factory/AccountFactory.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index e4d3394f34..40336ccaa4 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -238,6 +238,10 @@ class AccountFactory if ($account->accountType->type !== AccountType::ASSET) { $accountRole = ''; } + // only liability may have direction: + if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) { + $data['liability_direction'] = null; + } $data['account_role'] = $accountRole; $data['currency_id'] = $currency->id; @@ -257,7 +261,7 @@ class AccountFactory $fields = $this->validAssetFields; } if ($account->accountType->type === AccountType::ASSET && 'ccAsset' === $data['account_role']) { - $fields = $this->validCCFields; + $fields = $this->validCCFields; } /** @var AccountMetaFactory $factory */ @@ -269,10 +273,10 @@ class AccountFactory // convert boolean value: if (is_bool($data[$field]) && false === $data[$field]) { - $data[$field] = 0; + $data[$field] = 0; } if (is_bool($data[$field]) && true === $data[$field]) { - $data[$field] = 1; + $data[$field] = 1; } $factory->crud($account, $field, (string)$data[$field]); From dcc5d9f583108679daef5e03f0dc10c17262ac18 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 07:58:13 +0200 Subject: [PATCH 03/30] Trigger recalculation of credit when editing or storing a group. --- app/Providers/EventServiceProvider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 34bdfe4209..0436595d44 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -98,12 +98,14 @@ class EventServiceProvider extends ServiceProvider // is a Transaction Journal related event. StoredTransactionGroup::class => [ 'FireflyIII\Handlers\Events\StoredGroupEventHandler@processRules', + 'FireflyIII\Handlers\Events\StoredGroupEventHandler@recalculateCredit', 'FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerWebhooks', ], // is a Transaction Journal related event. UpdatedTransactionGroup::class => [ 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@unifyAccounts', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@processRules', + 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@recalculateCredit', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@triggerWebhooks', ], DestroyedTransactionGroup::class => [ From 21ac42d3a6a36fba8782dacda7fa06afc65b57d7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 07:59:41 +0200 Subject: [PATCH 04/30] Call service to recalculate debt. --- app/Handlers/Events/StoredGroupEventHandler.php | 13 +++++++++++++ app/Handlers/Events/UpdatedGroupEventHandler.php | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 3824d6f499..f89100129e 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -28,6 +28,7 @@ use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\Webhook; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use Illuminate\Support\Collection; use Log; @@ -101,4 +102,16 @@ class StoredGroupEventHandler event(new RequestedSendWebhookMessages); } + /** + * @param StoredTransactionGroup $event + */ + public function recalculateCredit(StoredTransactionGroup $event): void + { + $group = $event->transactionGroup; + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setGroup($group); + $object->recalculate(); + } + } diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index 5cd7cf4619..3a6bc19466 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -31,6 +31,7 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Models\Webhook; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use Illuminate\Support\Collection; use Log; @@ -94,6 +95,18 @@ class UpdatedGroupEventHandler event(new RequestedSendWebhookMessages); } + /** + * @param UpdatedTransactionGroup $event + */ + public function recalculateCredit(UpdatedTransactionGroup $event): void + { + $group = $event->transactionGroup; + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setGroup($group); + $object->recalculate(); + } + /** * This method will make sure all source / destination accounts are the same. * From 01234b52e3fccfc7fbb77f8357e2bca56f18ecec Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 07:59:54 +0200 Subject: [PATCH 05/30] Recaculation service (does not do much yet). --- .../Support/CreditRecalculateService.php | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 app/Services/Internal/Support/CreditRecalculateService.php diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php new file mode 100644 index 0000000000..2cbc5d6279 --- /dev/null +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -0,0 +1,156 @@ +. + */ + +namespace FireflyIII\Services\Internal\Support; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use Log; + +class CreditRecalculateService +{ + private TransactionGroup $group; + + /** + * + */ + public function recalculate(): void + { + if (true !== config('firefly.feature_flags.handle_debts')) { + Log::debug('handle_debts is disabled.'); + + return; + } + Log::error('TODO'); + + return; + Log::debug(sprintf('Now in %s', __METHOD__)); + /** @var TransactionJournal $journal */ + foreach ($this->group->transactionJournals as $journal) { + try { + $this->recalculateJournal($journal); + } catch (FireflyException $e) { + Log::error($e->getTraceAsString()); + Log::error('Could not recalculate'); + } + } + Log::debug(sprintf('Done with %s', __METHOD__));; + } + + /** + * @param TransactionJournal $journal + * + * @throws FireflyException + */ + private function recalculateJournal(TransactionJournal $journal): void + { + if (TransactionType::DEPOSIT !== $journal->transactionType->type) { + Log::debug('Journal is not a deposit.'); + + return; + } + $source = $this->getSourceAccount($journal); + $destination = $this->getDestinationAccount($journal); + // destination must be liability, source must be expense. + if (AccountType::REVENUE !== $source->accountType->type) { + Log::debug('Source is not a revenue account.'); + + return; + } + if (!in_array($destination->accountType->type, config('firefly.valid_liabilities'))) { + Log::debug('Destination is not a liability.'); + + return; + } + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($destination->user); + $direction = $repository->getMetaValue($destination, 'liability_direction'); + if ('credit' !== $direction) { + Log::debug(sprintf('Destination liabiltiy direction is "%s", do nothing.', $direction)); + } + /* + * This destination is a liability and an incoming debt. The amount paid into the liability changes the original debt amount. + * + */ + Log::debug('Do something!'); + } + + /** + * @param TransactionJournal $journal + * + * @return Account + * @throws FireflyException + */ + private function getSourceAccount(TransactionJournal $journal): Account + { + return $this->getAccount($journal, '<'); + } + + /** + * @param TransactionJournal $journal + * + * @return Account + * @throws FireflyException + */ + private function getDestinationAccount(TransactionJournal $journal): Account + { + return $this->getAccount($journal, '>'); + } + + /** + * @param TransactionJournal $journal + * @param string $direction + * + * @return Account + * @throws FireflyException + */ + private function getAccount(TransactionJournal $journal, string $direction): Account + { + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->where('amount', $direction, '0')->first(); + if (null === $transaction) { + throw new FireflyException(sprintf('Cannot find "%s"-transaction of journal #%d', $direction, $journal->id)); + } + $account = $transaction->account; + if (null === $account) { + throw new FireflyException(sprintf('Cannot find "%s"-account of transaction #%d of journal #%d', $direction, $transaction->id, $journal->id)); + } + + return $account; + } + + /** + * @param TransactionGroup $group + */ + public function setGroup(TransactionGroup $group): void + { + $this->group = $group; + } + + +} \ No newline at end of file From d01814821fdcf641bb7b3a7fc630c431046e739c Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 08:02:10 +0200 Subject: [PATCH 06/30] Update forms and transformer. --- app/Transformers/AccountTransformer.php | 15 +++++++++------ frontend/src/components/accounts/Create.vue | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 97cccf7953..49e5aa6623 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; + use Carbon\Carbon; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -56,10 +57,11 @@ class AccountTransformer extends AbstractTransformer $this->repository->setUser($account->user); // get account type: - $fullType = $account->accountType->type; - $accountType = (string)config(sprintf('firefly.shortNamesByFullName.%s', $fullType)); - $liabilityType = (string)config(sprintf('firefly.shortLiabilityNameByFullName.%s', $fullType)); - $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); + $fullType = $account->accountType->type; + $accountType = (string)config(sprintf('firefly.shortNamesByFullName.%s', $fullType)); + $liabilityType = (string)config(sprintf('firefly.shortLiabilityNameByFullName.%s', $fullType)); + $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); + $liabilityDirection = $this->repository->getMetaValue($account, 'liability_direction'); // get account role (will only work if the type is asset. $accountRole = $this->getAccountRole($account, $accountType); @@ -114,6 +116,7 @@ class AccountTransformer extends AbstractTransformer 'opening_balance' => $openingBalance, 'opening_balance_date' => $openingBalanceDate, 'liability_type' => $liabilityType, + 'liability_direction' => $liabilityDirection, 'interest' => (float)$interest, 'interest_period' => $interestPeriod, 'include_net_worth' => $includeNetWorth, @@ -195,7 +198,7 @@ class AccountTransformer extends AbstractTransformer $creditCardType = $this->repository->getMetaValue($account, 'cc_type'); $monthlyPaymentDate = $this->repository->getMetaValue($account, 'cc_monthly_payment_date'); } - if(null !== $monthlyPaymentDate) { + if (null !== $monthlyPaymentDate) { $monthlyPaymentDate = Carbon::createFromFormat('!Y-m-d', $monthlyPaymentDate, config('app.timezone'))->toAtomString(); } @@ -219,7 +222,7 @@ class AccountTransformer extends AbstractTransformer $openingBalance = $amount; $openingBalanceDate = $this->repository->getOpeningBalanceDate($account); } - if(null !== $openingBalanceDate) { + if (null !== $openingBalanceDate) { $openingBalanceDate = Carbon::createFromFormat('!Y-m-d', $openingBalanceDate, config('app.timezone'))->toAtomString(); } diff --git a/frontend/src/components/accounts/Create.vue b/frontend/src/components/accounts/Create.vue index afcee2ff95..fa4b72a411 100644 --- a/frontend/src/components/accounts/Create.vue +++ b/frontend/src/components/accounts/Create.vue @@ -238,6 +238,7 @@ export default { axios.post(url, submission) .then(response => { + this.errors = lodashClonedeep(this.defaultErrors); console.log('success!'); this.returnedId = parseInt(response.data.data.id); this.returnedTitle = response.data.data.attributes.name; @@ -281,6 +282,9 @@ export default { if (errors.errors.hasOwnProperty(i)) { this.errors[i] = errors.errors[i]; } + if('liability_start_date' === i) { + this.errors.opening_balance_date = errors.errors[i]; + } } }, getSubmission: function () { @@ -302,13 +306,18 @@ export default { submission.liability_type = this.liability_type.toLowerCase(); submission.interest = this.interest; submission.interest_period = this.interest_period; - submission.opening_balance = this.liability_amount; - submission.opening_balance_date = this.liability_date; + submission.liability_amount = this.liability_amount; + submission.liability_start_date = this.liability_date; + submission.liability_direction = this.liability_direction; } - if (null !== this.opening_balance && null !== this.opening_balance_date && 'asset' === this.type) { + if ((null !== this.opening_balance || null !== this.opening_balance_date) && 'asset' === this.type) { submission.opening_balance = this.opening_balance; submission.opening_balance_date = this.opening_balance_date; } + if('' === submission.opening_balance) { + delete submission.opening_balance; + } + if ('asset' === this.type && 'ccAsset' === this.account_role) { submission.credit_card_type = 'monthlyFull'; submission.monthly_payment_date = '2021-01-01'; From 7825fe4f1d1dfb5a14ce7205e7fddfc0522afc4f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 08:03:17 +0200 Subject: [PATCH 07/30] Add flag to feature branch. --- config/firefly.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/firefly.php b/config/firefly.php index 01bb9c3ed6..8f48c89606 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -94,9 +94,10 @@ return [ 'is_demo_site' => false, ], 'feature_flags' => [ - 'export' => true, - 'telemetry' => true, - 'webhooks' => false, + 'export' => true, + 'telemetry' => true, + 'webhooks' => false, + 'handle_debts' => true, ], 'version' => '5.5.6', From e07377af86b1603cdf523e11306c9207e0f96f52 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:24:20 +0200 Subject: [PATCH 08/30] Clear cache when updating account --- app/Api/V1/Controllers/Models/Account/UpdateController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Api/V1/Controllers/Models/Account/UpdateController.php b/app/Api/V1/Controllers/Models/Account/UpdateController.php index 1d53ee4cd1..e200977673 100644 --- a/app/Api/V1/Controllers/Models/Account/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Account/UpdateController.php @@ -31,6 +31,7 @@ use FireflyIII\Transformers\AccountTransformer; use Illuminate\Http\JsonResponse; use League\Fractal\Resource\Item; use Log; +use Preferences; /** * Class UpdateController @@ -75,6 +76,7 @@ class UpdateController extends Controller $account = $this->repository->update($account, $data); $manager = $this->getManager(); $account->refresh(); + Preferences::mark(); /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); From f5af0350a49afc41c605e920eb29bfba03446b5e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:24:38 +0200 Subject: [PATCH 09/30] Change rules for account requests --- .../Requests/Models/Account/StoreRequest.php | 12 ++++----- .../Requests/Models/Account/UpdateRequest.php | 25 +++++++++++++------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/Api/V1/Requests/Models/Account/StoreRequest.php b/app/Api/V1/Requests/Models/Account/StoreRequest.php index 6659baf155..e16b016a6f 100644 --- a/app/Api/V1/Requests/Models/Account/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Account/StoreRequest.php @@ -77,11 +77,11 @@ class StoreRequest extends FormRequest 'interest' => $this->string('interest'), 'interest_period' => $this->string('interest_period'), ]; - // append Location information. + // append location information. $data = $this->appendLocationData($data, null); if ('liability' === $data['account_type_name'] || 'liabilities' === $data['account_type_name']) { - $data['opening_balance'] = bcmul($this->string('liability_amount'), '-1'); + $data['opening_balance'] = app('steam')->negative($this->string('liability_amount')); $data['opening_balance_date'] = $this->date('liability_start_date'); $data['account_type_name'] = $this->string('liability_type'); $data['liability_direction'] = $this->string('liability_direction'); @@ -120,11 +120,11 @@ class StoreRequest extends FormRequest 'credit_card_type' => sprintf('in:%s|required_if:account_role,ccAsset', $ccPaymentTypes), 'monthly_payment_date' => 'date' . '|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull', 'liability_type' => 'required_if:type,liability|required_if:type,liabilities|in:loan,debt,mortgage', - 'liability_amount' => 'required_if:type,liability|required_if:type,liabilities|min:0|numeric', + 'liability_amount' => 'required_with:liability_start_date|min:0|numeric', + 'liability_start_date' => 'required_with:liability_amount|date', 'liability_direction' => 'required_if:type,liability|required_if:type,liabilities|in:credit,debit', - 'liability_start_date' => 'required_if:type,liability|required_if:type,liabilities|date', - 'interest' => 'required_if:type,liability|required_if:type,liabilities|between:0,100|numeric', - 'interest_period' => 'required_if:type,liability|required_if:type,liabilities|in:daily,monthly,yearly', + 'interest' => 'between:0,100|numeric', + 'interest_period' => sprintf('in:%s', join(',', config('firefly.interest_periods'))), 'notes' => 'min:0|max:65536', ]; diff --git a/app/Api/V1/Requests/Models/Account/UpdateRequest.php b/app/Api/V1/Requests/Models/Account/UpdateRequest.php index c53f70168a..b8bcf1fa3e 100644 --- a/app/Api/V1/Requests/Models/Account/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Account/UpdateRequest.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\Account; +use FireflyIII\Models\Account; use FireflyIII\Models\Location; use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\UniqueAccountNumber; @@ -32,6 +33,7 @@ use FireflyIII\Support\Request\AppendsLocationData; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; +use Log; /** * Class UpdateRequest @@ -68,16 +70,23 @@ class UpdateRequest extends FormRequest 'order' => ['order', 'integer'], 'currency_id' => ['currency_id', 'integer'], 'currency_code' => ['currency_code', 'string'], - 'liability_direction' => ['liability_direction', 'string'] + 'liability_direction' => ['liability_direction', 'string'], + 'liability_amount' => ['liability_amount', 'string'], + 'liability_start_date' => ['liability_start_date', 'date'], ]; - $data = $this->getAllData($fields); - $data = $this->appendLocationData($data, null); + /** @var Account $account */ + $account = $this->route()->parameter('account'); + $data = $this->getAllData($fields); + $data = $this->appendLocationData($data, null); + $valid = config('firefly.valid_liabilities'); + if (array_key_exists('liability_amount', $data) && in_array($account->accountType->type, $valid, true)) { + $data['opening_balance'] = app('steam')->negative($data['liability_amount']); + Log::debug(sprintf('Opening balance for liability is "%s".', $data['opening_balance'])); + } - if (array_key_exists('account_type_name', $data) && 'liability' === $data['account_type_name']) { - $data['opening_balance'] = bcmul($this->string('liability_amount'), '-1'); - $data['opening_balance_date'] = $this->date('liability_start_date'); - $data['account_type_name'] = $this->string('liability_type'); - $data['account_type_id'] = null; + if (array_key_exists('liability_start_date', $data) && in_array($account->accountType->type, $valid, true)) { + $data['opening_balance_date'] = $data['liability_start_date']; + Log::debug(sprintf('Opening balance date for liability is "%s".', $data['opening_balance_date'])); } return $data; From 30aea37391b92b32798d2c3f9e2b53ed5025f2b2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:24:47 +0200 Subject: [PATCH 10/30] New events for new accounts --- app/Events/StoredAccount.php | 46 +++++++++++++++++++++++++++++++++++ app/Events/UpdatedAccount.php | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 app/Events/StoredAccount.php create mode 100644 app/Events/UpdatedAccount.php diff --git a/app/Events/StoredAccount.php b/app/Events/StoredAccount.php new file mode 100644 index 0000000000..a5c948d5d3 --- /dev/null +++ b/app/Events/StoredAccount.php @@ -0,0 +1,46 @@ +. + */ + +namespace FireflyIII\Events; + +use FireflyIII\Models\Account; +use Illuminate\Queue\SerializesModels; + +/** + * Class StoredAccount + */ +class StoredAccount extends Event +{ + use SerializesModels; + + public Account $account; + + /** + * Create a new event instance. + * + * @param Account $account + */ + public function __construct(Account $account) + { + $this->account = $account; + } + +} \ No newline at end of file diff --git a/app/Events/UpdatedAccount.php b/app/Events/UpdatedAccount.php new file mode 100644 index 0000000000..1c01b8cf00 --- /dev/null +++ b/app/Events/UpdatedAccount.php @@ -0,0 +1,46 @@ +. + */ + +namespace FireflyIII\Events; + +use FireflyIII\Models\Account; +use Illuminate\Queue\SerializesModels; + +/** + * Class UpdatedAccount + */ +class UpdatedAccount extends Event +{ + use SerializesModels; + + public Account $account; + + /** + * Create a new event instance. + * + * @param Account $account + */ + public function __construct(Account $account) + { + $this->account = $account; + } + +} \ No newline at end of file From 36fa7ae97e11265e11a09138e2fe45c2cee0caa7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:25:00 +0200 Subject: [PATCH 11/30] Some new strings. --- resources/lang/en_US/firefly.php | 2 ++ resources/lang/en_US/validation.php | 1 + resources/views/v1/accounts/create.twig | 3 ++- resources/views/v1/list/groups.twig | 3 +++ resources/views/v1/transactions/show.twig | 6 ++++++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index ba9bf9e640..c305df31b8 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1131,6 +1131,7 @@ return [ 'already_cleared_transactions' => 'Already cleared transactions (:count)', 'submitted_end_balance' => 'Submitted end balance', 'initial_balance_description' => 'Initial balance for ":account"', + 'liability_credit_description' => 'Liability credit for ":account"', 'interest_calc_' => 'unknown', 'interest_calc_daily' => 'Per day', 'interest_calc_monthly' => 'Per month', @@ -1321,6 +1322,7 @@ return [ 'account_type_Credit card' => 'Credit card', 'liability_direction_credit' => 'I am owed this debt', 'liability_direction_debit' => 'I owe this debt to somebody else', + 'Liability credit' => 'Liability credit', 'budgets' => 'Budgets', 'tags' => 'Tags', 'reports' => 'Reports', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 48540ee6fe..b7e6143ff5 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -200,6 +200,7 @@ return [ 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'lc_source_need_data' => 'Need to get a valid source account ID to continue.', 'ob_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.', 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', diff --git a/resources/views/v1/accounts/create.twig b/resources/views/v1/accounts/create.twig index a74107c270..f5e4aeceb8 100644 --- a/resources/views/v1/accounts/create.twig +++ b/resources/views/v1/accounts/create.twig @@ -27,7 +27,8 @@ {% endif %} {% if objectType == 'liabilities' %} {{ ExpandedForm.select('liability_type_id', liabilityTypes) }} - {{ ExpandedForm.amountNoCurrency('opening_balance', null, {label:'debt_start_amount'|_, helpText: 'debt_start_amount_help'|_}) }} + {{ ExpandedForm.amountNoCurrency('opening_balance', null, {label:'debt_start_amount'|_}) }} + {{ ExpandedForm.select('liability_direction', liabilityDirections) }} {{ ExpandedForm.date('opening_balance_date', null, {label:'debt_start_date'|_}) }} {{ ExpandedForm.percentage('interest') }} {{ ExpandedForm.select('interest_period', interestPeriods, null, {helpText: 'interest_period_help'|_}) }} diff --git a/resources/views/v1/list/groups.twig b/resources/views/v1/list/groups.twig index 3b71fdb60d..5bef92952c 100644 --- a/resources/views/v1/list/groups.twig +++ b/resources/views/v1/list/groups.twig @@ -118,6 +118,9 @@ {% if transaction.transaction_type_type == 'Opening balance' %} {% endif %} + {% if transaction.transaction_type_type == 'Liability credit' %} + + {% endif %} diff --git a/resources/views/v1/transactions/show.twig b/resources/views/v1/transactions/show.twig index 47450983e0..e73ae146dc 100644 --- a/resources/views/v1/transactions/show.twig +++ b/resources/views/v1/transactions/show.twig @@ -39,6 +39,7 @@
  • {{ 'clone'|_ }}
  • {% endif %} + @@ -231,12 +232,17 @@ {{ journal.source_name }} → {% endif %} + {% if first.transactiontype.type == 'Withdrawal' or first.transactiontype.type == 'Deposit' %} {{ formatAmountBySymbol(journal.amount*-1, journal.currency_symbol, journal.currency_decimal_places) }} {% elseif first.transactiontype.type == 'Transfer' or first.transactiontype.type == 'Opening balance' %} {{ formatAmountBySymbol(journal.amount, journal.currency_symbol, journal.currency_decimal_places, false) }} + {% elseif first.transactiontype.type == 'Liability credit' %} + + {{ formatAmountBySymbol(journal.amount*-1, journal.currency_symbol, journal.currency_decimal_places, false) }} + {% endif %} From 41d1ef27b59f12b9f7b4f414918bb6722dd39d52 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:25:17 +0200 Subject: [PATCH 12/30] New account and transaction types --- config/firefly.php | 162 ++++++++++++--------- database/seeders/AccountTypeSeeder.php | 1 + database/seeders/TransactionTypeSeeder.php | 1 + 3 files changed, 92 insertions(+), 72 deletions(-) diff --git a/config/firefly.php b/config/firefly.php index 8f48c89606..0f388c4444 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -628,15 +628,16 @@ return [ // expected source types for each transaction type, in order of preference. 'expected_source_types' => [ 'source' => [ - TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, - AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION,], - TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE,], - TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, + AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION,], + TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE,], + TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT,], // in case no transaction type is known yet, it could be anything. - 'none' => [ + 'none' => [ AccountType::ASSET, AccountType::EXPENSE, AccountType::REVENUE, @@ -646,77 +647,82 @@ return [ ], ], 'destination' => [ - TransactionTypeModel::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE,], - TransactionTypeModel::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE,], - TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + TransactionTypeModel::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE,], + TransactionTypeModel::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE,], + TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], + TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], ], ], 'allowed_opposing_types' => [ 'source' => [ - AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, - AccountType::LOAN, AccountType::RECONCILIATION, AccountType::MORTGAGE], - AccountType::CASH => [AccountType::ASSET], - AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, - AccountType::MORTGAGE,], - AccountType::EXPENSE => [], // is not allowed as a source. - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, - AccountType::MORTGAGE,], - AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, - AccountType::MORTGAGE,], - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::REVENUE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, + AccountType::LOAN, AccountType::RECONCILIATION, AccountType::MORTGAGE], + AccountType::CASH => [AccountType::ASSET], + AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, + AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], + AccountType::EXPENSE => [], // is not allowed as a source. + AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, + AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], + AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, + AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], + AccountType::RECONCILIATION => [AccountType::ASSET], + AccountType::REVENUE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountType::LIABILITY_CREDIT => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], ], 'destination' => [ - AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, - AccountType::MORTGAGE, AccountType::RECONCILIATION, AccountType::REVENUE,], - AccountType::CASH => [AccountType::ASSET], - AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, - AccountType::REVENUE,], - AccountType::EXPENSE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, - AccountType::REVENUE,], - AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, - AccountType::REVENUE,], - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::REVENUE => [], // is not allowed as a destination + AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, + AccountType::MORTGAGE, AccountType::RECONCILIATION, AccountType::REVENUE,], + AccountType::CASH => [AccountType::ASSET], + AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, + AccountType::REVENUE,], + AccountType::EXPENSE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, + AccountType::REVENUE,], + AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, + AccountType::REVENUE,], + AccountType::RECONCILIATION => [AccountType::ASSET], + AccountType::REVENUE => [], // is not allowed as a destination + AccountType::LIABILITY_CREDIT => [],// is not allowed as a destination ], ], // depending on the account type, return the allowed transaction types: 'allowed_transaction_types' => [ 'source' => [ - AccountType::ASSET => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::RECONCILIATION,], - AccountType::EXPENSE => [], // is not allowed as a source. - AccountType::REVENUE => [TransactionTypeModel::DEPOSIT], - AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], - AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], + AccountType::ASSET => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, + TransactionTypeModel::RECONCILIATION,], + AccountType::EXPENSE => [], // is not allowed as a source. + AccountType::REVENUE => [TransactionTypeModel::DEPOSIT], + AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], + AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], + AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], + AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], + AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], + AccountType::LIABILITY_CREDIT => [TransactionTypeModel::LIABILITY_CREDIT], ], 'destination' => [ - AccountType::ASSET => [TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::RECONCILIATION,], - AccountType::EXPENSE => [TransactionTypeModel::WITHDRAWAL], - AccountType::REVENUE => [], // is not allowed as destination. - AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE,], - AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], - AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], + AccountType::ASSET => [TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, + TransactionTypeModel::RECONCILIATION,], + AccountType::EXPENSE => [TransactionTypeModel::WITHDRAWAL], + AccountType::REVENUE => [], // is not allowed as destination. + AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE,], + AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE,], + AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::DEPOSIT, TransactionTypeModel::TRANSFER, + TransactionTypeModel::OPENING_BALANCE,], + AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], + AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], + AccountType::LIABILITY_CREDIT => [], // is not allowed as a destination ], ], @@ -775,40 +781,51 @@ return [ AccountType::LOAN => TransactionTypeModel::DEPOSIT, AccountType::MORTGAGE => TransactionTypeModel::DEPOSIT, ], + AccountType::LIABILITY_CREDIT => [ + AccountType::DEBT => TransactionTypeModel::LIABILITY_CREDIT, + AccountType::LOAN => TransactionTypeModel::LIABILITY_CREDIT, + AccountType::MORTGAGE => TransactionTypeModel::LIABILITY_CREDIT, + ], + // AccountType::EXPENSE unlisted because it cant be a source ], // allowed source -> destination accounts. 'source_dests' => [ - TransactionTypeModel::WITHDRAWAL => [ + TransactionTypeModel::WITHDRAWAL => [ AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], AccountType::LOAN => [AccountType::EXPENSE, AccountType::CASH], AccountType::DEBT => [AccountType::EXPENSE, AccountType::CASH], AccountType::MORTGAGE => [AccountType::EXPENSE, AccountType::CASH], ], - TransactionTypeModel::DEPOSIT => [ + TransactionTypeModel::DEPOSIT => [ AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], AccountType::CASH => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], AccountType::LOAN => [AccountType::ASSET], AccountType::DEBT => [AccountType::ASSET], AccountType::MORTGAGE => [AccountType::ASSET], ], - TransactionTypeModel::TRANSFER => [ + TransactionTypeModel::TRANSFER => [ AccountType::ASSET => [AccountType::ASSET], AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], ], - TransactionTypeModel::OPENING_BALANCE => [ + TransactionTypeModel::OPENING_BALANCE => [ AccountType::ASSET => [AccountType::INITIAL_BALANCE], AccountType::LOAN => [AccountType::INITIAL_BALANCE], AccountType::DEBT => [AccountType::INITIAL_BALANCE], AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE], AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], ], - TransactionTypeModel::RECONCILIATION => [ + TransactionTypeModel::RECONCILIATION => [ AccountType::RECONCILIATION => [AccountType::ASSET], AccountType::ASSET => [AccountType::RECONCILIATION], ], + TransactionTypeModel::LIABILITY_CREDIT => [ + AccountType::LOAN => [AccountType::LIABILITY_CREDIT], + AccountType::DEBT => [AccountType::LIABILITY_CREDIT], + AccountType::MORTGAGE => [AccountType::LIABILITY_CREDIT], + ], ], // if you add fields to this array, dont forget to update the export routine (ExportDataGenerator). 'journal_meta_fields' => [ @@ -844,8 +861,9 @@ return [ Webhook::DELIVERY_JSON => 'DELIVERY_JSON', ], ], - 'can_have_virtual_amounts' => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], + 'can_have_virtual_amounts' => [AccountType::ASSET], + 'can_have_opening_balance' => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], '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'], + 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'], ]; diff --git a/database/seeders/AccountTypeSeeder.php b/database/seeders/AccountTypeSeeder.php index f22d029426..2356d5aa0c 100644 --- a/database/seeders/AccountTypeSeeder.php +++ b/database/seeders/AccountTypeSeeder.php @@ -46,6 +46,7 @@ class AccountTypeSeeder extends Seeder AccountType::RECONCILIATION, AccountType::DEBT, AccountType::MORTGAGE, + AccountType::LIABILITY_CREDIT ]; foreach ($types as $type) { try { diff --git a/database/seeders/TransactionTypeSeeder.php b/database/seeders/TransactionTypeSeeder.php index dc0bf3ea06..07aacd5ab0 100644 --- a/database/seeders/TransactionTypeSeeder.php +++ b/database/seeders/TransactionTypeSeeder.php @@ -40,6 +40,7 @@ class TransactionTypeSeeder extends Seeder TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION, TransactionType::INVALID, + TransactionType::LIABILITY_CREDIT ]; foreach ($types as $type) { From bff274d0587edd8387f5425d8cda8851929154f2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:25:24 +0200 Subject: [PATCH 13/30] New account and transaction types --- app/Models/AccountType.php | 2 ++ app/Models/TransactionType.php | 25 +++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index dc9f1281a6..3b6018eec3 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -74,6 +74,8 @@ class AccountType extends Model public const MORTGAGE = 'Mortgage'; /** @var string */ public const CREDITCARD = 'Credit card'; + /** @var string */ + public const LIABILITY_CREDIT = 'Liability credit account'; /** * The attributes that should be casted to native types. * diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index d48f725be6..28ac4a6911 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -34,13 +34,13 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * FireflyIII\Models\TransactionType * - * @property int $id - * @property Carbon|null $created_at - * @property Carbon|null $updated_at - * @property Carbon|null $deleted_at - * @property string $type + * @property int $id + * @property Carbon|null $created_at + * @property Carbon|null $updated_at + * @property Carbon|null $deleted_at + * @property string $type * @property-read Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals - * @property-read int|null $transaction_journals_count + * @property-read int|null $transaction_journals_count * @method static \Illuminate\Database\Eloquent\Builder|TransactionType newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|TransactionType newQuery() * @method static Builder|TransactionType onlyTrashed() @@ -57,12 +57,21 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class TransactionType extends Model { use SoftDeletes; + + /** @var string */ public const WITHDRAWAL = 'Withdrawal'; + /** @var string */ public const DEPOSIT = 'Deposit'; + /** @var string */ public const TRANSFER = 'Transfer'; + /** @var string */ public const OPENING_BALANCE = 'Opening balance'; + /** @var string */ public const RECONCILIATION = 'Reconciliation'; + /** @var string */ public const INVALID = 'Invalid'; + /** @var string */ + public const LIABILITY_CREDIT = 'Liability credit'; /** @var string[] */ protected $casts = [ @@ -70,7 +79,7 @@ class TransactionType extends Model 'updated_at' => 'datetime', 'deleted_at' => 'datetime', ]; - /** @var string[] */ + /** @var string[] */ protected $fillable = ['type']; /** @@ -78,8 +87,8 @@ class TransactionType extends Model * * @param string $type * - * @throws NotFoundHttpException * @return TransactionType + * @throws NotFoundHttpException */ public static function routeBinder(string $type): TransactionType { From 7fb4b2bb40143eb1e8097751c7547f90fb76f200 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:25:41 +0200 Subject: [PATCH 14/30] New events for new accounts --- .../Events/StoredAccountEventHandler.php | 44 ++++++++++++++++++ .../Events/UpdatedAccountEventHandler.php | 45 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 app/Handlers/Events/StoredAccountEventHandler.php create mode 100644 app/Handlers/Events/UpdatedAccountEventHandler.php diff --git a/app/Handlers/Events/StoredAccountEventHandler.php b/app/Handlers/Events/StoredAccountEventHandler.php new file mode 100644 index 0000000000..5d87364a69 --- /dev/null +++ b/app/Handlers/Events/StoredAccountEventHandler.php @@ -0,0 +1,44 @@ +. + */ + +namespace FireflyIII\Handlers\Events; + +use FireflyIII\Events\StoredAccount; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; + +/** + * Class StoredAccountEventHandler + */ +class StoredAccountEventHandler +{ + /** + * @param StoredAccount $event + */ + public function recalculateCredit(StoredAccount $event): void + { + $account = $event->account; + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setAccount($account); + $object->recalculate(); + } + +} \ No newline at end of file diff --git a/app/Handlers/Events/UpdatedAccountEventHandler.php b/app/Handlers/Events/UpdatedAccountEventHandler.php new file mode 100644 index 0000000000..674ab383a9 --- /dev/null +++ b/app/Handlers/Events/UpdatedAccountEventHandler.php @@ -0,0 +1,45 @@ +. + */ + +namespace FireflyIII\Handlers\Events; + + +use FireflyIII\Events\StoredAccount; +use FireflyIII\Events\UpdatedAccount; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; + +/** + * Class UpdatedAccountEventHandler + */ +class UpdatedAccountEventHandler +{ + /** + * @param UpdatedAccount $event + */ + public function recalculateCredit(UpdatedAccount $event): void + { + $account = $event->account; + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setAccount($account); + $object->recalculate(); + } +} \ No newline at end of file From 001c1f6518aaccd0513df7170001dc3cdaaea529 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:25:48 +0200 Subject: [PATCH 15/30] New events for new accounts --- app/Providers/EventServiceProvider.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 0436595d44..abd00598a3 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -31,7 +31,9 @@ use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedReportOnJournals; use FireflyIII\Events\RequestedSendWebhookMessages; use FireflyIII\Events\RequestedVersionCheckStatus; +use FireflyIII\Events\StoredAccount; use FireflyIII\Events\StoredTransactionGroup; +use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UserChangedEmail; use FireflyIII\Mail\OAuthTokenCreatedMail; @@ -120,6 +122,14 @@ class EventServiceProvider extends ServiceProvider RequestedSendWebhookMessages::class => [ 'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages', ], + + // account related events: + StoredAccount::class => [ + 'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit', + ], + UpdatedAccount::class => [ + 'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit', + ], ]; /** From a2957b9e809bd49d6a535c092ec9996c44f94734 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:26:00 +0200 Subject: [PATCH 16/30] Update validation --- .../Account/LiabilityValidation.php | 96 +++++++++++++++++++ app/Validation/AccountValidator.php | 12 ++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 app/Validation/Account/LiabilityValidation.php diff --git a/app/Validation/Account/LiabilityValidation.php b/app/Validation/Account/LiabilityValidation.php new file mode 100644 index 0000000000..bc29bee4a4 --- /dev/null +++ b/app/Validation/Account/LiabilityValidation.php @@ -0,0 +1,96 @@ +. + */ + +namespace FireflyIII\Validation\Account; + + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use Log; + +/** + * Trait LiabilityValidation + */ +trait LiabilityValidation +{ + + /** + * Source of an liability credit must be a liability. + * + * @param string|null $accountName + * + * @return bool + */ + protected function validateLCSource(?string $accountName): bool + { + $result = true; + Log::debug(sprintf('Now in validateLCDestination("%s")', $accountName)); + if ('' === $accountName || null === $accountName) { + $result = false; + } + if (true === $result) { + // set the source to be a (dummy) revenue account. + $account = new Account; + $accountType = AccountType::whereType(AccountType::LIABILITY_CREDIT)->first(); + $account->accountType = $accountType; + $this->source = $account; + } + + return $result; + } + + /** + * @param int|null $accountId + * + * @return bool + */ + protected function validateLCDestination(?int $accountId): bool + { + Log::debug(sprintf('Now in validateLCDestination(%d)', $accountId)); + $result = null; + $validTypes = config('firefly.valid_liabilities'); + + if (null === $accountId) { + $this->sourceError = (string)trans('validation.lc_destination_need_data'); + $result = false; + } + + Log::debug('Destination ID is not null.'); + $search = $this->accountRepository->findNull($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; + } + // 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; + } + +} \ No newline at end of file diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php index d98e4d5247..cf3ffe03de 100644 --- a/app/Validation/AccountValidator.php +++ b/app/Validation/AccountValidator.php @@ -34,6 +34,7 @@ use FireflyIII\Validation\Account\OBValidation; use FireflyIII\Validation\Account\ReconciliationValidation; use FireflyIII\Validation\Account\TransferValidation; use FireflyIII\Validation\Account\WithdrawalValidation; +use FireflyIII\Validation\Account\LiabilityValidation; use Log; /** @@ -41,7 +42,7 @@ use Log; */ class AccountValidator { - use AccountValidatorProperties, WithdrawalValidation, DepositValidation, TransferValidation, ReconciliationValidation, OBValidation; + use AccountValidatorProperties, WithdrawalValidation, DepositValidation, TransferValidation, ReconciliationValidation, OBValidation, LiabilityValidation; public bool $createMode; public string $destError; @@ -129,6 +130,9 @@ class AccountValidator case TransactionType::OPENING_BALANCE: $result = $this->validateOBDestination($accountId, $accountName); break; + case TransactionType::LIABILITY_CREDIT: + $result = $this->validateLCDestination($accountId); + break; case TransactionType::RECONCILIATION: $result = $this->validateReconciliationDestination($accountId); break; @@ -164,6 +168,10 @@ class AccountValidator case TransactionType::OPENING_BALANCE: $result = $this->validateOBSource($accountId, $accountName); break; + case TransactionType::LIABILITY_CREDIT: + $result = $this->validateLCSource($accountName); + break; + case TransactionType::RECONCILIATION: Log::debug('Calling validateReconciliationSource'); $result = $this->validateReconciliationSource($accountId); @@ -201,7 +209,7 @@ class AccountValidator */ protected function canCreateType(string $accountType): bool { - $canCreate = [AccountType::EXPENSE, AccountType::REVENUE, AccountType::INITIAL_BALANCE]; + $canCreate = [AccountType::EXPENSE, AccountType::REVENUE, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT]; if (in_array($accountType, $canCreate, true)) { return true; } From 5d7ca1ef9a4b43ada9ae89ced5c1992732a63a4d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:26:07 +0200 Subject: [PATCH 17/30] Remove logging --- app/Factory/TransactionJournalMetaFactory.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Factory/TransactionJournalMetaFactory.php b/app/Factory/TransactionJournalMetaFactory.php index feff846ff9..e109b04ad4 100644 --- a/app/Factory/TransactionJournalMetaFactory.php +++ b/app/Factory/TransactionJournalMetaFactory.php @@ -41,12 +41,12 @@ class TransactionJournalMetaFactory */ public function updateOrCreate(array $data): ?TransactionJournalMeta { - Log::debug('In updateOrCreate()'); + //Log::debug('In updateOrCreate()'); $value = $data['data']; /** @var TransactionJournalMeta $entry */ $entry = $data['journal']->transactionJournalMeta()->where('name', $data['name'])->first(); if (null === $value && null !== $entry) { - Log::debug('Value is empty, delete meta value.'); + //Log::debug('Value is empty, delete meta value.'); try { $entry->delete(); } catch (Exception $e) { // @phpstan-ignore-line @@ -57,11 +57,11 @@ class TransactionJournalMetaFactory } if ($data['data'] instanceof Carbon) { - Log::debug('Is a carbon object.'); + //Log::debug('Is a carbon object.'); $value = $data['data']->toW3cString(); } if ('' === (string)$value) { - Log::debug('Is an empty string.'); + //Log::debug('Is an empty string.'); // don't store blank strings. if (null !== $entry) { Log::debug('Will not store empty strings, delete meta value'); @@ -76,7 +76,7 @@ class TransactionJournalMetaFactory } if (null === $entry) { - Log::debug('Will create new object.'); + //Log::debug('Will create new object.'); Log::debug(sprintf('Going to create new meta-data entry to store "%s".', $data['name'])); $entry = new TransactionJournalMeta(); $entry->transactionJournal()->associate($data['journal']); From 0426fa63d077aea05ce82c3e13fcec0729c63ba7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:26:36 +0200 Subject: [PATCH 18/30] Make sure the user can create liabilities in the "credit" direction with the right transactions. --- app/Factory/AccountFactory.php | 68 ++- .../Controllers/Account/CreateController.php | 27 +- app/Http/Requests/AccountFormRequest.php | 2 + .../Account/AccountRepository.php | 19 +- .../Account/AccountRepositoryInterface.php | 7 + .../Internal/Support/AccountServiceTrait.php | 428 +++++++++++++++--- .../Support/CreditRecalculateService.php | 176 +++++-- .../Internal/Update/AccountUpdateService.php | 56 ++- .../Http/Controllers/UserNavigation.php | 2 +- 9 files changed, 636 insertions(+), 149 deletions(-) diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index 40336ccaa4..0a976bd8d3 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Factory; +use FireflyIII\Events\StoredAccount; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -48,6 +49,7 @@ class AccountFactory protected array $validCCFields; protected array $validFields; private array $canHaveVirtual; + private array $canHaveOpeningBalance; private User $user; /** @@ -57,11 +59,12 @@ class AccountFactory */ public function __construct() { - $this->accountRepository = app(AccountRepositoryInterface::class); - $this->canHaveVirtual = config('firefly.can_have_virtual_amounts'); - $this->validAssetFields = config('firefly.valid_asset_fields'); - $this->validCCFields = config('firefly.valid_cc_fields'); - $this->validFields = config('firefly.valid_account_fields'); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->canHaveVirtual = config('firefly.can_have_virtual_amounts'); + $this->canHaveOpeningBalance = config('firefly.can_have_opening_balance'); + $this->validAssetFields = config('firefly.valid_asset_fields'); + $this->validCCFields = config('firefly.valid_cc_fields'); + $this->validFields = config('firefly.valid_account_fields'); } /** @@ -113,10 +116,14 @@ class AccountFactory // account may exist already: $return = $this->find($data['name'], $type->type); - if (null === $return) { - $return = $this->createAccount($type, $data); + if (null !== $return) { + return $return; } + $return = $this->createAccount($type, $data); + + event(new StoredAccount($return)); + return $return; } @@ -203,7 +210,18 @@ class AccountFactory $this->storeMetaData($account, $data); // create opening balance - $this->storeOpeningBalance($account, $data); + try { + $this->storeOpeningBalance($account, $data); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } + + // create credit liability data (if relevant) + try { + $this->storeCreditLiability($account, $data); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } // create notes $notes = array_key_exists('notes', $data) ? $data['notes'] : ''; @@ -242,7 +260,6 @@ class AccountFactory if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) { $data['liability_direction'] = null; } - $data['account_role'] = $accountRole; $data['currency_id'] = $currency->id; @@ -287,15 +304,18 @@ class AccountFactory /** * @param Account $account * @param array $data + * + * @throws FireflyException */ private function storeOpeningBalance(Account $account, array $data) { $accountType = $account->accountType->type; - // if it can have a virtual balance, it can also have an opening balance. - if (in_array($accountType, $this->canHaveVirtual, true)) { + if (in_array($accountType, $this->canHaveOpeningBalance, true)) { if ($this->validOBData($data)) { - $this->updateOBGroup($account, $data); + $openingBalance = $data['opening_balance']; + $openingBalanceDate = $data['opening_balance_date']; + $this->updateOBGroupV2($account, $openingBalance, $openingBalanceDate); } if (!$this->validOBData($data)) { $this->deleteOBGroup($account); @@ -303,6 +323,30 @@ class AccountFactory } } + /** + * @param Account $account + * @param array $data + * + * @throws FireflyException + */ + private function storeCreditLiability(Account $account, array $data) + { + $account->refresh(); + $accountType = $account->accountType->type; + $direction = $this->accountRepository->getMetaValue($account, 'liability_direction'); + $valid = config('firefly.valid_liabilities'); + if (in_array($accountType, $valid, true) && 'credit' === $direction) { + if ($this->validOBData($data)) { + $openingBalance = $data['opening_balance']; + $openingBalanceDate = $data['opening_balance_date']; + $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + } + if (!$this->validOBData($data)) { + $this->deleteCreditTransaction($account); + } + } + } + /** * @param Account $account * @param array $data diff --git a/app/Http/Controllers/Account/CreateController.php b/app/Http/Controllers/Account/CreateController.php index 392e03cb2b..a01febc004 100644 --- a/app/Http/Controllers/Account/CreateController.php +++ b/app/Http/Controllers/Account/CreateController.php @@ -80,14 +80,14 @@ class CreateController extends Controller */ public function create(Request $request, string $objectType = null) { - $objectType = $objectType ?? 'asset'; - $defaultCurrency = app('amount')->getDefaultCurrency(); - $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); - $subTitle = (string)trans(sprintf('firefly.make_new_%s_account', $objectType)); - $roles = $this->getRoles(); - $liabilityTypes = $this->getLiabilityTypes(); - $hasOldInput = null !== $request->old('_token'); - $locations = [ + $objectType = $objectType ?? 'asset'; + $defaultCurrency = app('amount')->getDefaultCurrency(); + $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); + $subTitle = (string)trans(sprintf('firefly.make_new_%s_account', $objectType)); + $roles = $this->getRoles(); + $liabilityTypes = $this->getLiabilityTypes(); + $hasOldInput = null !== $request->old('_token'); + $locations = [ 'location' => [ 'latitude' => $hasOldInput ? old('location_latitude') : config('firefly.default_location.latitude'), 'longitude' => $hasOldInput ? old('location_longitude') : config('firefly.default_location.longitude'), @@ -95,6 +95,10 @@ class CreateController extends Controller 'has_location' => $hasOldInput ? 'true' === old('location_has_location') : false, ], ]; + $liabilityDirections = [ + 'debit' => trans('firefly.liability_direction_debit'), + 'credit' => trans('firefly.liability_direction_credit'), + ]; // interest calculation periods: $interestPeriods = [ @@ -119,7 +123,10 @@ class CreateController extends Controller $request->session()->forget('accounts.create.fromStore'); Log::channel('audit')->info('Creating new account.'); - return prefixView('accounts.create', compact('subTitleIcon', 'locations', 'objectType', 'interestPeriods', 'subTitle', 'roles', 'liabilityTypes')); + return prefixView( + 'accounts.create', + compact('subTitleIcon', 'liabilityDirections', 'locations', 'objectType', 'interestPeriods', 'subTitle', 'roles', 'liabilityTypes') + ); } /** @@ -156,7 +163,7 @@ class CreateController extends Controller } if (count($this->attachments->getMessages()->get('attachments')) > 0) { - $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); + $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); } // redirect to previous URL. diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index ff6bc71ed6..86370a1440 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -62,6 +62,7 @@ class AccountFormRequest extends FormRequest 'interest' => $this->string('interest'), 'interest_period' => $this->string('interest_period'), 'include_net_worth' => '1', + 'liability_direction' => $this->string('liability_direction'), ]; $data = $this->appendLocationData($data, 'location'); @@ -74,6 +75,7 @@ class AccountFormRequest extends FormRequest if ('liabilities' === $data['account_type_name']) { $data['account_type_name'] = null; $data['account_type_id'] = $this->integer('liability_type_id'); + $data['opening_balance'] = app('steam')->negative($data['opening_balance']); } return $data; diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 06ed6e8dd6..2717239829 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -125,7 +125,7 @@ class AccountRepository implements AccountRepositoryInterface { $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban'); - if (0!==count($types)) { + if (0 !== count($types)) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->whereIn('account_types.type', $types); } @@ -747,4 +747,21 @@ class AccountRepository implements AccountRepositoryInterface return $service->update($account, $data); } + + /** + * @inheritDoc + */ + public function getCreditTransactionGroup(Account $account): ?TransactionGroup + { + $journal = TransactionJournal + ::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->transactionTypes([TransactionType::LIABILITY_CREDIT]) + ->first(['transaction_journals.*']); + if (null === $journal) { + return null; + } + + return $journal->transactionGroup; + } } diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 99385c33e4..79172efbf0 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -198,6 +198,13 @@ interface AccountRepositoryInterface */ public function getOpeningBalanceDate(Account $account): ?string; + /** + * @param Account $account + * + * @return TransactionGroup|null + */ + public function getCreditTransactionGroup(Account $account): ?TransactionGroup; + /** * @param Account $account * diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php index 5926f6fcf7..2b35d1713a 100644 --- a/app/Services/Internal/Support/AccountServiceTrait.php +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Services\Internal\Support; +use Carbon\Carbon; use Exception; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\AccountMetaFactory; use FireflyIII\Factory\TransactionCurrencyFactory; @@ -37,6 +39,7 @@ use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Services\Internal\Destroy\TransactionGroupDestroyService; +use JsonException; use Log; use Validator; @@ -126,7 +129,7 @@ trait AccountServiceTrait } if ($account->accountType->type === AccountType::ASSET && array_key_exists('account_role', $data) && 'ccAsset' === $data['account_role']) { - $fields = $this->validCCFields; + $fields = $this->validCCFields; } /** @var AccountMetaFactory $factory */ $factory = app(AccountMetaFactory::class); @@ -137,10 +140,10 @@ trait AccountServiceTrait // convert boolean value: if (is_bool($data[$field]) && false === $data[$field]) { - $data[$field] = 0; + $data[$field] = 0; } if (is_bool($data[$field]) && true === $data[$field]) { - $data[$field] = 1; + $data[$field] = 1; } $factory->crud($account, $field, (string)$data[$field]); @@ -191,9 +194,10 @@ trait AccountServiceTrait { $data['opening_balance'] = (string)($data['opening_balance'] ?? ''); if ('' !== $data['opening_balance'] && 0 === bccomp($data['opening_balance'], '0')) { - $data['opening_balance'] = ''; + $data['opening_balance'] = ''; } - if ('' !== $data['opening_balance'] && array_key_exists('opening_balance_date', $data) && '' !== $data['opening_balance_date']) { + if ('' !== $data['opening_balance'] && array_key_exists('opening_balance_date', $data) && '' !== $data['opening_balance_date'] + && $data['opening_balance_date'] instanceof Carbon) { Log::debug('Array has valid opening balance data.'); return true; @@ -234,11 +238,26 @@ trait AccountServiceTrait return $this->accountRepository->getOpeningBalanceGroup($account); } + /** + * Returns the credit transaction group, or NULL if it does not exist. + * + * @param Account $account + * + * @return TransactionGroup|null + */ + protected function getCreditTransaction(Account $account): ?TransactionGroup + { + Log::debug(sprintf('Now at %s', __METHOD__)); + + return $this->accountRepository->getCreditTransactionGroup($account); + } + /** * @param int $currencyId * @param string $currencyCode * * @return TransactionCurrency + * @throws JsonException */ protected function getCurrency(int $currencyId, string $currencyCode): TransactionCurrency { @@ -259,59 +278,57 @@ trait AccountServiceTrait } /** - * Update or create the opening balance group. Assumes valid data in $data. - * - * Returns null if this fails. + * Update or create the opening balance group. + * Since opening balance and date can still be empty strings, it may fail. * * @param Account $account - * @param array $data + * @param string $openingBalance + * @param Carbon $openingBalanceDate * - * @return TransactionGroup|null + * @return TransactionGroup + * @throws FireflyException */ - protected function updateOBGroup(Account $account, array $data): ?TransactionGroup + protected function updateOBGroupV2(Account $account, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup { + Log::debug(sprintf('Now in %s', __METHOD__)); + // create if not exists: $obGroup = $this->getOBGroup($account); if (null === $obGroup) { - return $this->createOBGroup($account, $data); + return $this->createOBGroupV2($account, $openingBalance, $openingBalanceDate); } - // $data['currency_id'] is empty so creating a new journal may break. - if (!array_key_exists('currency_id', $data)) { - $currency = $this->accountRepository->getAccountCurrency($account); - if (null === $currency) { - $currency = app('default')->getDefaultCurrencyByUser($account->user); - } - $data['currency_id'] = $currency->id; + // if exists, update: + $currency = $this->accountRepository->getAccountCurrency($account); + if (null === $currency) { + $currency = app('default')->getDefaultCurrencyByUser($account->user); } - /** @var TransactionJournal $journal */ - $journal = $obGroup->transactionJournals()->first(); - $journal->date = $data['opening_balance_date'] ?? $journal->date; - $journal->transaction_currency_id = $data['currency_id']; + // simply grab the first journal and change it: + $journal = $this->getObJournal($obGroup); + $obTransaction = $this->getOBTransaction($journal, $account); + $accountTransaction = $this->getNotOBTransaction($journal, $account); + $journal->date = $openingBalanceDate; + $journal->transactionCurrency()->associate($currency); - /** @var Transaction $obTransaction */ - $obTransaction = $journal->transactions()->where('account_id', '!=', $account->id)->first(); - /** @var Transaction $accountTransaction */ - $accountTransaction = $journal->transactions()->where('account_id', $account->id)->first(); // if amount is negative: - if (1 === bccomp('0', $data['opening_balance'])) { + if (1 === bccomp('0', $openingBalance)) { // account transaction loses money: - $accountTransaction->amount = app('steam')->negative($data['opening_balance']); - $accountTransaction->transaction_currency_id = $data['currency_id']; + $accountTransaction->amount = app('steam')->negative($openingBalance); + $accountTransaction->transaction_currency_id = $currency->id; // OB account transaction gains money - $obTransaction->amount = app('steam')->positive($data['opening_balance']); - $obTransaction->transaction_currency_id = $data['currency_id']; + $obTransaction->amount = app('steam')->positive($openingBalance); + $obTransaction->transaction_currency_id = $currency->id; } - if (-1 === bccomp('0', $data['opening_balance'])) { + if (-1 === bccomp('0', $openingBalance)) { // account gains money: - $accountTransaction->amount = app('steam')->positive($data['opening_balance']); - $accountTransaction->transaction_currency_id = $data['currency_id']; + $accountTransaction->amount = app('steam')->positive($openingBalance); + $accountTransaction->transaction_currency_id = $currency->id; // OB account loses money: - $obTransaction->amount = app('steam')->negative($data['opening_balance']); - $obTransaction->transaction_currency_id = $data['currency_id']; + $obTransaction->amount = app('steam')->negative($openingBalance); + $obTransaction->transaction_currency_id = $currency->id; } // save both $accountTransaction->save(); @@ -323,12 +340,105 @@ trait AccountServiceTrait } /** - * @param Account $account - * @param array $data + * Create the opposing "credit liability" transaction for credit liabilities. * - * @return TransactionGroup|null + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate + * + * @return TransactionGroup + * @throws FireflyException */ - protected function createOBGroup(Account $account, array $data): ?TransactionGroup + protected function updateCreditTransaction(Account $account, 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.'); + } + + // create if not exists: + $clGroup = $this->getCreditTransaction($account); + if (null === $clGroup) { + return $this->createCreditTransaction($account, $openingBalance, $openingBalanceDate); + } + // if exists, update: + $currency = $this->accountRepository->getAccountCurrency($account); + if (null === $currency) { + $currency = app('default')->getDefaultCurrencyByUser($account->user); + } + + // simply grab the first journal and change it: + $journal = $this->getObJournal($clGroup); + $clTransaction = $this->getOBTransaction($journal, $account); + $accountTransaction = $this->getNotOBTransaction($journal, $account); + $journal->date = $openingBalanceDate; + $journal->transactionCurrency()->associate($currency); + + // account always gains money: + $accountTransaction->amount = app('steam')->positive($openingBalance); + $accountTransaction->transaction_currency_id = $currency->id; + + // CL account always loses money: + $clTransaction->amount = app('steam')->negative($openingBalance); + $clTransaction->transaction_currency_id = $currency->id; + // save both + $accountTransaction->save(); + $clTransaction->save(); + $journal->save(); + $clGroup->refresh(); + + return $clGroup; + } + + /** + * TODO rename to "getOpposingTransaction" + * + * @param TransactionJournal $journal + * @param Account $account + * + * @return Transaction + * @throws FireflyException + */ + private function getOBTransaction(TransactionJournal $journal, Account $account): Transaction + { + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->where('account_id', '!=', $account->id)->first(); + if (null === $transaction) { + throw new FireflyException(sprintf('Could not get OB transaction for journal #%d', $journal->id)); + } + + return $transaction; + } + + /** + * @param TransactionJournal $journal + * @param Account $account + * + * @return Transaction + * @throws FireflyException + */ + private function getNotOBTransaction(TransactionJournal $journal, Account $account): Transaction + { + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->where('account_id', $account->id)->first(); + if (null === $transaction) { + throw new FireflyException(sprintf('Could not get non-OB transaction for journal #%d', $journal->id)); + } + + return $transaction; + } + + /** + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate + * + * @return TransactionGroup + * @throws FireflyException + */ + protected function createOBGroupV2(Account $account, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup { Log::debug('Now going to create an OB group.'); $language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data; @@ -336,48 +446,48 @@ trait AccountServiceTrait $sourceName = null; $destId = null; $destName = null; - $amount = $data['opening_balance']; - if (1 === bccomp($amount, '0')) { - Log::debug(sprintf('Amount is %s, which is positive. Source is a new IB account, destination is #%d', $amount, $account->id)); - // amount is positive. + + // amount is positive. + if (1 === bccomp($openingBalance, '0')) { + Log::debug(sprintf('Amount is %s, which is positive. Source is a new IB account, destination is #%d', $openingBalance, $account->id)); $sourceName = trans('firefly.initial_balance_description', ['account' => $account->name], $language); $destId = $account->id; } - if (-1 === bccomp($amount, '0')) { - Log::debug(sprintf('Amount is %s, which is negative. Destination is a new IB account, source is #%d', $amount, $account->id)); - // amount is not positive + // amount is not positive + if (-1 === bccomp($openingBalance, '0')) { + Log::debug(sprintf('Amount is %s, which is negative. Destination is a new IB account, source is #%d', $openingBalance, $account->id)); $destName = trans('firefly.initial_balance_account', ['account' => $account->name], $language); $sourceId = $account->id; } - if (0 === bccomp($amount, '0')) { - + // amount is 0 + if (0 === bccomp($openingBalance, '0')) { Log::debug('Amount is zero, so will not make an OB group.'); - - return null; - - } - $amount = app('steam')->positive($amount); - if (!array_key_exists('currency_id', $data)) { - $currency = $this->accountRepository->getAccountCurrency($account); - if (null === $currency) { - $currency = app('default')->getDefaultCurrencyByUser($account->user); - } - $data['currency_id'] = $currency->id; + throw new FireflyException('Amount for new opening balance was unexpectedly 0.'); } + // make amount positive, regardless: + $amount = app('steam')->positive($openingBalance); + + // get or grab currency: + $currency = $this->accountRepository->getAccountCurrency($account); + if (null === $currency) { + $currency = app('default')->getDefaultCurrencyByUser($account->user); + } + + // submit to factory: $submission = [ 'group_title' => null, 'user' => $account->user_id, 'transactions' => [ [ 'type' => 'Opening balance', - 'date' => $data['opening_balance_date'], + 'date' => $openingBalanceDate, 'source_id' => $sourceId, 'source_name' => $sourceName, 'destination_id' => $destId, 'destination_name' => $destName, 'user' => $account->user_id, - 'currency_id' => $data['currency_id'], + 'currency_id' => $currency->id, 'order' => 0, 'amount' => $amount, 'foreign_amount' => null, @@ -395,18 +505,204 @@ trait AccountServiceTrait ], ]; Log::debug('Going for submission', $submission); - $group = null; + /** @var TransactionGroupFactory $factory */ $factory = app(TransactionGroupFactory::class); $factory->setUser($account->user); try { $group = $factory->create($submission); - - } catch (FireflyException $e) { + } catch (DuplicateTransactionException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage(), 0, $e); } + return $group; } + + /** + * @param Account $account + * @param string $openingBalance + * @param Carbon $openingBalanceDate + * + * @return TransactionGroup + * @throws FireflyException + */ + protected function createCreditTransaction(Account $account, string $openingBalance, Carbon $openingBalanceDate): TransactionGroup + { + Log::debug('Now going to create an createCreditTransaction.'); + + if (0 === bccomp($openingBalance, '0')) { + Log::debug('Amount is zero, so will not make an liability credit group.'); + throw new FireflyException('Amount for new liability credit was unexpectedly 0.'); + } + + $language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data; + $amount = app('steam')->positive($openingBalance); + + // get or grab currency: + $currency = $this->accountRepository->getAccountCurrency($account); + if (null === $currency) { + $currency = app('default')->getDefaultCurrencyByUser($account->user); + } + + // submit to factory: + $submission = [ + 'group_title' => null, + 'user' => $account->user_id, + 'transactions' => [ + [ + '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, + 'user' => $account->user_id, + 'currency_id' => $currency->id, + 'order' => 0, + 'amount' => $amount, + 'foreign_amount' => null, + 'description' => trans('firefly.liability_credit_description', ['account' => $account->name]), + 'budget_id' => null, + 'budget_name' => null, + 'category_id' => null, + 'category_name' => null, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'reconciled' => false, + 'notes' => null, + 'tags' => [], + ], + ], + ]; + Log::debug('Going for submission', $submission); + + /** @var TransactionGroupFactory $factory */ + $factory = app(TransactionGroupFactory::class); + $factory->setUser($account->user); + + try { + $group = $factory->create($submission); + } catch (DuplicateTransactionException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage(), 0, $e); + } + + return $group; + } + + + /** + * @param Account $account + * @param array $data + * + * @return TransactionGroup + * @throws FireflyException + * @deprecated + */ + protected function createOBGroup(Account $account, array $data): TransactionGroup + { + Log::debug('Now going to create an OB group.'); + $language = app('preferences')->getForUser($account->user, 'language', 'en_US')->data; + $sourceId = null; + $sourceName = null; + $destId = null; + $destName = null; + $amount = array_key_exists('opening_balance', $data) ? $data['opening_balance'] : '0'; + + // amount is positive. + if (1 === bccomp($amount, '0')) { + Log::debug(sprintf('Amount is %s, which is positive. Source is a new IB account, destination is #%d', $amount, $account->id)); + $sourceName = trans('firefly.initial_balance_description', ['account' => $account->name], $language); + $destId = $account->id; + } + // amount is not positive + if (-1 === bccomp($amount, '0')) { + Log::debug(sprintf('Amount is %s, which is negative. Destination is a new IB account, source is #%d', $amount, $account->id)); + $destName = trans('firefly.initial_balance_account', ['account' => $account->name], $language); + $sourceId = $account->id; + } + // amount is 0 + if (0 === bccomp($amount, '0')) { + Log::debug('Amount is zero, so will not make an OB group.'); + throw new FireflyException('Amount for new opening balance was unexpectedly 0.'); + } + + // make amount positive, regardless: + $amount = app('steam')->positive($amount); + + // get or grab currency: + $currency = $this->accountRepository->getAccountCurrency($account); + if (null === $currency) { + $currency = app('default')->getDefaultCurrencyByUser($account->user); + } + + // submit to factory: + $submission = [ + 'group_title' => null, + 'user' => $account->user_id, + 'transactions' => [ + [ + 'type' => 'Opening balance', + 'date' => $data['opening_balance_date'], + 'source_id' => $sourceId, + 'source_name' => $sourceName, + 'destination_id' => $destId, + 'destination_name' => $destName, + 'user' => $account->user_id, + 'currency_id' => $currency->id, + 'order' => 0, + 'amount' => $amount, + 'foreign_amount' => null, + 'description' => trans('firefly.initial_balance_description', ['account' => $account->name]), + 'budget_id' => null, + 'budget_name' => null, + 'category_id' => null, + 'category_name' => null, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'reconciled' => false, + 'notes' => null, + 'tags' => [], + ], + ], + ]; + Log::debug('Going for submission', $submission); + + /** @var TransactionGroupFactory $factory */ + $factory = app(TransactionGroupFactory::class); + $factory->setUser($account->user); + + try { + $group = $factory->create($submission); + } catch (DuplicateTransactionException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage(), 0, $e); + } + + return $group; + } + + /** + * TODO Refactor to "getFirstJournal" + * + * @param TransactionGroup $group + * + * @return TransactionJournal + * @throws FireflyException + */ + private function getObJournal(TransactionGroup $group): TransactionJournal + { + /** @var TransactionJournal $journal */ + $journal = $group->transactionJournals()->first(); + if (null === $journal) { + throw new FireflyException(sprintf('Group #%d has no OB journal', $group->id)); + } + + return $journal; + } } diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index 2cbc5d6279..2f925730d0 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -23,43 +23,115 @@ namespace FireflyIII\Services\Internal\Support; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AccountMetaFactory; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Log; class CreditRecalculateService { - private TransactionGroup $group; + private ?Account $account; + private ?TransactionGroup $group; + private array $work; + private AccountRepositoryInterface $repository; + + /** + * CreditRecalculateService constructor. + */ + public function __construct() + { + $this->group = null; + $this->account = null; + $this->work = []; + } /** * */ public function recalculate(): void { + Log::debug(sprintf('Now in %s', __METHOD__)); if (true !== config('firefly.feature_flags.handle_debts')) { Log::debug('handle_debts is disabled.'); return; } - Log::error('TODO'); + if (null !== $this->group && null === $this->account) { + $this->processGroup(); + } + if (null !== $this->account && null === $this->group) { + // work based on account. + $this->processAccount(); + } + if (0 === count($this->work)) { + Log::debug('No work accounts, do not do CreditRecalculationService'); - return; + return; + } + $this->processWork(); + + + Log::debug('Will now do CreditRecalculationService'); + // do something + } + + /** + * + */ + private function processWork(): void + { + $this->repository = app(AccountRepositoryInterface::class); + Log::debug(sprintf('Now in %s', __METHOD__)); + foreach ($this->work as $account) { + $this->processWorkAccount($account); + } + Log::debug(sprintf('Done with %s', __METHOD__)); + } + + /** + * @param Account $account + */ + private function processWorkAccount(Account $account): void + { + Log::debug(sprintf('Now in %s(#%d)', __METHOD__, $account->id)); + + // get opening balance (if present) + $this->repository->setUser($account->user); + $startOfDebt = $this->repository->getOpeningBalanceAmount($account) ?? '0'; + + /** @var AccountMetaFactory $factory */ + $factory = app(AccountMetaFactory::class); + $factory->crud($account, 'start_of_debt', $startOfDebt); + $factory->crud($account, 'current_debt', $startOfDebt); + + // update meta data: + + + Log::debug(sprintf('Done with %s(#%d)', __METHOD__, $account->id)); + } + + + /** + * + */ + private function processGroup(): void + { Log::debug(sprintf('Now in %s', __METHOD__)); /** @var TransactionJournal $journal */ foreach ($this->group->transactionJournals as $journal) { - try { - $this->recalculateJournal($journal); - } catch (FireflyException $e) { - Log::error($e->getTraceAsString()); - Log::error('Could not recalculate'); + if (0 === count($this->work)) { + try { + $this->findByJournal($journal); + } catch (FireflyException $e) { + Log::error($e->getTraceAsString()); + Log::error(sprintf('Could not find work account for transaction group #%d.', $this->group->id)); + } } } - Log::debug(sprintf('Done with %s', __METHOD__));; + Log::debug(sprintf('Done with %s', __METHOD__)); } /** @@ -67,37 +139,22 @@ class CreditRecalculateService * * @throws FireflyException */ - private function recalculateJournal(TransactionJournal $journal): void + private function findByJournal(TransactionJournal $journal): void { - if (TransactionType::DEPOSIT !== $journal->transactionType->type) { - Log::debug('Journal is not a deposit.'); - - return; - } + Log::debug(sprintf('Now in %s', __METHOD__)); $source = $this->getSourceAccount($journal); $destination = $this->getDestinationAccount($journal); - // destination must be liability, source must be expense. - if (AccountType::REVENUE !== $source->accountType->type) { - Log::debug('Source is not a revenue account.'); - return; + // destination or source must be liability. + $valid = config('firefly.valid_liabilities'); + if (in_array($destination->accountType->type, $valid)) { + Log::debug(sprintf('Dest account type is "%s", include it.', $destination->accountType->type)); + $this->work[] = $destination; } - if (!in_array($destination->accountType->type, config('firefly.valid_liabilities'))) { - Log::debug('Destination is not a liability.'); - - return; + if (in_array($source->accountType->type, $valid)) { + Log::debug(sprintf('Src account type is "%s", include it.', $source->accountType->type)); + $this->work[] = $source; } - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($destination->user); - $direction = $repository->getMetaValue($destination, 'liability_direction'); - if ('credit' !== $direction) { - Log::debug(sprintf('Destination liabiltiy direction is "%s", do nothing.', $direction)); - } - /* - * This destination is a liability and an incoming debt. The amount paid into the liability changes the original debt amount. - * - */ - Log::debug('Do something!'); } /** @@ -108,18 +165,7 @@ class CreditRecalculateService */ private function getSourceAccount(TransactionJournal $journal): Account { - return $this->getAccount($journal, '<'); - } - - /** - * @param TransactionJournal $journal - * - * @return Account - * @throws FireflyException - */ - private function getDestinationAccount(TransactionJournal $journal): Account - { - return $this->getAccount($journal, '>'); + return $this->getAccountByDirection($journal, '<'); } /** @@ -129,7 +175,7 @@ class CreditRecalculateService * @return Account * @throws FireflyException */ - private function getAccount(TransactionJournal $journal, string $direction): Account + private function getAccountByDirection(TransactionJournal $journal, string $direction): Account { /** @var Transaction $transaction */ $transaction = $journal->transactions()->where('amount', $direction, '0')->first(); @@ -144,6 +190,38 @@ class CreditRecalculateService return $account; } + /** + * @param TransactionJournal $journal + * + * @return Account + * @throws FireflyException + */ + private function getDestinationAccount(TransactionJournal $journal): Account + { + return $this->getAccountByDirection($journal, '>'); + } + + /** + * + */ + private function processAccount(): void + { + Log::debug(sprintf('Now in %s', __METHOD__)); + $valid = config('firefly.valid_liabilities'); + if (in_array($this->account->accountType->type, $valid)) { + Log::debug(sprintf('Account type is "%s", include it.', $this->account->accountType->type)); + $this->work[] = $this->account; + } + } + + /** + * @param Account|null $account + */ + public function setAccount(?Account $account): void + { + $this->account = $account; + } + /** * @param TransactionGroup $group */ diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php index 09ca4f9833..e3b293899a 100644 --- a/app/Services/Internal/Update/AccountUpdateService.php +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Services\Internal\Update; +use FireflyIII\Events\UpdatedAccount; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Location; @@ -44,6 +46,7 @@ class AccountUpdateService protected array $validCCFields; protected array $validFields; private array $canHaveVirtual; + private array $canHaveOpeningBalance; private User $user; /** @@ -51,11 +54,12 @@ class AccountUpdateService */ public function __construct() { - $this->canHaveVirtual = config('firefly.can_have_virtual_amounts'); - $this->validAssetFields = config('firefly.valid_asset_fields'); - $this->validCCFields = config('firefly.valid_cc_fields'); - $this->validFields = config('firefly.valid_account_fields'); - $this->accountRepository = app(AccountRepositoryInterface::class); + $this->canHaveVirtual = config('firefly.can_have_virtual_amounts'); + $this->canHaveOpeningBalance = config('firefly.can_have_opening_balance'); + $this->validAssetFields = config('firefly.valid_asset_fields'); + $this->validCCFields = config('firefly.valid_cc_fields'); + $this->validFields = config('firefly.valid_account_fields'); + $this->accountRepository = app(AccountRepositoryInterface::class); } /** @@ -98,6 +102,9 @@ class AccountUpdateService // update opening balance. $this->updateOpeningBalance($account, $data); + // update opening balance. + $this->updateCreditLiability($account, $data); + // update note: if (array_key_exists('notes', $data) && null !== $data['notes']) { $this->updateNote($account, (string)$data['notes']); @@ -106,6 +113,8 @@ class AccountUpdateService // update preferences if inactive: $this->updatePreferences($account); + event(new UpdatedAccount($account)); + return $account; } @@ -266,19 +275,21 @@ class AccountUpdateService /** * @param Account $account * @param array $data + * + * @throws FireflyException */ private function updateOpeningBalance(Account $account, array $data): void { // has valid initial balance (IB) data? $type = $account->accountType; - // if it can have a virtual balance, it can also have an opening balance. - - if (in_array($type->type, $this->canHaveVirtual, true)) { - + if (in_array($type->type, $this->canHaveOpeningBalance, true)) { // check if is submitted as empty, that makes it valid: if ($this->validOBData($data) && !$this->isEmptyOBData($data)) { - $this->updateOBGroup($account, $data); + $openingBalance = $data['opening_balance']; + $openingBalanceDate = $data['opening_balance_date']; + + $this->updateOBGroupV2($account, $openingBalance, $openingBalanceDate); } if (!$this->validOBData($data) && $this->isEmptyOBData($data)) { @@ -287,6 +298,31 @@ class AccountUpdateService } } + /** + * @param Account $account + * @param array $data + * + * @throws FireflyException + */ + private function updateCreditLiability(Account $account, array $data): void + { + $type = $account->accountType; + $valid = config('firefly.valid_liabilities'); + if (in_array($type->type, $valid, true)) { + // check if is submitted as empty, that makes it valid: + if ($this->validOBData($data) && !$this->isEmptyOBData($data)) { + $openingBalance = $data['opening_balance']; + $openingBalanceDate = $data['opening_balance_date']; + + $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + } + + if (!$this->validOBData($data) && $this->isEmptyOBData($data)) { + $this->deleteCreditTransaction($account); + } + } + } + /** * @param Account $account */ diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php index 455e1b2c54..c0696bdace 100644 --- a/app/Support/Http/Controllers/UserNavigation.php +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -110,7 +110,7 @@ trait UserNavigation final protected function redirectAccountToAccount(Account $account) { $type = $account->accountType->type; - if (AccountType::RECONCILIATION === $type || AccountType::INITIAL_BALANCE === $type) { + if (AccountType::RECONCILIATION === $type || AccountType::INITIAL_BALANCE === $type || AccountType::LIABILITY_CREDIT === $type) { // reconciliation must be stored somewhere in this account's transactions. /** @var Transaction|null $transaction */ From 202facf43d821e688f768774d30ec98c568d1e88 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 17:56:09 +0200 Subject: [PATCH 19/30] Final touches for the balance and transactions. --- app/Factory/AccountFactory.php | 4 ++++ .../Controllers/Account/EditController.php | 15 ++++++++++----- .../Controllers/Account/IndexController.php | 4 ++-- .../Internal/Support/AccountServiceTrait.php | 18 ++++++++++++++++++ .../Internal/Update/AccountUpdateService.php | 11 ++++++++--- resources/views/v1/accounts/edit.twig | 1 + 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index 0a976bd8d3..3f1642ff4c 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -331,17 +331,21 @@ class AccountFactory */ private function storeCreditLiability(Account $account, array $data) { + Log::debug('storeCreditLiability'); $account->refresh(); $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 ($this->validOBData($data)) { + Log::debug('Has valid CL data.'); $openingBalance = $data['opening_balance']; $openingBalanceDate = $data['opening_balance_date']; $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); } if (!$this->validOBData($data)) { + Log::debug('Has NOT valid CL data.'); $this->deleteCreditTransaction($account); } } diff --git a/app/Http/Controllers/Account/EditController.php b/app/Http/Controllers/Account/EditController.php index be6a0e1da8..3aaacbf4cf 100644 --- a/app/Http/Controllers/Account/EditController.php +++ b/app/Http/Controllers/Account/EditController.php @@ -45,10 +45,8 @@ class EditController extends Controller use ModelInformation; private AttachmentHelperInterface $attachments; - /** @var CurrencyRepositoryInterface The currency repository */ - private $currencyRepos; - /** @var AccountRepositoryInterface The account repository */ - private $repository; + private CurrencyRepositoryInterface $currencyRepos; + private AccountRepositoryInterface $repository; /** * EditController constructor. @@ -106,6 +104,11 @@ class EditController extends Controller ], ]; + $liabilityDirections = [ + 'debit' => trans('firefly.liability_direction_debit'), + 'credit' => trans('firefly.liability_direction_credit'), + ]; + // interest calculation periods: $interestPeriods = [ 'daily' => (string)trans('firefly.interest_calc_daily'), @@ -119,7 +122,7 @@ class EditController extends Controller } $request->session()->forget('accounts.edit.fromUpdate'); - $openingBalanceAmount = (string)$repository->getOpeningBalanceAmount($account); + $openingBalanceAmount = app('steam')->positive((string)$repository->getOpeningBalanceAmount($account)); $openingBalanceDate = $repository->getOpeningBalanceDate($account); $currency = $this->repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); @@ -138,6 +141,7 @@ class EditController extends Controller 'opening_balance_date' => $openingBalanceDate, 'liability_type_id' => $account->account_type_id, 'opening_balance' => $openingBalanceAmount, + 'liability_direction' => $this->repository->getMetaValue($account, 'liability_direction'), 'virtual_balance' => $account->virtual_balance, 'currency_id' => $currency->id, 'include_net_worth' => $includeNetWorth, @@ -157,6 +161,7 @@ class EditController extends Controller 'subTitle', 'subTitleIcon', 'locations', + 'liabilityDirections', 'objectType', 'roles', 'preFilled', diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php index 028e2cfce6..ac0c95de4a 100644 --- a/app/Http/Controllers/Account/IndexController.php +++ b/app/Http/Controllers/Account/IndexController.php @@ -103,7 +103,7 @@ class IndexController extends Controller $account->startBalance = $this->isInArray($startBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id); $account->difference = bcsub($account->endBalance, $account->startBalance); - $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 6, '.', ''); + $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 4, '.', ''); $account->interestPeriod = (string)trans(sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period'))); $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); } @@ -164,7 +164,7 @@ class IndexController extends Controller $account->startBalance = $this->isInArray($startBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id); $account->difference = bcsub($account->endBalance, $account->startBalance); - $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 6, '.', ''); + $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 4, '.', ''); $account->interestPeriod = (string)trans(sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period'))); $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); $account->location = $this->repository->getLocation($account); diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php index 2b35d1713a..68b4076e16 100644 --- a/app/Services/Internal/Support/AccountServiceTrait.php +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -226,6 +226,24 @@ trait AccountServiceTrait } } + /** + * Delete TransactionGroup with liability credit in it. + * + * @param Account $account + */ + protected function deleteCreditTransaction(Account $account): void + { + Log::debug(sprintf('deleteCreditTransaction() for account #%d', $account->id)); + $creditGroup = $this->getCreditTransaction($account); + + if (null !== $creditGroup) { + Log::debug('Credit journal found, delete journal.'); + /** @var TransactionGroupDestroyService $service */ + $service = app(TransactionGroupDestroyService::class); + $service->destroy($creditGroup); + } + } + /** * Returns the opening balance group, or NULL if it does not exist. * diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php index e3b293899a..3219bde6ea 100644 --- a/app/Services/Internal/Update/AccountUpdateService.php +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -306,20 +306,25 @@ class AccountUpdateService */ private function updateCreditLiability(Account $account, array $data): void { - $type = $account->accountType; + $type = $account->accountType; $valid = config('firefly.valid_liabilities'); if (in_array($type->type, $valid, true)) { + $direction = array_key_exists('liability_direction', $data) ? $data['liability_direction'] : 'empty'; // check if is submitted as empty, that makes it valid: if ($this->validOBData($data) && !$this->isEmptyOBData($data)) { $openingBalance = $data['opening_balance']; $openingBalanceDate = $data['opening_balance_date']; - - $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + if ('credit' === $data['liability_direction']) { + $this->updateCreditTransaction($account, $openingBalance, $openingBalanceDate); + } } if (!$this->validOBData($data) && $this->isEmptyOBData($data)) { $this->deleteCreditTransaction($account); } + if ($this->validOBData($data) && !$this->isEmptyOBData($data) && 'credit' !== $direction) { + $this->deleteCreditTransaction($account); + } } } diff --git a/resources/views/v1/accounts/edit.twig b/resources/views/v1/accounts/edit.twig index 01013d7d72..a52a1e5f56 100644 --- a/resources/views/v1/accounts/edit.twig +++ b/resources/views/v1/accounts/edit.twig @@ -36,6 +36,7 @@ {% if objectType == 'liabilities' %} {{ ExpandedForm.select('liability_type_id', liabilityTypes) }} {{ ExpandedForm.amountNoCurrency('opening_balance', null, {label:'debt_start_amount'|_, helpText: 'debt_start_amount_help'|_}) }} + {{ ExpandedForm.select('liability_direction', liabilityDirections) }} {{ ExpandedForm.date('opening_balance_date', null, {label:'debt_start_date'|_}) }} {{ ExpandedForm.percentage('interest') }} {{ ExpandedForm.select('interest_period', interestPeriods) }} From 16b51711f5874352cb2ed1b55526dfeb1737aa03 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Apr 2021 18:21:52 +0200 Subject: [PATCH 20/30] Actual debit indicator --- resources/views/v1/list/accounts.twig | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/resources/views/v1/list/accounts.twig b/resources/views/v1/list/accounts.twig index 9e679d9e5f..8c487ff83d 100644 --- a/resources/views/v1/list/accounts.twig +++ b/resources/views/v1/list/accounts.twig @@ -14,7 +14,10 @@ {{ trans('list.interest') }} ({{ trans('list.interest_period') }}) {% endif %} {{ trans('form.account_number') }} - {{ trans('list.currentBalance') }} + {{ trans('list.currentBalance') }} + {% if objectType == 'liabilities' %} + Left to pay off + {% endif %} {{ trans('list.active') }} {# hide last activity to make room for other stuff #} {% if objectType != 'liabilities' %} @@ -50,10 +53,8 @@ {% endif %} {% if objectType == 'liabilities' %} - {{ account.accountTypeString }} - - {{ account.interest }}% ({{ account.interestPeriod|lower }}) - + {{ account.accountTypeString }} + {{ account.interest }}% ({{ account.interestPeriod|lower }}) {% endif %} {{ account.iban }}{% if account.iban == '' %}{{ accountGetMetaField(account, 'account_number') }}{% endif %} @@ -61,6 +62,11 @@ {{ formatAmountByAccount(account, account.endBalance) }} + {% if objectType == 'liabilities' %} + + Test + + {% endif %} {% if account.active %} From a41d7378ef34d224dfbb4d484e944fd2db9c1c57 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 11 Apr 2021 06:41:21 +0200 Subject: [PATCH 21/30] Some fixes in the account list --- .../Controllers/Account/IndexController.php | 21 +++++++++----- .../Support/CreditRecalculateService.php | 29 +++++++++++++++++-- resources/lang/en_US/firefly.php | 3 ++ resources/views/v1/list/accounts.twig | 10 +++++-- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php index ac0c95de4a..c80673db05 100644 --- a/app/Http/Controllers/Account/IndexController.php +++ b/app/Http/Controllers/Account/IndexController.php @@ -160,14 +160,19 @@ class IndexController extends Controller $accounts->each( function (Account $account) use ($activities, $startBalances, $endBalances) { // TODO lots of queries executed in this block. - $account->lastActivityDate = $this->isInArray($activities, $account->id); - $account->startBalance = $this->isInArray($startBalances, $account->id); - $account->endBalance = $this->isInArray($endBalances, $account->id); - $account->difference = bcsub($account->endBalance, $account->startBalance); - $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 4, '.', ''); - $account->interestPeriod = (string)trans(sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period'))); - $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); - $account->location = $this->repository->getLocation($account); + $account->lastActivityDate = $this->isInArray($activities, $account->id); + $account->startBalance = $this->isInArray($startBalances, $account->id); + $account->endBalance = $this->isInArray($endBalances, $account->id); + $account->difference = bcsub($account->endBalance, $account->startBalance); + $account->interest = number_format((float)$this->repository->getMetaValue($account, 'interest'), 4, '.', ''); + $account->interestPeriod = (string)trans( + sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period')) + ); + $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); + $account->location = $this->repository->getLocation($account); + + $account->liability_direction = $this->repository->getMetaValue($account, 'liability_direction'); + $account->current_debt = $this->repository->getMetaValue($account, 'current_debt'); } ); // make paginator: diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index 2f925730d0..c4d2a8a457 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -28,6 +28,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Log; @@ -101,13 +102,18 @@ class CreditRecalculateService // get opening balance (if present) $this->repository->setUser($account->user); $startOfDebt = $this->repository->getOpeningBalanceAmount($account) ?? '0'; - + $leftOfDebt = app('steam')->positive($startOfDebt); /** @var AccountMetaFactory $factory */ $factory = app(AccountMetaFactory::class); $factory->crud($account, 'start_of_debt', $startOfDebt); - $factory->crud($account, 'current_debt', $startOfDebt); - // update meta data: + // now loop all transactions (except opening balance and credit thing) + $transactions = $account->transactions()->get(); + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $leftOfDebt = $this->processTransaction($transaction, $leftOfDebt); + } + $factory->crud($account, 'current_debt', $leftOfDebt); Log::debug(sprintf('Done with %s(#%d)', __METHOD__, $account->id)); @@ -230,5 +236,22 @@ class CreditRecalculateService $this->group = $group; } + /** + * @param Transaction $transaction + * @param string $amount + * + * @return string + */ + private function processTransaction(Transaction $transaction, string $amount): string + { + $journal = $transaction->transactionJournal; + $type = $journal->transactionType->type; + if (in_array($type, [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER], true)) { + $amount = bcadd($amount, bcmul($transaction->amount, '-1')); + } + + return $amount; + } + } \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c305df31b8..c3c5f46972 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1322,6 +1322,8 @@ return [ 'account_type_Credit card' => 'Credit card', 'liability_direction_credit' => 'I am owed this debt', 'liability_direction_debit' => 'I owe this debt to somebody else', + 'liability_direction_credit_short' => 'Owed this debt', + 'liability_direction_debit_short' => 'Owe this debt', 'Liability credit' => 'Liability credit', 'budgets' => 'Budgets', 'tags' => 'Tags', @@ -1393,6 +1395,7 @@ return [ 'splitByAccount' => 'Split by account', 'coveredWithTags' => 'Covered with tags', 'leftInBudget' => 'Left in budget', + 'left_in_debt' => 'Left', 'sumOfSums' => 'Sum of sums', 'noCategory' => '(no category)', 'notCharged' => 'Not charged (yet)', diff --git a/resources/views/v1/list/accounts.twig b/resources/views/v1/list/accounts.twig index 8c487ff83d..03c2694001 100644 --- a/resources/views/v1/list/accounts.twig +++ b/resources/views/v1/list/accounts.twig @@ -11,12 +11,15 @@ {% endif %} {% if objectType == 'liabilities' %} {{ trans('list.liability_type') }} + {{ trans('form.liability_direction') }} {{ trans('list.interest') }} ({{ trans('list.interest_period') }}) {% endif %} {{ trans('form.account_number') }} {{ trans('list.currentBalance') }} {% if objectType == 'liabilities' %} - Left to pay off + + {{ trans('firefly.left_in_debt') }} + {% endif %} {{ trans('list.active') }} {# hide last activity to make room for other stuff #} @@ -54,6 +57,7 @@ {% endif %} {% if objectType == 'liabilities' %} {{ account.accountTypeString }} + {{ trans('firefly.liability_direction_'~account.liability_direction~'_short') }} {{ account.interest }}% ({{ account.interestPeriod|lower }}) {% endif %} {{ account.iban }}{% if account.iban == '' %}{{ accountGetMetaField(account, 'account_number') }}{% endif %} @@ -63,8 +67,8 @@ {% if objectType == 'liabilities' %} - - Test + + {{ formatAmountByAccount(account, account.current_debt, false) }} {% endif %} From 39925f813987a117eb3dfc3148ecfda7fb45f615 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 12 Apr 2021 06:08:21 +0200 Subject: [PATCH 22/30] Small changes in list --- app/Http/Controllers/Account/IndexController.php | 2 +- app/Http/Requests/AccountFormRequest.php | 9 +++------ resources/views/v1/list/accounts.twig | 4 +++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php index c80673db05..9d9ecabc80 100644 --- a/app/Http/Controllers/Account/IndexController.php +++ b/app/Http/Controllers/Account/IndexController.php @@ -172,7 +172,7 @@ class IndexController extends Controller $account->location = $this->repository->getLocation($account); $account->liability_direction = $this->repository->getMetaValue($account, 'liability_direction'); - $account->current_debt = $this->repository->getMetaValue($account, 'current_debt'); + $account->current_debt = $this->repository->getMetaValue($account, 'current_debt') ?? '-'; } ); // make paginator: diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index 86370a1440..4cc9298b32 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -75,7 +75,9 @@ class AccountFormRequest extends FormRequest if ('liabilities' === $data['account_type_name']) { $data['account_type_name'] = null; $data['account_type_id'] = $this->integer('liability_type_id'); - $data['opening_balance'] = app('steam')->negative($data['opening_balance']); + if ('' !== $data['opening_balance']) { + $data['opening_balance'] = app('steam')->negative($data['opening_balance']); + } } return $data; @@ -110,11 +112,6 @@ class AccountFormRequest extends FormRequest ]; $rules = Location::requestRules($rules); - if ('liabilities' === $this->get('objectType')) { - $rules['opening_balance'] = ['numeric', 'required', 'max:1000000000']; - $rules['opening_balance_date'] = 'date|required'; - } - /** @var Account $account */ $account = $this->route()->parameter('account'); if (null !== $account) { diff --git a/resources/views/v1/list/accounts.twig b/resources/views/v1/list/accounts.twig index 03c2694001..5923f81a6f 100644 --- a/resources/views/v1/list/accounts.twig +++ b/resources/views/v1/list/accounts.twig @@ -68,7 +68,9 @@ {% if objectType == 'liabilities' %} - {{ formatAmountByAccount(account, account.current_debt, false) }} + {% if '-' != account.current_debt %} + {{ formatAmountByAccount(account, account.current_debt, false) }} + {% endif %} {% endif %} From e3b93af297310b5c786eec8dd49bf804d1e8ad02 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 23 Apr 2021 19:13:38 +0200 Subject: [PATCH 23/30] Add some code to fix liabilities. --- .../Commands/Upgrade/UpgradeLiabilities.php | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 app/Console/Commands/Upgrade/UpgradeLiabilities.php diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradeLiabilities.php new file mode 100644 index 0000000000..55bfa8670f --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradeLiabilities.php @@ -0,0 +1,175 @@ +isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + $this->upgradeLiabilities(); + + //$this->markAsExecuted(); + + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('in %s seconds.', $end)); + + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + + /** + * @param Account $account + * @param TransactionJournal $openingBalance + */ + private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void + { + $source = $this->getSourceTransaction($openingBalance); + $destination = $this->getDestinationTransaction($openingBalance); + if (null === $source || null === $destination) { + return; + } + // source MUST be the liability. + if ((int)$destination->account_id === (int)$account->id) { + Log::debug(sprintf('Must switch around, because account #%d is the destination.', $destination->account_id)); + // so if not, switch things around: + $sourceAccountId = (int)$source->account_id; + $source->account_id = $destination->account_id; + $destination->account_id = $sourceAccountId; + $source->save(); + $destination->save(); + Log::debug(sprintf('Source transaction #%d now has account #%d', $source->id, $source->account_id)); + Log::debug(sprintf('Dest transaction #%d now has account #%d', $destination->id, $destination->account_id)); + } + } + + /** + * @param TransactionJournal $journal + * + * @return Transaction + */ + private function getSourceTransaction(TransactionJournal $journal): ?Transaction + { + return $journal->transactions()->where('amount', '<', 0)->first(); + } + + /** + * @param TransactionJournal $journal + * + * @return Transaction + */ + private function getDestinationTransaction(TransactionJournal $journal): ?Transaction + { + return $journal->transactions()->where('amount', '>', 0)->first(); + } + + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * + */ + private function upgradeLiabilities(): void + { + Log::debug('Upgrading liabilities.'); + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + $this->upgradeForUser($user); + } + } + + /** + * @param User $user + */ + private function upgradeForUser(User $user): void + { + Log::debug(sprintf('Upgrading liabilities for user #%d', $user->id)); + $accounts = $user->accounts() + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', config('firefly.valid_liabilities')) + ->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + $this->upgradeLiability($account); + } + } + + /** + * @param Account $account + */ + private function upgradeLiability(Account $account): void + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($account->user); + Log::debug(sprintf('Upgrade liability #%d', $account->id)); + + // get opening balance, and correct if necessary. + $openingBalance = $repository->getOpeningBalance($account); + if (null !== $openingBalance) { + // correct if necessary + $this->correctOpeningBalance($account, $openingBalance); + } + + // add liability direction property + /** @var AccountMetaFactory $factory */ + $factory = app(AccountMetaFactory::class); + $factory->crud($account, 'liability_direction', 'debit'); + } + +} From be3cb791a54b6a14c078d165b595fb7fe6c3b168 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 23 Apr 2021 19:15:03 +0200 Subject: [PATCH 24/30] Fix text --- app/Console/Commands/Upgrade/UpgradeLiabilities.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradeLiabilities.php index 55bfa8670f..8cdf7169fc 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradeLiabilities.php @@ -45,10 +45,10 @@ class UpgradeLiabilities extends Command } $this->upgradeLiabilities(); - //$this->markAsExecuted(); + $this->markAsExecuted(); $end = round(microtime(true) - $start, 2); - $this->info(sprintf('in %s seconds.', $end)); + $this->info(sprintf('Upgraded liabilities in %s seconds.', $end)); return 0; } @@ -111,7 +111,6 @@ class UpgradeLiabilities extends Command return $journal->transactions()->where('amount', '>', 0)->first(); } - /** * */ From 0b948516236232620da42516eb2c7bec3560ebc1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 26 Apr 2021 07:29:39 +0200 Subject: [PATCH 25/30] Fix some cases in loans --- .../Support/CreditRecalculateService.php | 24 ++++++++++++++----- resources/views/v1/list/groups.twig | 20 ++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index c4d2a8a457..6f457e9649 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -72,11 +72,8 @@ class CreditRecalculateService return; } - $this->processWork(); - - Log::debug('Will now do CreditRecalculationService'); - // do something + $this->processWork(); } /** @@ -111,7 +108,7 @@ class CreditRecalculateService $transactions = $account->transactions()->get(); /** @var Transaction $transaction */ foreach ($transactions as $transaction) { - $leftOfDebt = $this->processTransaction($transaction, $leftOfDebt); + $leftOfDebt = $this->processTransaction($account, $transaction, $leftOfDebt); } $factory->crud($account, 'current_debt', $leftOfDebt); @@ -242,13 +239,28 @@ class CreditRecalculateService * * @return string */ - private function processTransaction(Transaction $transaction, string $amount): string + private function processTransaction(Account $account, Transaction $transaction, string $amount): string { + Log::debug(sprintf('Now in %s(#%d, %s)', __METHOD__, $transaction->id, $amount)); $journal = $transaction->transactionJournal; $type = $journal->transactionType->type; + + Log::debug(sprintf('Type is "%s"', $type)); + if (in_array($type, [TransactionType::WITHDRAWAL]) && (int)$account->id === (int)$transaction->account_id && 1 === bccomp($transaction->amount, '0')) { + Log::debug(sprintf('Transaction #%d is withdrawal into liability #%d, does not influence the amount left.', $account->id, $transaction->account_id)); + + return $amount; + } + if (in_array($type, [TransactionType::DEPOSIT]) && (int)$account->id === (int)$transaction->account_id && -1 === bccomp($transaction->amount, '0')) { + Log::debug(sprintf('Transaction #%d is deposit from liability #%d,does not influence the amount left.', $account->id, $transaction->account_id)); + + return $amount; + } + if (in_array($type, [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER], true)) { $amount = bcadd($amount, bcmul($transaction->amount, '-1')); } + Log::debug(sprintf('Amount is now %s', $amount)); return $amount; } diff --git a/resources/views/v1/list/groups.twig b/resources/views/v1/list/groups.twig index 5bef92952c..e8ccf53311 100644 --- a/resources/views/v1/list/groups.twig +++ b/resources/views/v1/list/groups.twig @@ -139,11 +139,13 @@ {% endif %} + {# deposit #} {% if transaction.transaction_type_type == 'Deposit' %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {# transfer #} {% elseif transaction.transaction_type_type == 'Transfer' %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places, false) }} @@ -151,6 +153,7 @@ ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places, false) }}) {% endif %} + {# opening balance #} {% elseif transaction.transaction_type_type == 'Opening balance' %} {% if transaction.source_account_type == 'Initial balance account' %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places) }} @@ -163,6 +166,7 @@ ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} {% endif %} + {# reconciliation #} {% elseif transaction.transaction_type_type == 'Reconciliation' %} {% if transaction.source_account_type == 'Reconciliation account' %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places) }} @@ -175,6 +179,22 @@ ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} {% endif %} + {# liability credit #} + {% elseif transaction.transaction_type_type == 'Liability credit' %} + {% if transaction.source_account_type == 'Liability credit' %} + {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} + {% if null != transaction.foreign_amount %} + ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) + {% endif %} + {% else %} + {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places) }} + {% if null != transaction.foreign_amount %} + ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) + {% endif %} + {% endif %} + + + {# THE REST #} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} From 8ef6595cedd0b1fbc8c793196d179f1f930acf9c Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 26 Apr 2021 09:48:04 +0200 Subject: [PATCH 26/30] Better sentence --- resources/lang/en_US/firefly.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c3c5f46972..6adc1c8959 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1395,7 +1395,7 @@ return [ 'splitByAccount' => 'Split by account', 'coveredWithTags' => 'Covered with tags', 'leftInBudget' => 'Left in budget', - 'left_in_debt' => 'Left', + 'left_in_debt' => 'Amount due', 'sumOfSums' => 'Sum of sums', 'noCategory' => '(no category)', 'notCharged' => 'Not charged (yet)', From be844b82af950c182123f8a3f36f8071ff5ab91c Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 27 Apr 2021 06:57:06 +0200 Subject: [PATCH 27/30] Add missing method. --- .../Account/AccountRepository.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 2717239829..a3f7e4c198 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -764,4 +764,30 @@ class AccountRepository implements AccountRepositoryInterface return $journal->transactionGroup; } + + /** + * @inheritDoc + */ + public function findByAccountNumber(string $number, array $types): ?Account + { + $dbQuery = $this->user + ->accounts() + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->where('accounts.active', true) + ->where( + function (EloquentBuilder $q1) use ($number) { + $json = json_encode($number); + $q1->where('account_meta.name', '=', 'account_number'); + $q1->where('account_meta.data', '=', $json); + } + ); + + if (0 !== count($types)) { + $dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); + $dbQuery->whereIn('account_types.type', $types); + } + + return $dbQuery->first(['accounts.*']); + } + } From a83578d1ae812d21e6145f7dc40aa156f39c7c0f Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 27 Apr 2021 07:55:54 +0200 Subject: [PATCH 28/30] Update code for proper index. --- .../Commands/Upgrade/UpgradeLiabilities.php | 4 ++ .../Controllers/Account/EditController.php | 3 + .../Account/AccountRepository.php | 2 +- app/Transformers/AccountTransformer.php | 1 + frontend/src/components/accounts/Index.vue | 67 ++++++++++++++++++- resources/lang/en_US/config.php | 1 + resources/lang/en_US/firefly.php | 3 + resources/lang/en_US/list.php | 3 +- 8 files changed, 80 insertions(+), 4 deletions(-) diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradeLiabilities.php index 8cdf7169fc..528ecf7b09 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradeLiabilities.php @@ -7,6 +7,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\User; use Illuminate\Console\Command; use Log; @@ -145,6 +146,9 @@ class UpgradeLiabilities extends Command /** @var Account $account */ foreach ($accounts as $account) { $this->upgradeLiability($account); + $service = app(CreditRecalculateService::class); + $service->setAccount($account); + $service->recalculate(); } } diff --git a/app/Http/Controllers/Account/EditController.php b/app/Http/Controllers/Account/EditController.php index 3aaacbf4cf..16e02e6403 100644 --- a/app/Http/Controllers/Account/EditController.php +++ b/app/Http/Controllers/Account/EditController.php @@ -123,6 +123,9 @@ class EditController extends Controller $request->session()->forget('accounts.edit.fromUpdate'); $openingBalanceAmount = app('steam')->positive((string)$repository->getOpeningBalanceAmount($account)); + if('0' === $openingBalanceAmount) { + $openingBalanceAmount = ''; + } $openingBalanceDate = $repository->getOpeningBalanceDate($account); $currency = $this->repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index a3f7e4c198..fe1a0284fa 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -789,5 +789,5 @@ class AccountRepository implements AccountRepositoryInterface return $dbQuery->first(['accounts.*']); } - + } diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 23c9627a51..da1522c070 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -119,6 +119,7 @@ class AccountTransformer extends AbstractTransformer 'liability_direction' => $liabilityDirection, 'interest' => $interest, 'interest_period' => $interestPeriod, + 'current_debt' => $this->repository->getMetaValue($account,'current_debt'), 'include_net_worth' => $includeNetWorth, 'longitude' => $longitude, 'latitude' => $latitude, diff --git a/frontend/src/components/accounts/Index.vue b/frontend/src/components/accounts/Index.vue index b5ae2f03a6..6f3bda6978 100644 --- a/frontend/src/components/accounts/Index.vue +++ b/frontend/src/components/accounts/Index.vue @@ -54,6 +54,31 @@ {{ data.item.account_number }} {{ data.item.iban }} ({{ data.item.account_number }}) + + +