mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-18 10:16:49 +00:00
Allow rule to be applied to transactions (not just group).
This commit is contained in:
@@ -13,12 +13,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Http\Controllers;
|
namespace FireflyIII\Http\Controllers;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use ExpandedForm;
|
||||||
use FireflyIII\Http\Requests\RuleFormRequest;
|
use FireflyIII\Http\Requests\RuleFormRequest;
|
||||||
|
use FireflyIII\Http\Requests\SelectTransactionsRequest;
|
||||||
use FireflyIII\Http\Requests\TestRuleFormRequest;
|
use FireflyIII\Http\Requests\TestRuleFormRequest;
|
||||||
|
use FireflyIII\Jobs\ExecuteRuleOnExistingTransactions;
|
||||||
|
use FireflyIII\Models\AccountType;
|
||||||
use FireflyIII\Models\Rule;
|
use FireflyIII\Models\Rule;
|
||||||
use FireflyIII\Models\RuleAction;
|
use FireflyIII\Models\RuleAction;
|
||||||
use FireflyIII\Models\RuleGroup;
|
use FireflyIII\Models\RuleGroup;
|
||||||
use FireflyIII\Models\RuleTrigger;
|
use FireflyIII\Models\RuleTrigger;
|
||||||
|
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||||
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
|
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
|
||||||
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
|
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
|
||||||
use FireflyIII\Rules\TransactionMatcher;
|
use FireflyIII\Rules\TransactionMatcher;
|
||||||
@@ -237,6 +243,58 @@ class RuleController extends Controller
|
|||||||
return Response::json('true');
|
return Response::json('true');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Execute the given rule on a set of existing transactions
|
||||||
|
*
|
||||||
|
* @param SelectTransactionsRequest $request
|
||||||
|
* @param AccountRepositoryInterface $repository
|
||||||
|
* @param RuleGroup $ruleGroup
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function execute(SelectTransactionsRequest $request, AccountRepositoryInterface $repository, Rule $rule)
|
||||||
|
{
|
||||||
|
// Get parameters specified by the user
|
||||||
|
$accounts = $repository->getAccountsById($request->get('accounts'));
|
||||||
|
$startDate = new Carbon($request->get('start_date'));
|
||||||
|
$endDate = new Carbon($request->get('end_date'));
|
||||||
|
|
||||||
|
// Create a job to do the work asynchronously
|
||||||
|
$job = new ExecuteRuleOnExistingTransactions($rule);
|
||||||
|
|
||||||
|
// Apply parameters to the job
|
||||||
|
$job->setUser(auth()->user());
|
||||||
|
$job->setAccounts($accounts);
|
||||||
|
$job->setStartDate($startDate);
|
||||||
|
$job->setEndDate($endDate);
|
||||||
|
|
||||||
|
// Dispatch a new job to execute it in a queue
|
||||||
|
$this->dispatch($job);
|
||||||
|
|
||||||
|
// Tell the user that the job is queued
|
||||||
|
Session::flash('success', strval(trans('firefly.applied_rule_selection', ['title' => $rule->title])));
|
||||||
|
|
||||||
|
return redirect()->route('rules.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AccountRepositoryInterface $repository
|
||||||
|
* @param RuleGroup $ruleGroup
|
||||||
|
*
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function selectTransactions(AccountRepositoryInterface $repository, Rule $rule)
|
||||||
|
{
|
||||||
|
// does the user have shared accounts?
|
||||||
|
$accounts = $repository->getAccountsByType([AccountType::ASSET]);
|
||||||
|
$accountList = ExpandedForm::makeSelectList($accounts);
|
||||||
|
$checkedAccounts = array_keys($accountList);
|
||||||
|
$first = session('first')->format('Y-m-d');
|
||||||
|
$today = Carbon::create()->format('Y-m-d');
|
||||||
|
$subTitle = (string)trans('firefly.apply_rule_selection', ['title' => $rule->title]);
|
||||||
|
|
||||||
|
return view('rules.rule.select-transactions', compact('checkedAccounts', 'accountList', 'first', 'today', 'rule', 'subTitle'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param RuleFormRequest $request
|
* @param RuleFormRequest $request
|
||||||
@@ -265,6 +323,52 @@ class RuleController extends Controller
|
|||||||
return redirect($this->getPreviousUri('rules.create.uri'));
|
return redirect($this->getPreviousUri('rules.create.uri'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method allows the user to test a certain set of rule triggers. The rule triggers are grabbed from
|
||||||
|
* the rule itself.
|
||||||
|
*
|
||||||
|
* This method will parse and validate those rules and create a "TransactionMatcher" which will attempt
|
||||||
|
* to find transaction journals matching the users input. A maximum range of transactions to try (range) and
|
||||||
|
* a maximum number of transactions to return (limit) are set as well.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param Rule $rule
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function testTriggersByRule(Rule $rule) {
|
||||||
|
|
||||||
|
$triggers = $rule->ruleTriggers;
|
||||||
|
|
||||||
|
if (count($triggers) === 0) {
|
||||||
|
return Response::json(['html' => '', 'warning' => trans('firefly.warning_no_valid_triggers')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = config('firefly.test-triggers.limit');
|
||||||
|
$range = config('firefly.test-triggers.range');
|
||||||
|
|
||||||
|
/** @var TransactionMatcher $matcher */
|
||||||
|
$matcher = app(TransactionMatcher::class);
|
||||||
|
$matcher->setLimit($limit);
|
||||||
|
$matcher->setRange($range);
|
||||||
|
$matcher->setRule($rule);
|
||||||
|
$matchingTransactions = $matcher->findTransactionsByRule();
|
||||||
|
|
||||||
|
// Warn the user if only a subset of transactions is returned
|
||||||
|
$warning = '';
|
||||||
|
if (count($matchingTransactions) === $limit) {
|
||||||
|
$warning = trans('firefly.warning_transaction_subset', ['max_num_transactions' => $limit]);
|
||||||
|
}
|
||||||
|
if (count($matchingTransactions) === 0) {
|
||||||
|
$warning = trans('firefly.warning_no_matching_transactions', ['num_transactions' => $range]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return json response
|
||||||
|
$view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render();
|
||||||
|
|
||||||
|
return Response::json(['html' => $view, 'warning' => $warning]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method allows the user to test a certain set of rule triggers. The rule triggers are passed along
|
* This method allows the user to test a certain set of rule triggers. The rule triggers are passed along
|
||||||
* using the URL parameters (GET), and are usually put there using a Javascript thing.
|
* using the URL parameters (GET), and are usually put there using a Javascript thing.
|
||||||
@@ -294,7 +398,7 @@ class RuleController extends Controller
|
|||||||
$matcher->setLimit($limit);
|
$matcher->setLimit($limit);
|
||||||
$matcher->setRange($range);
|
$matcher->setRange($range);
|
||||||
$matcher->setTriggers($triggers);
|
$matcher->setTriggers($triggers);
|
||||||
$matchingTransactions = $matcher->findMatchingTransactions();
|
$matchingTransactions = $matcher->findTransactionsByTriggers();
|
||||||
|
|
||||||
// Warn the user if only a subset of transactions is returned
|
// Warn the user if only a subset of transactions is returned
|
||||||
$warning = '';
|
$warning = '';
|
||||||
|
@@ -178,7 +178,7 @@ class RuleGroupController extends Controller
|
|||||||
$this->dispatch($job);
|
$this->dispatch($job);
|
||||||
|
|
||||||
// Tell the user that the job is queued
|
// Tell the user that the job is queued
|
||||||
Session::flash('success', strval(trans('firefly.executed_group_on_existing_transactions', ['title' => $ruleGroup->title])));
|
Session::flash('success', strval(trans('firefly.applied_rule_group_selection', ['title' => $ruleGroup->title])));
|
||||||
|
|
||||||
return redirect()->route('rules.index');
|
return redirect()->route('rules.index');
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ class RuleGroupController extends Controller
|
|||||||
$checkedAccounts = array_keys($accountList);
|
$checkedAccounts = array_keys($accountList);
|
||||||
$first = session('first')->format('Y-m-d');
|
$first = session('first')->format('Y-m-d');
|
||||||
$today = Carbon::create()->format('Y-m-d');
|
$today = Carbon::create()->format('Y-m-d');
|
||||||
$subTitle = (string)trans('firefly.execute_on_existing_transactions');
|
$subTitle = (string)trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]);
|
||||||
|
|
||||||
return view('rules.rule-group.select-transactions', compact('checkedAccounts', 'accountList', 'first', 'today', 'ruleGroup', 'subTitle'));
|
return view('rules.rule-group.select-transactions', compact('checkedAccounts', 'accountList', 'first', 'today', 'ruleGroup', 'subTitle'));
|
||||||
}
|
}
|
||||||
|
161
app/Jobs/ExecuteRuleOnExistingTransactions.php
Normal file
161
app/Jobs/ExecuteRuleOnExistingTransactions.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ExecuteRuleOnExistingTransactions.php
|
||||||
|
* Copyright (c) 2017 thegrumpydictator@gmail.com
|
||||||
|
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||||
|
*
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FireflyIII\Jobs;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||||
|
use FireflyIII\Models\Rule;
|
||||||
|
use FireflyIII\Rules\Processor;
|
||||||
|
use FireflyIII\User;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ExecuteRuleOnExistingTransactions
|
||||||
|
*
|
||||||
|
* @package FireflyIII\Jobs
|
||||||
|
*/
|
||||||
|
class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
|
||||||
|
{
|
||||||
|
use InteractsWithQueue, SerializesModels;
|
||||||
|
|
||||||
|
/** @var Collection */
|
||||||
|
private $accounts;
|
||||||
|
/** @var Carbon */
|
||||||
|
private $endDate;
|
||||||
|
/** @var Rule */
|
||||||
|
private $rule;
|
||||||
|
/** @var Carbon */
|
||||||
|
private $startDate;
|
||||||
|
/** @var User */
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @param Rule $rule
|
||||||
|
*/
|
||||||
|
public function __construct(Rule $rule)
|
||||||
|
{
|
||||||
|
$this->rule = $rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function getAccounts(): Collection
|
||||||
|
{
|
||||||
|
return $this->accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Collection $accounts
|
||||||
|
*/
|
||||||
|
public function setAccounts(Collection $accounts)
|
||||||
|
{
|
||||||
|
$this->accounts = $accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Carbon\Carbon
|
||||||
|
*/
|
||||||
|
public function getEndDate(): Carbon
|
||||||
|
{
|
||||||
|
return $this->endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Carbon $date
|
||||||
|
*/
|
||||||
|
public function setEndDate(Carbon $date)
|
||||||
|
{
|
||||||
|
$this->endDate = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Carbon\Carbon
|
||||||
|
*/
|
||||||
|
public function getStartDate(): Carbon
|
||||||
|
{
|
||||||
|
return $this->startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Carbon $date
|
||||||
|
*/
|
||||||
|
public function setStartDate(Carbon $date)
|
||||||
|
{
|
||||||
|
$this->startDate = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
*/
|
||||||
|
public function setUser(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// Lookup all journals that match the parameters specified
|
||||||
|
$transactions = $this->collectJournals();
|
||||||
|
$processor = Processor::make($this->rule);
|
||||||
|
|
||||||
|
// Execute the rules for each transaction
|
||||||
|
foreach ($transactions as $transaction) {
|
||||||
|
|
||||||
|
$processor->handleTransaction($transaction);
|
||||||
|
|
||||||
|
// Stop processing this group if the rule specifies 'stop_processing'
|
||||||
|
if ($processor->getRule()->stop_processing) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all journals that should be processed
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
protected function collectJournals()
|
||||||
|
{
|
||||||
|
/** @var JournalCollectorInterface $collector */
|
||||||
|
$collector = app(JournalCollectorInterface::class);
|
||||||
|
$collector->setUser($this->user);
|
||||||
|
$collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate);
|
||||||
|
|
||||||
|
return $collector->getJournals();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -265,7 +265,7 @@ class RuleRepository implements RuleRepositoryInterface
|
|||||||
$ruleAction->active = 1;
|
$ruleAction->active = 1;
|
||||||
$ruleAction->stop_processing = $values['stopProcessing'];
|
$ruleAction->stop_processing = $values['stopProcessing'];
|
||||||
$ruleAction->action_type = $values['action'];
|
$ruleAction->action_type = $values['action'];
|
||||||
$ruleAction->action_value = $values['value'];
|
$ruleAction->action_value = is_null($values['value']) ? '' : $values['value'];
|
||||||
$ruleAction->save();
|
$ruleAction->save();
|
||||||
|
|
||||||
|
|
||||||
|
@@ -59,9 +59,11 @@ final class Processor
|
|||||||
*
|
*
|
||||||
* @param Rule $rule
|
* @param Rule $rule
|
||||||
*
|
*
|
||||||
|
* @param bool $includeActions
|
||||||
|
*
|
||||||
* @return Processor
|
* @return Processor
|
||||||
*/
|
*/
|
||||||
public static function make(Rule $rule)
|
public static function make(Rule $rule, $includeActions = true)
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('Making new rule from Rule %d', $rule->id));
|
Log::debug(sprintf('Making new rule from Rule %d', $rule->id));
|
||||||
$self = new self;
|
$self = new self;
|
||||||
@@ -72,7 +74,9 @@ final class Processor
|
|||||||
Log::debug(sprintf('Push trigger %d', $trigger->id));
|
Log::debug(sprintf('Push trigger %d', $trigger->id));
|
||||||
$self->triggers->push(TriggerFactory::getTrigger($trigger));
|
$self->triggers->push(TriggerFactory::getTrigger($trigger));
|
||||||
}
|
}
|
||||||
|
if ($includeActions) {
|
||||||
$self->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get();
|
$self->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get();
|
||||||
|
}
|
||||||
|
|
||||||
return $self;
|
return $self;
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ declare(strict_types=1);
|
|||||||
namespace FireflyIII\Rules;
|
namespace FireflyIII\Rules;
|
||||||
|
|
||||||
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
|
||||||
|
use FireflyIII\Models\Rule;
|
||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
use FireflyIII\Models\TransactionType;
|
use FireflyIII\Models\TransactionType;
|
||||||
use FireflyIII\Repositories\Journal\JournalTaskerInterface;
|
use FireflyIII\Repositories\Journal\JournalTaskerInterface;
|
||||||
@@ -32,6 +33,8 @@ class TransactionMatcher
|
|||||||
private $limit = 10;
|
private $limit = 10;
|
||||||
/** @var int Maximum number of transaction to search in (for performance reasons) * */
|
/** @var int Maximum number of transaction to search in (for performance reasons) * */
|
||||||
private $range = 200;
|
private $range = 200;
|
||||||
|
/** @var Rule */
|
||||||
|
private $rule;
|
||||||
/** @var JournalTaskerInterface */
|
/** @var JournalTaskerInterface */
|
||||||
private $tasker;
|
private $tasker;
|
||||||
/** @var array */
|
/** @var array */
|
||||||
@@ -50,6 +53,31 @@ class TransactionMatcher
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will search the user's transaction journal (with an upper limit of $range) for
|
||||||
|
* transaction journals matching the given rule. This is accomplished by trying to fire these
|
||||||
|
* triggers onto each transaction journal until enough matches are found ($limit).
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function findTransactionsByRule()
|
||||||
|
{
|
||||||
|
if (count($this->rule->ruleTriggers) === 0) {
|
||||||
|
return new Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables used within the loop
|
||||||
|
$processor = Processor::make($this->rule, false);
|
||||||
|
$result = $this->runProcessor($processor);
|
||||||
|
|
||||||
|
// If the list of matchingTransactions is larger than the maximum number of results
|
||||||
|
// (e.g. if a large percentage of the transactions match), truncate the list
|
||||||
|
$result = $result->slice(0, $this->limit);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will search the user's transaction journal (with an upper limit of $range) for
|
* This method will search the user's transaction journal (with an upper limit of $range) for
|
||||||
* transaction journals matching the given $triggers. This is accomplished by trying to fire these
|
* transaction journals matching the given $triggers. This is accomplished by trying to fire these
|
||||||
@@ -58,64 +86,15 @@ class TransactionMatcher
|
|||||||
* @return Collection
|
* @return Collection
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function findMatchingTransactions(): Collection
|
public function findTransactionsByTriggers(): Collection
|
||||||
{
|
{
|
||||||
if (count($this->triggers) === 0) {
|
if (count($this->triggers) === 0) {
|
||||||
return new Collection;
|
return new Collection;
|
||||||
}
|
}
|
||||||
$pageSize = min($this->range / 2, $this->limit * 2);
|
|
||||||
|
|
||||||
// Variables used within the loop
|
// Variables used within the loop
|
||||||
$processed = 0;
|
|
||||||
$page = 1;
|
|
||||||
$result = new Collection();
|
|
||||||
$processor = Processor::makeFromStringArray($this->triggers);
|
$processor = Processor::makeFromStringArray($this->triggers);
|
||||||
|
$result = $this->runProcessor($processor);
|
||||||
// Start a loop to fetch batches of transactions. The loop will finish if:
|
|
||||||
// - all transactions have been fetched from the database
|
|
||||||
// - the maximum number of transactions to return has been found
|
|
||||||
// - the maximum number of transactions to search in have been searched
|
|
||||||
do {
|
|
||||||
// Fetch a batch of transactions from the database
|
|
||||||
/** @var JournalCollectorInterface $collector */
|
|
||||||
$collector = app(JournalCollectorInterface::class);
|
|
||||||
$collector->setUser(auth()->user());
|
|
||||||
$collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTypes($this->transactionTypes);
|
|
||||||
$set = $collector->getPaginatedJournals();
|
|
||||||
Log::debug(sprintf('Found %d journals to check. ', $set->count()));
|
|
||||||
|
|
||||||
// Filter transactions that match the given triggers.
|
|
||||||
$filtered = $set->filter(
|
|
||||||
function (Transaction $transaction) use ($processor) {
|
|
||||||
Log::debug(sprintf('Test these triggers on journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id));
|
|
||||||
|
|
||||||
return $processor->handleTransaction($transaction);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Log::debug(sprintf('Found %d journals that match.', $filtered->count()));
|
|
||||||
|
|
||||||
// merge:
|
|
||||||
/** @var Collection $result */
|
|
||||||
$result = $result->merge($filtered);
|
|
||||||
Log::debug(sprintf('Total count is now %d', $result->count()));
|
|
||||||
|
|
||||||
// Update counters
|
|
||||||
$page++;
|
|
||||||
$processed += count($set);
|
|
||||||
|
|
||||||
Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed));
|
|
||||||
|
|
||||||
// Check for conditions to finish the loop
|
|
||||||
$reachedEndOfList = $set->count() < 1;
|
|
||||||
$foundEnough = $result->count() >= $this->limit;
|
|
||||||
$searchedEnough = ($processed >= $this->range);
|
|
||||||
|
|
||||||
Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true)));
|
|
||||||
Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true)));
|
|
||||||
Log::debug(sprintf('searchedEnough: %s', var_export($searchedEnough, true)));
|
|
||||||
|
|
||||||
} while (!$reachedEndOfList && !$foundEnough && !$searchedEnough);
|
|
||||||
|
|
||||||
// If the list of matchingTransactions is larger than the maximum number of results
|
// If the list of matchingTransactions is larger than the maximum number of results
|
||||||
// (e.g. if a large percentage of the transactions match), truncate the list
|
// (e.g. if a large percentage of the transactions match), truncate the list
|
||||||
@@ -185,5 +164,73 @@ class TransactionMatcher
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Rule $rule
|
||||||
|
*/
|
||||||
|
public function setRule(Rule $rule)
|
||||||
|
{
|
||||||
|
$this->rule = $rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Processor $processor
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
private function runProcessor(Processor $processor): Collection
|
||||||
|
{
|
||||||
|
// Start a loop to fetch batches of transactions. The loop will finish if:
|
||||||
|
// - all transactions have been fetched from the database
|
||||||
|
// - the maximum number of transactions to return has been found
|
||||||
|
// - the maximum number of transactions to search in have been searched
|
||||||
|
$pageSize = min($this->range / 2, $this->limit * 2);
|
||||||
|
$processed = 0;
|
||||||
|
$page = 1;
|
||||||
|
$result = new Collection();
|
||||||
|
do {
|
||||||
|
// Fetch a batch of transactions from the database
|
||||||
|
/** @var JournalCollectorInterface $collector */
|
||||||
|
$collector = app(JournalCollectorInterface::class);
|
||||||
|
$collector->setUser(auth()->user());
|
||||||
|
$collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTypes($this->transactionTypes);
|
||||||
|
$set = $collector->getPaginatedJournals();
|
||||||
|
Log::debug(sprintf('Found %d journals to check. ', $set->count()));
|
||||||
|
|
||||||
|
// Filter transactions that match the given triggers.
|
||||||
|
$filtered = $set->filter(
|
||||||
|
function (Transaction $transaction) use ($processor) {
|
||||||
|
Log::debug(sprintf('Test these triggers on journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id));
|
||||||
|
|
||||||
|
return $processor->handleTransaction($transaction);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::debug(sprintf('Found %d journals that match.', $filtered->count()));
|
||||||
|
|
||||||
|
// merge:
|
||||||
|
/** @var Collection $result */
|
||||||
|
$result = $result->merge($filtered);
|
||||||
|
Log::debug(sprintf('Total count is now %d', $result->count()));
|
||||||
|
|
||||||
|
// Update counters
|
||||||
|
$page++;
|
||||||
|
$processed += count($set);
|
||||||
|
|
||||||
|
Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed));
|
||||||
|
|
||||||
|
// Check for conditions to finish the loop
|
||||||
|
$reachedEndOfList = $set->count() < 1;
|
||||||
|
$foundEnough = $result->count() >= $this->limit;
|
||||||
|
$searchedEnough = ($processed >= $this->range);
|
||||||
|
|
||||||
|
Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true)));
|
||||||
|
Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true)));
|
||||||
|
Log::debug(sprintf('searchedEnough: %s', var_export($searchedEnough, true)));
|
||||||
|
|
||||||
|
} while (!$reachedEndOfList && !$foundEnough && !$searchedEnough);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -37,9 +37,40 @@ $(function () {
|
|||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// test rule triggers button:
|
||||||
|
$('.test_rule_triggers').click(testRuleTriggers);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function testRuleTriggers(e) {
|
||||||
|
var obj = $(e.target);
|
||||||
|
var ruleId = parseInt(obj.data('id'));
|
||||||
|
|
||||||
|
// Find a list of existing transactions that match these triggers
|
||||||
|
$.get('rules/test-rule/' + ruleId).done(function (data) {
|
||||||
|
var modal = $("#testTriggerModal");
|
||||||
|
|
||||||
|
// Set title and body
|
||||||
|
modal.find(".transactions-list").html(data.html);
|
||||||
|
|
||||||
|
// Show warning if appropriate
|
||||||
|
if (data.warning) {
|
||||||
|
modal.find(".transaction-warning .warning-contents").text(data.warning);
|
||||||
|
modal.find(".transaction-warning").show();
|
||||||
|
} else {
|
||||||
|
modal.find(".transaction-warning").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal dialog
|
||||||
|
modal.modal();
|
||||||
|
}).fail(function () {
|
||||||
|
alert('Cannot get transactions for given triggers.');
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function sortStop(event, ui) {
|
function sortStop(event, ui) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
@@ -205,7 +205,6 @@ return [
|
|||||||
|
|
||||||
// rules
|
// rules
|
||||||
'rules' => 'Rules',
|
'rules' => 'Rules',
|
||||||
'rules_explanation' => 'Here you can manage rules. Rules are triggered when a transaction is created or updated. Then, if the transaction has certain properties (called "triggers") Firefly will execute the "actions". Combined, you can make Firefly respond in a certain way to new transactions.',
|
|
||||||
'rule_name' => 'Name of rule',
|
'rule_name' => 'Name of rule',
|
||||||
'rule_triggers' => 'Rule triggers when',
|
'rule_triggers' => 'Rule triggers when',
|
||||||
'rule_actions' => 'Rule will',
|
'rule_actions' => 'Rule will',
|
||||||
@@ -255,14 +254,14 @@ return [
|
|||||||
'warning_transaction_subset' => 'For performance reasons this list is limited to :max_num_transactions and may only show a subset of matching transactions',
|
'warning_transaction_subset' => 'For performance reasons this list is limited to :max_num_transactions and may only show a subset of matching transactions',
|
||||||
'warning_no_matching_transactions' => 'No matching transactions found. Please note that for performance reasons, only the last :num_transactions transactions have been checked.',
|
'warning_no_matching_transactions' => 'No matching transactions found. Please note that for performance reasons, only the last :num_transactions transactions have been checked.',
|
||||||
'warning_no_valid_triggers' => 'No valid triggers provided.',
|
'warning_no_valid_triggers' => 'No valid triggers provided.',
|
||||||
'execute_on_existing_transactions' => 'Execute for existing transactions',
|
'apply_rule_selection' => 'Apply rule ":title" to a selection of your transactions',
|
||||||
'rule_group_select_transactions' => 'Execute rule group ":title" on existing transactions',
|
'apply_rule_selection_intro' => 'Rules like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run it on a selection of your existing transactions. This can be useful when you have updated a rule and you need the changes to be applied to all of your other transactions.',
|
||||||
'execute_on_existing_transactions_intro' => 'When a rule or group has been changed or added, you can execute it for existing transactions',
|
|
||||||
'execute_on_existing_transactions_short' => 'Existing transactions',
|
|
||||||
'executed_group_on_existing_transactions' => 'Executed group ":title" for existing transactions',
|
|
||||||
'execute_group_on_existing_transactions' => 'Execute group ":title" for existing transactions',
|
|
||||||
'include_transactions_from_accounts' => 'Include transactions from these accounts',
|
'include_transactions_from_accounts' => 'Include transactions from these accounts',
|
||||||
|
'applied_rule_selection' => 'Rule ":title" has been applied to your selection.',
|
||||||
'execute' => 'Execute',
|
'execute' => 'Execute',
|
||||||
|
'apply_rule_group_selection' => 'Apply rule group ":title" to a selection of your transactions',
|
||||||
|
'apply_rule_group_selection_intro' => 'Rule groups like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run all the rules in this group on a selection of your existing transactions. This can be useful when you have updated a group of rules and you need the changes to be applied to all of your other transactions.',
|
||||||
|
'applied_rule_group_selection' => 'Rule ":title" has been applied to your selection.',
|
||||||
|
|
||||||
// actions and triggers
|
// actions and triggers
|
||||||
'rule_trigger_user_action' => 'User action is ":trigger_value"',
|
'rule_trigger_user_action' => 'User action is ":trigger_value"',
|
||||||
|
@@ -6,18 +6,11 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 col-sm-12">
|
<div class="col-lg-12 col-md-12 col-sm-12">
|
||||||
<div class="box box-primary">
|
|
||||||
<div class="box-header with-border">
|
|
||||||
<h3 class="box-title">{{ 'rules'|_ }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<p>
|
<p>
|
||||||
{{ 'rules_explanation'|_ }}
|
<a href="{{ route('rule-groups.create') }}" class="btn btn-success">{{ 'new_rule_group'|_ }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for ruleGroup in ruleGroups %}
|
{% for ruleGroup in ruleGroups %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -38,12 +31,11 @@
|
|||||||
<button class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown"><i
|
<button class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown"><i
|
||||||
class="fa fa-ellipsis-v"></i></button>
|
class="fa fa-ellipsis-v"></i></button>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li><a href="{{ route('rule-groups.edit',ruleGroup.id) }}"><i
|
<li><a href="{{ route('rule-groups.edit',ruleGroup.id) }}"><i class="fa fa-fw fa-pencil"></i> {{ 'edit'|_ }}</a></li>
|
||||||
class="fa fa-fw fa-pencil"></i> {{ 'edit'|_ }}</a></li>
|
<li><a href="{{ route('rule-groups.delete',ruleGroup.id) }}"><i class="fa fa-fw fa-trash"></i> {{ 'delete'|_ }}</a></li>
|
||||||
<li><a href="{{ route('rule-groups.delete',ruleGroup.id) }}"><i
|
|
||||||
class="fa fa-fw fa-trash"></i> {{ 'delete'|_ }}</a></li>
|
|
||||||
<li><a href="{{ route('rule-groups.select-transactions',ruleGroup.id) }}"><i
|
<li><a href="{{ route('rule-groups.select-transactions',ruleGroup.id) }}"><i
|
||||||
class="fa fa-fw fa-anchor"></i> {{ 'execute_on_existing_transactions_short'|_ }}</a></li>
|
class="fa fa-fw fa-power-off"></i> {{ trans('firefly.apply_rule_group_selection', {title: ruleGroup.title}) }}
|
||||||
|
</a></li>
|
||||||
{% if ruleGroup.order > 1 %}
|
{% if ruleGroup.order > 1 %}
|
||||||
<li><a href="{{ route('rule-groups.up',ruleGroup.id) }}"><i
|
<li><a href="{{ route('rule-groups.up',ruleGroup.id) }}"><i
|
||||||
class="fa fa-fw fa-arrow-up"></i> {{ 'move_rule_group_up'|_ }}</a></li>
|
class="fa fa-fw fa-arrow-up"></i> {{ 'move_rule_group_up'|_ }}</a></li>
|
||||||
@@ -67,7 +59,10 @@
|
|||||||
<table class="table table-hover table-striped">
|
<table class="table table-hover table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2">{{ 'rule_name'|_ }}</th>
|
<th> </th>
|
||||||
|
<th> </th>
|
||||||
|
<th> </th>
|
||||||
|
<th>{{ 'rule_name'|_ }}</th>
|
||||||
<th class="hidden-xs">{{ 'rule_triggers'|_ }}</th>
|
<th class="hidden-xs">{{ 'rule_triggers'|_ }}</th>
|
||||||
<th class="hidden-xs">{{ 'rule_actions'|_ }}</th>
|
<th class="hidden-xs">{{ 'rule_actions'|_ }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -96,6 +91,10 @@
|
|||||||
<a href="#" class="btn btn-default"><span
|
<a href="#" class="btn btn-default"><span
|
||||||
class="fa fa-fw"></span></a>
|
class="fa fa-fw"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-xs">
|
||||||
<a title="{{ 'edit'|_ }}" href="{{ route('rules.edit', rule.id) }}"
|
<a title="{{ 'edit'|_ }}" href="{{ route('rules.edit', rule.id) }}"
|
||||||
class="btn btn-default"><span
|
class="btn btn-default"><span
|
||||||
class="fa fa-fw fa-pencil"></span></a>
|
class="fa fa-fw fa-pencil"></span></a>
|
||||||
@@ -104,13 +103,18 @@
|
|||||||
class="btn btn-danger"><span
|
class="btn btn-danger"><span
|
||||||
class="fa fa-fw fa-trash"></span></a>
|
class="fa fa-fw fa-trash"></span></a>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
</td>
|
||||||
|
<td>
|
||||||
<div class="btn-group btn-group-xs">
|
<div class="btn-group btn-group-xs">
|
||||||
<a href="{{ route('rule-groups.select-transactions',ruleGroup.id) }}" class="btn btn-default"
|
{# show which transactions would match #}
|
||||||
title=" {{ 'execute_on_existing_transactions_short'|_ }}">
|
<a href="#" class="btn btn-default test_rule_triggers" data-id="{{ rule.id }}"
|
||||||
<i class="fa fa-fw fa-check-circle"></i></a>
|
title="{{ 'test_rule_triggers'|_ }}"><i data-id="{{ rule.id }}" class="fa fa-fw fa-flask"></i></a>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{# actually execute rule #}
|
||||||
|
<a href="{{ route('rules.select-transactions',ruleGroup.id) }}" class="btn btn-default"
|
||||||
|
title=" {{ trans('firefly.apply_rule_selection', {title: rule.title}) }}">
|
||||||
|
<i class="fa fa-fw fa-power-off "></i></a>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if rule.active %}
|
{% if rule.active %}
|
||||||
@@ -188,6 +192,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% include '/rules/partials/test-trigger-modal' %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 col-sm-12">
|
<div class="col-lg-12 col-md-12 col-sm-12">
|
||||||
<a href="{{ route('rule-groups.create') }}" class="btn btn-success">{{ 'new_rule_group'|_ }}</a>
|
<a href="{{ route('rule-groups.create') }}" class="btn btn-success">{{ 'new_rule_group'|_ }}</a>
|
||||||
|
@@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">{{ 'execute_on_existing_transactions'|_ }}</h3>
|
<h3 class="box-title">{{ subTitle }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div id="form-body">
|
<div id="form-body">
|
||||||
<p>
|
<p>
|
||||||
{{ 'execute_on_existing_transactions_intro'|_ }}
|
{{ trans('firefly.apply_rule_group_selection_intro', {title: ruleGroup.title}) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-12">
|
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-12">
|
||||||
|
@@ -62,7 +62,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="#" class="btn btn-default add_rule_trigger">{{ 'add_rule_trigger'|_ }}</a>
|
<a href="#" class="btn btn-default add_rule_trigger">{{ 'add_rule_trigger'|_ }}</a>
|
||||||
<a href="#" class="btn btn-default test_rule_triggers">{{ 'test_rule_triggers'|_ }}</a>
|
<a href="#" class="btn btn-default test_rule_triggers"><i class="fa fa-flask"></i> {{ 'test_rule_triggers'|_ }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
52
resources/views/rules/rule/select-transactions.twig
Normal file
52
resources/views/rules/rule/select-transactions.twig
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends "./layout/default" %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, rule) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('rules.execute', rule.id) }}" accept-charset="UTF-8" class="form-horizontal" id="execute-rule">
|
||||||
|
<input name="_token" type="hidden" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-sm-12 col-xs-12">
|
||||||
|
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">{{ subTitle }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div id="form-body">
|
||||||
|
<p>
|
||||||
|
{{ trans('firefly.apply_rule_selection_intro', {title: rule.title}) }}
|
||||||
|
</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 col-md-8 col-sm-12 col-xs-12">
|
||||||
|
{{ ExpandedForm.date('start_date', first) }}
|
||||||
|
{{ ExpandedForm.date('end_date', today) }}
|
||||||
|
|
||||||
|
<!-- ACCOUNTS -->
|
||||||
|
{{ ExpandedForm.multiCheckbox('accounts',accountList, checkedAccounts, {' class': 'account-checkbox', 'label': trans('firefly.include_transactions_from_accounts') }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-footer">
|
||||||
|
<input type="submit" name="submit" value="{{ 'execute'|_ }}" id="do-execute-button" class="btn btn-success pull-right"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript" src="js/lib/modernizr-custom.js"></script>
|
||||||
|
<script type="text/javascript" src="js/lib/jquery-ui.min.js"></script>
|
||||||
|
<script type="text/javascript" src="js/ff/rules/select-transactions.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link href="css/jquery-ui/jquery-ui.structure.min.css" type="text/css" rel="stylesheet" media="all">
|
||||||
|
<link href="css/jquery-ui/jquery-ui.theme.min.css" type="text/css" rel="stylesheet" media="all">
|
||||||
|
{% endblock %}
|
@@ -608,12 +608,15 @@ Route::group(
|
|||||||
Route::get('edit/{rule}', ['uses' => 'RuleController@edit', 'as' => 'edit']);
|
Route::get('edit/{rule}', ['uses' => 'RuleController@edit', 'as' => 'edit']);
|
||||||
Route::get('delete/{rule}', ['uses' => 'RuleController@delete', 'as' => 'delete']);
|
Route::get('delete/{rule}', ['uses' => 'RuleController@delete', 'as' => 'delete']);
|
||||||
Route::get('test', ['uses' => 'RuleController@testTriggers', 'as' => 'test-triggers']);
|
Route::get('test', ['uses' => 'RuleController@testTriggers', 'as' => 'test-triggers']);
|
||||||
|
Route::get('test-rule/{rule}', ['uses' => 'RuleController@testTriggersByRule', 'as' => 'test-triggers-rule']);
|
||||||
|
Route::get('select/{rule}', ['uses' => 'RuleController@selectTransactions', 'as' => 'select-transactions']);
|
||||||
|
|
||||||
Route::post('trigger/order/{rule}', ['uses' => 'RuleController@reorderRuleTriggers', 'as' => 'reorder-triggers']);
|
Route::post('trigger/order/{rule}', ['uses' => 'RuleController@reorderRuleTriggers', 'as' => 'reorder-triggers']);
|
||||||
Route::post('action/order/{rule}', ['uses' => 'RuleController@reorderRuleActions', 'as' => 'reorder-actions']);
|
Route::post('action/order/{rule}', ['uses' => 'RuleController@reorderRuleActions', 'as' => 'reorder-actions']);
|
||||||
Route::post('store/{ruleGroup}', ['uses' => 'RuleController@store', 'as' => 'store']);
|
Route::post('store/{ruleGroup}', ['uses' => 'RuleController@store', 'as' => 'store']);
|
||||||
Route::post('update/{rule}', ['uses' => 'RuleController@update', 'as' => 'update']);
|
Route::post('update/{rule}', ['uses' => 'RuleController@update', 'as' => 'update']);
|
||||||
Route::post('destroy/{rule}', ['uses' => 'RuleController@destroy', 'as' => 'destroy']);
|
Route::post('destroy/{rule}', ['uses' => 'RuleController@destroy', 'as' => 'destroy']);
|
||||||
|
Route::post('execute/{rule}', ['uses' => 'RuleController@execute', 'as' => 'execute']);
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user