Allow rule to be applied to transactions (not just group).

This commit is contained in:
James Cole
2017-07-16 13:04:45 +02:00
parent b676b1fef9
commit 09f838089b
13 changed files with 495 additions and 88 deletions

View File

@@ -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 = '';

View File

@@ -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'));
} }

View 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();
}
}

View File

@@ -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();

View File

@@ -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));
} }
$self->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); if ($includeActions) {
$self->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get();
}
return $self; return $self;
} }

View File

@@ -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;
}
} }

View File

@@ -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";

View File

@@ -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"',

View File

@@ -6,16 +6,9 @@
<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"> <p>
<div class="box-header with-border"> <a href="{{ route('rule-groups.create') }}" class="btn btn-success">{{ 'new_rule_group'|_ }}</a>
<h3 class="box-title">{{ 'rules'|_ }}</h3> </p>
</div>
<div class="box-body">
<p>
{{ 'rules_explanation'|_ }}
</p>
</div>
</div>
</div> </div>
</div> </div>
@@ -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>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</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>

View File

@@ -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">

View File

@@ -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>

View 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 %}

View File

@@ -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']);
} }
); );