From 939c636a74897d08d84f10a1a27f04ec6ff8f6f8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 11 Aug 2023 06:16:40 +0200 Subject: [PATCH] Add new action that can switch the source and destination account of a transfer --- .../Actions/SwitchAccounts.php | 97 +++++++++++++++++++ config/firefly.php | 2 + public/v1/js/ff/rules/create-edit.js | 91 ++++++++--------- resources/lang/en_US/firefly.php | 14 ++- 4 files changed, 156 insertions(+), 48 deletions(-) create mode 100644 app/TransactionRules/Actions/SwitchAccounts.php diff --git a/app/TransactionRules/Actions/SwitchAccounts.php b/app/TransactionRules/Actions/SwitchAccounts.php new file mode 100644 index 0000000000..98d67259fc --- /dev/null +++ b/app/TransactionRules/Actions/SwitchAccounts.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\TransactionRules\Actions; + +use DB; +use FireflyIII\Events\TriggeredAuditLog; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Illuminate\Support\Facades\Log; + +/** + * + * Class SwitchAccounts + */ +class SwitchAccounts implements ActionInterface +{ + private RuleAction $action; + + /** + * TriggerInterface constructor. + * + * @param RuleAction $action + */ + public function __construct(RuleAction $action) + { + $this->action = $action; + } + + /** + * @inheritDoc + */ + public function actOnArray(array $journal): bool + { + // make object from array (so the data is fresh). + /** @var TransactionJournal|null $object */ + $object = TransactionJournal::where('user_id', $journal['user_id'])->find($journal['transaction_journal_id']); + if (null === $object) { + Log::error(sprintf('Cannot find journal #%d, cannot switch accounts.', $journal['transaction_journal_id'])); + return false; + } + $groupCount = TransactionJournal::where('transaction_group_id', $journal['transaction_group_id'])->count(); + if ($groupCount > 1) { + Log::error(sprintf('Group #%d has more than one transaction in it, cannot switch accounts.', $journal['transaction_group_id'])); + return false; + } + + $type = $object->transactionType->type; + if (TransactionType::TRANSFER !== $type) { + Log::error(sprintf('Journal #%d is NOT a transfer (rule #%d), cannot switch accounts.', $journal['transaction_journal_id'], $this->action->rule_id)); + + return false; + } + + /** @var Transaction $sourceTransaction */ + $sourceTransaction = $object->transactions()->where('amount', '<', 0)->first(); + /** @var Transaction $destTransaction */ + $destTransaction = $object->transactions()->where('amount', '>', 0)->first(); + if (null === $sourceTransaction || null === $destTransaction) { + Log::error(sprintf('Journal #%d has no source or destination transaction (rule #%d), cannot switch accounts.', $journal['transaction_journal_id'], $this->action->rule_id)); + + return false; + } + $sourceAccountId = (int)$sourceTransaction->account_id; + $destinationAccountId = $destTransaction->account_id; + $sourceTransaction->account_id = $destinationAccountId; + $destTransaction->account_id = $sourceAccountId; + $sourceTransaction->save(); + $destTransaction->save(); + + event(new TriggeredAuditLog($this->action->rule, $object, 'switch_accounts', $sourceAccountId, $destinationAccountId)); + + return true; + } +} diff --git a/config/firefly.php b/config/firefly.php index 9330870c16..3ded7995ae 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -86,6 +86,7 @@ use FireflyIII\TransactionRules\Actions\SetDescription; use FireflyIII\TransactionRules\Actions\SetDestinationAccount; use FireflyIII\TransactionRules\Actions\SetNotes; use FireflyIII\TransactionRules\Actions\SetSourceAccount; +use FireflyIII\TransactionRules\Actions\SwitchAccounts; use FireflyIII\TransactionRules\Actions\UpdatePiggybank; use FireflyIII\User; @@ -503,6 +504,7 @@ return [ 'convert_withdrawal' => ConvertToWithdrawal::class, 'convert_deposit' => ConvertToDeposit::class, 'convert_transfer' => ConvertToTransfer::class, + 'switch_accounts' => SwitchAccounts::class, 'update_piggy' => UpdatePiggybank::class, 'delete_transaction' => DeleteTransaction::class, 'append_descr_to_notes' => AppendDescriptionToNotes::class, diff --git a/public/v1/js/ff/rules/create-edit.js b/public/v1/js/ff/rules/create-edit.js index 16b99fe032..eb3688cfa1 100644 --- a/public/v1/js/ff/rules/create-edit.js +++ b/public/v1/js/ff/rules/create-edit.js @@ -162,7 +162,7 @@ function onAddNewAction() { "use strict"; console.log('Now in onAddNewAction()'); - var selectQuery = 'select[name^="actions["][name$="][type]"]'; + var selectQuery = 'select[name^="actions["][name$="][type]"]'; var selectResult = $(selectQuery); console.log('Select query is "' + selectQuery + '" and the result length is ' + selectResult.length); @@ -190,7 +190,7 @@ function onAddNewTrigger() { "use strict"; console.log('Now in onAddNewTrigger()'); - var selectQuery = 'select[name^="triggers["][name$="][type]"]'; + var selectQuery = 'select[name^="triggers["][name$="][type]"]'; var selectResult = $(selectQuery); console.log('Select query is "' + selectQuery + '" and the result length is ' + selectResult.length); @@ -217,9 +217,9 @@ function onAddNewTrigger() { function updateActionInput(selectList) { console.log('Now in updateActionInput() for a select list, currently with value "' + selectList.val() + '".'); // the actual row this select list is in: - var parent = selectList.parent().parent(); + var parent = selectList.parent().parent(); // the text input we're looking for: - var inputQuery = 'input[name^="actions["][name$="][value]"]'; + var inputQuery = 'input[name^="actions["][name$="][value]"]'; var inputResult = parent.find(inputQuery); console.log('Searching for children in this row with query "' + inputQuery + '" resulted in ' + inputResult.length + ' results.'); @@ -234,6 +234,7 @@ function updateActionInput(selectList) { case 'clear_budget': case 'append_descr_to_notes': case 'append_notes_to_descr': + case 'switch_accounts': case 'move_descr_to_notes': case 'move_notes_to_descr': case 'clear_notes': @@ -296,9 +297,9 @@ function updateActionInput(selectList) { function updateTriggerInput(selectList) { console.log('Now in updateTriggerInput() for a select list, currently with value "' + selectList.val() + '".'); // the actual row this select list is in: - var parent = selectList.parent().parent(); + var parent = selectList.parent().parent(); // the text input we're looking for: - var inputQuery = 'input[name^="triggers["][name$="][value]"]'; + var inputQuery = 'input[name^="triggers["][name$="][value]"]'; var inputResult = parent.find(inputQuery); console.log('Searching for children in this row with query "' + inputQuery + '" resulted in ' + inputResult.length + ' results.'); @@ -391,50 +392,50 @@ function createAutoComplete(input, URL) { input.typeahead('destroy'); // append URL: - var lastChar = URL[URL.length - 1]; + var lastChar = URL[URL.length - 1]; var urlParamSplit = '?'; if ('&' === lastChar) { urlParamSplit = ''; } var source = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - prefetch: { - url: URL + urlParamSplit + 'uid=' + uid, - filter: function (list) { - return $.map(list, function (item) { - if (item.hasOwnProperty('active') && item.active === true) { - return {name: item.name}; - } - if (item.hasOwnProperty('active') && item.active === false) { - return; - } - if (item.hasOwnProperty('active')) { - console.log(item.active); - } - return {name: item.name}; - }); - } - }, - remote: { - url: URL + urlParamSplit + 'query=%QUERY&uid=' + uid, - wildcard: '%QUERY', - filter: function (list) { - return $.map(list, function (item) { - if (item.hasOwnProperty('active') && item.active === true) { - return {name: item.name}; - } - if (item.hasOwnProperty('active') && item.active === false) { - return; - } - if (item.hasOwnProperty('active')) { - console.log(item.active); - } - return {name: item.name}; - }); - } - } - }); + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + prefetch: { + url: URL + urlParamSplit + 'uid=' + uid, + filter: function (list) { + return $.map(list, function (item) { + if (item.hasOwnProperty('active') && item.active === true) { + return {name: item.name}; + } + if (item.hasOwnProperty('active') && item.active === false) { + return; + } + if (item.hasOwnProperty('active')) { + console.log(item.active); + } + return {name: item.name}; + }); + } + }, + remote: { + url: URL + urlParamSplit + 'query=%QUERY&uid=' + uid, + wildcard: '%QUERY', + filter: function (list) { + return $.map(list, function (item) { + if (item.hasOwnProperty('active') && item.active === true) { + return {name: item.name}; + } + if (item.hasOwnProperty('active') && item.active === false) { + return; + } + if (item.hasOwnProperty('active')) { + console.log(item.active); + } + return {name: item.name}; + }); + } + } + }); source.initialize(); input.typeahead({hint: true, highlight: true,}, {source: source, displayKey: 'name', autoSelect: false}); } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index b5d5bcbed1..94b792c88f 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -904,10 +904,14 @@ return [ 'rule_trigger_internal_reference_is' => 'Internal reference is ":trigger_value"', 'rule_trigger_journal_id_choice' => 'Transaction journal ID is..', 'rule_trigger_journal_id' => 'Transaction journal ID is ":trigger_value"', - 'rule_trigger_no_external_url' => 'Transaction has no external URL', - 'rule_trigger_any_external_url' => 'Transaction has an external URL', - 'rule_trigger_any_external_url_choice' => 'Transaction has an external URL', + 'rule_trigger_any_external_url' => 'Transaction has an (any) external URL', + 'rule_trigger_any_external_url_choice' => 'Transaction has an (any) external URL', + 'rule_trigger_any_external_id' => 'Transaction has an (any) external ID', + 'rule_trigger_any_external_id_choice' => 'Transaction has an (any) external ID', 'rule_trigger_no_external_url_choice' => 'Transaction has no external URL', + 'rule_trigger_no_external_url' => 'Transaction has no external URL', + 'rule_trigger_no_external_id_choice' => 'Transaction has no external ID', + 'rule_trigger_no_external_id' => 'Transaction has no external ID', 'rule_trigger_id_choice' => 'Transaction ID is..', 'rule_trigger_id' => 'Transaction ID is ":trigger_value"', 'rule_trigger_sepa_ct_is_choice' => 'SEPA CT is..', @@ -1178,6 +1182,7 @@ return [ // Ignore this comment // actions + // set, clear, add, remove, append/prepend 'rule_action_delete_transaction_choice' => 'DELETE transaction(!)', 'rule_action_delete_transaction' => 'DELETE transaction(!)', 'rule_action_set_category' => 'Set category to ":action_value"', @@ -1215,6 +1220,8 @@ return [ 'rule_action_set_notes_choice' => 'Set notes to ..', 'rule_action_link_to_bill_choice' => 'Link to a bill ..', 'rule_action_link_to_bill' => 'Link to bill ":action_value"', + 'rule_action_switch_accounts_choice' => 'Switch source and destination accounts (transfers only!)', + 'rule_action_switch_accounts' => 'Switch source and destination ', 'rule_action_set_notes' => 'Set notes to ":action_value"', 'rule_action_convert_deposit_choice' => 'Convert the transaction to a deposit', 'rule_action_convert_deposit' => 'Convert the transaction to a deposit from ":action_value"', @@ -2603,6 +2610,7 @@ return [ 'ale_action_clear_tag' => 'Cleared tag', 'ale_action_clear_all_tags' => 'Cleared all tags', 'ale_action_set_bill' => 'Linked to bill', + 'ale_action_switch_accounts' => 'Switched source and destination account', 'ale_action_set_budget' => 'Set budget', 'ale_action_set_category' => 'Set category', 'ale_action_set_source' => 'Set source account',