Lots of new code for recurring transactions. #1469

This commit is contained in:
James Cole 2018-06-16 21:47:51 +02:00
parent 968abd26e8
commit 1cf91c78f8
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
19 changed files with 769 additions and 79 deletions

View File

@ -0,0 +1,124 @@
<?php
/**
* RecurrenceFactory.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Factory;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Services\Internal\Support\TransactionServiceTrait;
use FireflyIII\Services\Internal\Support\TransactionTypeTrait;
use FireflyIII\User;
/**
* Class RecurrenceFactory
*/
class RecurrenceFactory
{
use TransactionTypeTrait, TransactionServiceTrait;
/** @var User */
private $user;
/**
* @param array $data
*
* @throws FireflyException
* @return Recurrence
*/
public function create(array $data): Recurrence
{
echo '<pre>';
print_r($data);
echo '</pre>';
$type = $this->findTransactionType(ucfirst($data['recurrence']['type']));
$recurrence = new Recurrence(
[
'user_id' => $this->user->id,
'transaction_type_id' => $type->id,
'title' => $data['recurrence']['title'],
'description' => $data['recurrence']['description'],
'first_date' => $data['recurrence']['first_date']->format('Y-m-d'),
'repeat_until' => $data['recurrence']['repeat_until'],
'latest_date' => null,
'repetitions' => $data['recurrence']['repetitions'],
'apply_rules' => $data['recurrence']['apply_rules'],
'active' => $data['recurrence']['active'],
]
);
$recurrence->save();
var_dump($recurrence->toArray());
// create transactions
foreach ($data['transactions'] as $trArray) {
$source = null;
$destination = null;
// search source account, depends on type
switch ($type->type) {
default:
throw new FireflyException(sprintf('Cannot create "%s".', $type->type));
case TransactionType::WITHDRAWAL:
$source = $this->findAccount(AccountType::ASSET, $trArray['source_account_id'], null);
$destination = $this->findAccount(AccountType::EXPENSE, null, $trArray['destination_account_name']);
break;
}
// search destination account
$transaction = new RecurrenceTransaction(
[
'recurrence_id' => $recurrence->id,
'transaction_currency_id' => $trArray['transaction_currency_id'],
'foreign_currency_id' => '' === (string)$trArray['foreign_amount'] ? null : $trArray['foreign_currency_id'],
'source_account_id' => $source->id,
'destination_account_id' => $destination->id,
'amount' => $trArray['amount'],
'foreign_amount' => '' === (string)$trArray['foreign_amount'] ? null : (string)$trArray['foreign_amount'],
'description' => $trArray['description'],
]
);
$transaction->save();
var_dump($transaction->toArray());
}
// create meta data:
if(\count($data['meta']['tags']) > 0) {
// todo store tags
}
exit;
}
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
}

View File

@ -28,6 +28,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Services\Internal\Support\JournalServiceTrait;
use FireflyIII\Services\Internal\Support\TransactionTypeTrait;
use FireflyIII\User;
use Log;
@ -36,7 +37,7 @@ use Log;
*/
class TransactionJournalFactory
{
use JournalServiceTrait;
use JournalServiceTrait, TransactionTypeTrait;
/** @var User */
private $user;
@ -137,25 +138,4 @@ class TransactionJournalFactory
}
}
/**
* Get the transaction type. Since this is mandatory, will throw an exception when nothing comes up. Will always
* use TransactionType repository.
*
* @param string $type
*
* @return TransactionType
* @throws FireflyException
*/
protected function findTransactionType(string $type): TransactionType
{
$factory = app(TransactionTypeFactory::class);
$transactionType = $factory->find($type);
if (null === $transactionType) {
Log::error(sprintf('Could not find transaction type for "%s"', $type)); // @codeCoverageIgnore
throw new FireflyException(sprintf('Could not find transaction type for "%s"', $type)); // @codeCoverageIgnore
}
return $transactionType;
}
}

View File

@ -26,7 +26,9 @@ namespace FireflyIII\Http\Controllers\Recurring;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\RecurrenceFormRequest;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use Illuminate\Http\Request;
@ -38,6 +40,8 @@ class CreateController extends Controller
{
/** @var BudgetRepositoryInterface */
private $budgets;
/** @var PiggyBankRepositoryInterface */
private $piggyBanks;
/** @var RecurringRepositoryInterface */
private $recurring;
@ -55,8 +59,9 @@ class CreateController extends Controller
app('view')->share('title', trans('firefly.recurrences'));
app('view')->share('subTitle', trans('firefly.create_new_recurrence'));
$this->recurring = app(RecurringRepositoryInterface::class);
$this->budgets = app(BudgetRepositoryInterface::class);
$this->recurring = app(RecurringRepositoryInterface::class);
$this->budgets = app(BudgetRepositoryInterface::class);
$this->piggyBanks = app(PiggyBankRepositoryInterface::class);
return $next($request);
}
@ -64,6 +69,8 @@ class CreateController extends Controller
}
/**
* @param Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function create(Request $request)
@ -71,6 +78,8 @@ class CreateController extends Controller
// todo refactor to expandedform method.
$budgets = app('expandedform')->makeSelectListWithEmpty($this->budgets->getActiveBudgets());
$defaultCurrency = app('amount')->getDefaultCurrency();
$piggyBanks = $this->piggyBanks->getPiggyBanksWithAmount();
$piggies = app('expandedform')->makeSelectListWithEmpty($piggyBanks);
$tomorrow = new Carbon;
$tomorrow->addDay();
@ -90,7 +99,18 @@ class CreateController extends Controller
];
$request->session()->flash('preFilled', $preFilled);
return view('recurring.create', compact('tomorrow', 'preFilled','typesOfRepetitions', 'defaultCurrency', 'budgets'));
return view('recurring.create', compact('tomorrow', 'preFilled', 'piggies', 'typesOfRepetitions', 'defaultCurrency', 'budgets'));
}
/**
* @param RecurrenceFormRequest $request
*/
public function store(RecurrenceFormRequest $request)
{
$data = $request->getAll();
$this->recurring->store($data);
var_dump($data);
exit;
}
}

View File

@ -76,9 +76,20 @@ class IndexController extends Controller
$return = [];
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
$firstDate = Carbon::createFromFormat('Y-m-d', $request->get('first_date'));
$endDate = '' !== (string)$request->get('end_date') ? Carbon::createFromFormat('Y-m-d', $request->get('end_date')) : null;
$endsAt = (string)$request->get('ends');
$repetitionType = explode(',', $request->get('type'))[0];
$repetitions = (int)$request->get('reps');
$repetitionMoment = '';
$start->startOfDay();
// if $firstDate is beyond $end, simply return an empty array.
if ($firstDate->gt($end)) {
return Response::json([]);
}
// if $firstDate is beyond start, use that one:
$actualStart = clone $firstDate;
switch ($repetitionType) {
default:
@ -90,32 +101,51 @@ class IndexController extends Controller
$repetitionMoment = explode(',', $request->get('type'))[1] ?? '1';
break;
case 'ndom':
$repetitionMoment = explode(',', $request->get('type'))[1] ?? '1,1';
$repetitionMoment = str_ireplace('ndom,', '', $request->get('type'));
break;
case 'yearly':
$repetitionMoment = explode(',', $request->get('type'))[1] ?? '2018-01-01';
break;
}
$repetition = new RecurrenceRepetition;
$repetition->repetition_type = $repetitionType;
$repetition->repetition_moment = $repetitionMoment;
$repetition->repetition_skip = (int)$request->get('skip');
var_dump($repository->getXOccurrences($repetition, $start, 5));
exit;
// calculate events in range, depending on type:
$actualEnd = clone $end;
switch ($endsAt) {
default:
throw new FireflyException(sprintf('Cannot generate events for "%s"', $endsAt));
throw new FireflyException(sprintf('Cannot generate events for type that ends at "%s".', $endsAt));
case 'forever':
// simply generate up until $end. No change from default behavior.
$occurrences = $repository->getOccurrencesInRange($repetition, $actualStart, $actualEnd);
break;
case 'until_date':
$actualEnd = $endDate ?? clone $end;
$occurrences = $repository->getOccurrencesInRange($repetition, $actualStart, $actualEnd);
break;
case 'times':
$occurrences = $repository->getXOccurrences($repetition, $actualStart, $repetitions);
break;
}
/** @var Carbon $current */
foreach ($occurrences as $current) {
if ($current->gte($start)) {
$event = [
'id' => $repetitionType . $firstDate->format('Ymd'),
'title' => 'X',
'allDay' => true,
'start' => $current->format('Y-m-d'),
'end' => $current->format('Y-m-d'),
'editable' => false,
'rendering' => 'background',
];
$return[] = $event;
}
}
return Response::json($return);
}

View File

@ -37,6 +37,7 @@ use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Log;
use Preferences;
@ -218,7 +219,7 @@ class SingleController extends Controller
*
* @internal param JournalRepositoryInterface $repository
*/
public function destroy(TransactionJournal $transactionJournal)
public function destroy(TransactionJournal $transactionJournal): RedirectResponse
{
// @codeCoverageIgnoreStart
if ($this->isOpeningBalance($transactionJournal)) {
@ -329,9 +330,10 @@ class SingleController extends Controller
* @param JournalFormRequest $request
* @param JournalRepositoryInterface $repository
*
* @return \Illuminate\Http\RedirectResponse
* @return RedirectResponse
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function store(JournalFormRequest $request, JournalRepositoryInterface $repository)
public function store(JournalFormRequest $request, JournalRepositoryInterface $repository): RedirectResponse
{
$doSplit = 1 === (int)$request->get('split_journal');
$createAnother = 1 === (int)$request->get('create_another');

View File

@ -0,0 +1,189 @@
<?php
/**
* RecurrenceFormRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionType;
use FireflyIII\Rules\ValidRecurrenceRepetitionType;
/**
* Class RecurrenceFormRequest
*/
class RecurrenceFormRequest extends Request
{
/**
* @return bool
*/
public function authorize(): bool
{
// Only allow logged in users
return auth()->check();
}
/**
* @return array
* @throws FireflyException
*/
public function getAll(): array
{
$data = $this->all();
$return = [
'recurrence' => [
'type' => $this->string('transaction_type'),
'title' => $this->string('title'),
'description' => $this->string('recurring_description'),
'first_date' => $this->date('first_date'),
'repeat_until' => $this->date('repeat_until'),
'repetitions' => $this->integer('repetitions'),
'apply_rules' => $this->boolean('apply_rules'),
'active' => $this->boolean('active'),
],
'transactions' => [
[
'transaction_currency_id' => $this->integer('transaction_currency_id'),
'type' => $this->string('transaction_type'),
'description' => $this->string('transaction_description'),
'amount' => $this->string('amount'),
'foreign_amount' => null,
'foreign_currency_id' => null,
'budget_id' => $this->integer('budget_id'),
'category_name' => $this->string('category'),
],
],
'meta' => [
// tags and piggy bank ID.
'tags' => explode(',', $this->string('tags')),
'piggy_bank_id' => $this->integer('piggy_bank_id'),
],
'repetitions' => [
[
'skip' => $this->integer('skip'),
],
],
];
// fill in foreign currency data
if (null !== $this->float('foreign_amount')) {
$return['transactions'][0]['foreign_amount'] = $this->string('foreign_amount');
$return['transactions'][0]['foreign_currency_id'] = $this->integer('foreign_currency_id');
}
// fill in source and destination account data
switch ($this->string('transaction_type')) {
default:
throw new FireflyException(sprintf('Cannot handle transaction type "%s"', $this->string('transaction_type')));
case 'withdrawal':
$return['transactions'][0]['source_account_id'] = $this->integer('source_account_id');
$return['transactions'][0]['destination_account_name'] = $this->string('destination_account_name');
break;
}
return $return;
}
/**
* @return array
* @throws FireflyException
*/
public function rules(): array
{
$today = new Carbon;
$tomorrow = clone $today;
$tomorrow->addDay();
$rules = [
// mandatory info for recurrence.
//'title' => 'required|between:1,255|uniqueObjectForUser:recurrences,title',
'title' => 'required|between:1,255',
'first_date' => 'required|date|after:' . $today->format('Y-m-d'),
'repetition_type' => ['required', new ValidRecurrenceRepetitionType, 'between:1,20'],
'skip' => 'required|numeric|between:0,31',
// optional for recurrence:
'recurring_description' => 'between:0,65000',
'active' => 'numeric|between:0,1',
'apply_rules' => 'numeric|between:0,1',
// mandatory for transaction:
'transaction_description' => 'required|between:1,255',
'transaction_type' => 'required|in:withdrawal,deposit,transfer',
'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'amount' => 'numeric|required|more:0',
// mandatory account info:
'source_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'source_account_name' => 'between:1,255|nullable',
'destination_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'destination_account_name' => 'between:1,255|nullable',
// foreign amount data:
'foreign_currency_id' => 'exists:transaction_currencies,id',
'foreign_amount' => 'nullable|more:0',
// optional fields:
'budget_id' => 'mustExist:budgets,id|belongsToUser:budgets,id|nullable',
'category' => 'between:1,255|nullable',
'tags' => 'between:1,255|nullable',
];
// if ends after X repetitions, set another rule
if ($this->string('repetition_end') === 'times') {
$rules['repetitions'] = 'required|numeric|between:0,254';
}
// if foreign amount, currency must be different.
if ($this->float('foreign_amount') !== 0.0) {
$rules['foreign_currency_id'] = 'exists:transaction_currencies,id|different:transaction_currency_id';
}
// if ends at date X, set another rule.
if ($this->string('repetition_end') === 'until_date') {
$rules['repeat_until'] = 'required|date|after:' . $tomorrow->format('Y-m-d');
}
// switchc on type to expand rules for source and destination accounts:
switch ($this->string('transaction_type')) {
case strtolower(TransactionType::WITHDRAWAL):
$rules['source_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts';
$rules['destination_account_name'] = 'between:1,255|nullable';
break;
case strtolower(TransactionType::DEPOSIT):
$rules['source_account_name'] = 'between:1,255|nullable';
$rules['destination_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts';
break;
case strtolower(TransactionType::TRANSFER):
// this may not work:
$rules['source_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:destination_account_id';
$rules['destination_account_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:source_account_id';
break;
default:
throw new FireflyException(sprintf('Cannot handle transaction type of type "%s"', $this->string('transaction_type'))); // @codeCoverageIgnore
}
return $rules;
}
}

View File

@ -47,6 +47,16 @@ class Request extends FormRequest
return 1 === (int)$this->input($field);
}
/**
* @param string $field
*
* @return float
*/
public function float(string $field): float
{
return (float)$this->get($field);
}
/**
* @param string $field
*

View File

@ -64,8 +64,7 @@ class Recurrence extends Model
* @var array
*/
protected $casts
= [
= [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'first_date' => 'date',
@ -73,6 +72,10 @@ class Recurrence extends Model
'active' => 'bool',
'apply_rules' => 'bool',
];
/** @var array */
protected $fillable
= ['user_id', 'transaction_type_id', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active'];
/** @var string */
protected $table = 'recurrences';
/**

View File

@ -47,6 +47,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/
class RecurrenceTransaction extends Model
{
/** @var array */
protected $fillable
= ['recurrence_id', 'transaction_currency_id', 'foreign_currency_id', 'source_account_id', 'destination_account_id', 'amount', 'foreign_amount',
'description'];
/** @var string */
protected $table = 'recurrences_transactions';
/**
@ -82,7 +87,7 @@ class RecurrenceTransaction extends Model
*/
public function recurrenceTransactionMeta(): HasMany
{
return $this->hasMany(RecurrenceTransactionMeta::class,'rt_id');
return $this->hasMany(RecurrenceTransactionMeta::class, 'rt_id');
}
/**

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\Recurring;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\RecurrenceFactory;
use FireflyIII\Models\Note;
use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
@ -73,28 +74,35 @@ class RecurringRepository implements RecurringRepositoryInterface
}
/**
* Calculate the next X iterations starting on the date given in $date.
* Generate events in the date range.
*
* @param RecurrenceRepetition $repetition
* @param Carbon $date
* @param int $count
* @param Carbon $start
* @param Carbon $end
*
* @throws FireflyException
*
* @return array
* @throws FireflyException
*/
public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count = 5): array
public function getOccurrencesInRange(RecurrenceRepetition $repetition, Carbon $start, Carbon $end): array
{
$return = [];
$mutator = clone $date;
$mutator = clone $start;
$mutator->startOfDay();
$skipMod = $repetition->repetition_skip + 1;
$attempts = 0;
switch ($repetition->repetition_type) {
default:
throw new FireflyException(
sprintf('Cannot calculate occurrences for recurring transaction repetition type "%s"', $repetition->repetition_type)
);
case 'daily':
for ($i = 0; $i < $count; $i++) {
while ($mutator <= $end) {
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
}
$mutator->addDay();
$return[] = clone $mutator;
$attempts++;
}
break;
case 'weekly':
@ -110,35 +118,38 @@ class RecurringRepository implements RecurringRepositoryInterface
// today is friday (5), expected is monday (1), subtract four days.
$dayDifference = $dayOfWeek - $mutator->dayOfWeekIso;
$mutator->addDays($dayDifference);
for ($i = 0; $i < $count; $i++) {
$return[] = clone $mutator;
while ($mutator <= $end) {
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
}
$attempts++;
$mutator->addWeek();
}
break;
case 'monthly':
$mutator->addDay(); // always assume today has passed.
$dayOfMonth = (int)$repetition->repetition_moment;
if ($mutator->day > $dayOfMonth) {
// day has passed already, add a month.
$mutator->addMonth();
}
for ($i = 0; $i < $count; $i++) {
while ($mutator < $end) {
$domCorrected = min($dayOfMonth, $mutator->daysInMonth);
$mutator->day = $domCorrected;
$return[] = clone $mutator;
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
}
$attempts++;
$mutator->endOfMonth()->addDay();
}
break;
case 'ndom':
$mutator->addDay(); // always assume today has passed.
$mutator->startOfMonth();
// this feels a bit like a cop out but why reinvent the wheel?
$string = '%s %s of %s %s';
$counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth',];
$daysOfWeek = [1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',];
$parts = explode(',', $repetition->repetition_moment);
for ($i = 0; $i < $count; $i++) {
while ($mutator <= $end) {
$string = sprintf('%s %s of %s %s', $counters[$parts[0]], $daysOfWeek[$parts[1]], $mutator->format('F'), $mutator->format('Y'));
$newCarbon = new Carbon($string);
$return[] = clone $newCarbon;
@ -150,11 +161,131 @@ class RecurringRepository implements RecurringRepositoryInterface
$date->year = $mutator->year;
if ($mutator > $date) {
$date->addYear();
}
for ($i = 0; $i < $count; $i++) {
$obj = clone $date;
$obj->addYears($i);
$return[] = $obj;
// is $date between $start and $end?
$obj = clone $date;
$count = 0;
while ($obj <= $end && $obj >= $mutator && $count < 10) {
$return[] = clone $obj;
$obj->addYears(1);
$count++;
}
break;
}
return $return;
}
/**
* Calculate the next X iterations starting on the date given in $date.
*
* @param RecurrenceRepetition $repetition
* @param Carbon $date
* @param int $count
*
* @return array
* @throws FireflyException
*/
public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count): array
{
$return = [];
$mutator = clone $date;
$skipMod = $repetition->repetition_skip + 1;
$total = 0;
$attempts = 0;
switch ($repetition->repetition_type) {
default:
throw new FireflyException(
sprintf('Cannot calculate occurrences for recurring transaction repetition type "%s"', $repetition->repetition_type)
);
case 'daily':
while ($total < $count) {
$mutator->addDay();
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
$total++;
}
$attempts++;
}
break;
case 'weekly':
// monday = 1
// sunday = 7
$mutator->addDay(); // always assume today has passed.
$dayOfWeek = (int)$repetition->repetition_moment;
if ($mutator->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
$mutator->addWeek();
}
// today is wednesday (3), expected is friday (5): add two days.
// today is friday (5), expected is monday (1), subtract four days.
$dayDifference = $dayOfWeek - $mutator->dayOfWeekIso;
$mutator->addDays($dayDifference);
while ($total < $count) {
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
$total++;
}
$attempts++;
$mutator->addWeek();
}
break;
case 'monthly':
$mutator->addDay(); // always assume today has passed.
$dayOfMonth = (int)$repetition->repetition_moment;
if ($mutator->day > $dayOfMonth) {
// day has passed already, add a month.
$mutator->addMonth();
}
while ($total < $count) {
$domCorrected = min($dayOfMonth, $mutator->daysInMonth);
$mutator->day = $domCorrected;
if ($attempts % $skipMod === 0) {
$return[] = clone $mutator;
$total++;
}
$attempts++;
$mutator->endOfMonth()->addDay();
}
break;
case 'ndom':
$mutator->addDay(); // always assume today has passed.
$mutator->startOfMonth();
// this feels a bit like a cop out but why reinvent the wheel?
$counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth',];
$daysOfWeek = [1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',];
$parts = explode(',', $repetition->repetition_moment);
while ($total < $count) {
$string = sprintf('%s %s of %s %s', $counters[$parts[0]], $daysOfWeek[$parts[1]], $mutator->format('F'), $mutator->format('Y'));
$newCarbon = new Carbon($string);
if ($attempts % $skipMod === 0) {
$return[] = clone $newCarbon;
$total++;
}
$attempts++;
$mutator->endOfMonth()->addDay();
}
break;
case 'yearly':
$date = new Carbon($repetition->repetition_moment);
$date->year = $mutator->year;
if ($mutator > $date) {
$date->addYear();
}
$obj = clone $date;
while ($total < $count) {
if ($attempts % $skipMod === 0) {
$return[] = clone $obj;
$total++;
}
$obj->addYears(1);
$attempts++;
}
break;
}
@ -223,4 +354,18 @@ class RecurringRepository implements RecurringRepositoryInterface
{
$this->user = $user;
}
/**
* @param array $data
*
* @throws FireflyException
* @return Recurrence
*/
public function store(array $data): Recurrence
{
$factory = new RecurrenceFactory;
$factory->setUser($this->user);
return $factory->create($data);
}
}

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Recurring;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\User;
@ -53,6 +54,19 @@ interface RecurringRepositoryInterface
*/
public function getNoteText(Recurrence $recurrence): string;
/**
* Generate events in the date range.
*
* @param RecurrenceRepetition $repetition
* @param Carbon $start
* @param Carbon $end
*
* @throws FireflyException
*
* @return array
*/
public function getOccurrencesInRange(RecurrenceRepetition $repetition, Carbon $start, Carbon $end): array;
/**
* Calculate the next X iterations starting on the date given in $date.
* Returns an array of Carbon objects.
@ -61,9 +75,10 @@ interface RecurringRepositoryInterface
* @param Carbon $date
* @param int $count
*
* @throws FireflyException
* @return array
*/
public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count = 5): array;
public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count): array;
/**
* Parse the repetition in a string that is user readable.
@ -81,4 +96,12 @@ interface RecurringRepositoryInterface
*/
public function setUser(User $user): void;
/**
* @param array $data
*
* @throws FireflyException
* @return Recurrence
*/
public function store(array $data): Recurrence;
}

View File

@ -0,0 +1,72 @@
<?php
/**
* ValidRecurrenceRepetitionType.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Rules;
use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidRecurrenceRepetitionType
*/
class ValidRecurrenceRepetitionType implements Rule
{
/**
* Get the validation error message.
*
* @return string
*/
public function message(): string
{
return trans('validation.valid_recurrence_rep_type');
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
*
* @return bool
*/
public function passes($attribute, $value): bool
{
$value = (string)$value;
if ($value === 'daily') {
return true;
}
//monthly,17
//ndom,3,7
if (\in_array(substr($value, 0, 6), ['yearly', 'weekly'])) {
return true;
}
if (0 === strpos($value, 'monthly')) {
return true;
}
if (0 === strpos($value, 'ndom')) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* TransactionTypeTrait.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Internal\Support;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionTypeFactory;
use FireflyIII\Models\TransactionType;
use Log;
/**
* Trait TransactionTypeTrait
*
* @package FireflyIII\Services\Internal\Support
*/
trait TransactionTypeTrait
{
/**
* Get the transaction type. Since this is mandatory, will throw an exception when nothing comes up. Will always
* use TransactionType repository.
*
* @param string $type
*
* @return TransactionType
* @throws FireflyException
*/
protected function findTransactionType(string $type): TransactionType
{
$factory = app(TransactionTypeFactory::class);
$transactionType = $factory->find($type);
if (null === $transactionType) {
Log::error(sprintf('Could not find transaction type for "%s"', $type)); // @codeCoverageIgnore
throw new FireflyException(sprintf('Could not find transaction type for "%s"', $type)); // @codeCoverageIgnore
}
return $transactionType;
}
}

View File

@ -20,6 +20,8 @@
/** global: Modernizr, currencies */
var calendar;
$(document).ready(function () {
"use strict";
if (!Modernizr.inputtypes.date) {
@ -37,6 +39,19 @@ $(document).ready(function () {
$('#ffInput_repetition_end').on('change', respondToRepetitionEnd);
$('#ffInput_first_date').on('change', respondToFirstDateChange);
// create calendar on load:
calendar = $('#recurring_calendar').fullCalendar(
{
defaultDate: '2018-06-13',
editable: false,
height: 400,
width: 200,
contentHeight: 400,
aspectRatio: 1.25,
eventLimit: true,
eventSources: [],
});
$('#calendar-link').on('click', showRepCalendar);
});
@ -49,22 +64,17 @@ function showRepCalendar() {
var newEventsUri = eventsUri + '?type=' + $('#ffInput_repetition_type').val();
newEventsUri += '&skip=' + $('#ffInput_skip').val();
newEventsUri += '&ends=' + $('#ffInput_repetition_end').val();
newEventsUri += '&endDate=' + $('#ffInput_repeat_until').val();
newEventsUri += '&end_date=' + $('#ffInput_repeat_until').val();
newEventsUri += '&reps=' + $('#ffInput_repetitions').val();
newEventsUri += '&first_date=' + $('#ffInput_first_date').val();
// remove all event sources from calendar:
calendar.fullCalendar('removeEventSources');
$('#recurring_calendar').fullCalendar(
{
defaultDate: '2018-06-13',
editable: false,
height: 400,
width: 200,
contentHeight: 300,
aspectRatio: 1.25,
eventLimit: true, // allow "more" link when too many events
events: newEventsUri
});
// add a new one:
calendar.fullCalendar('addEventSource', newEventsUri);
$('#calendarModal').modal('show');
return false;
}
@ -169,6 +179,7 @@ function initializeButtons() {
console.log('Value is ' + btn.data('value'));
if (btn.data('value') === transactionType) {
btn.addClass('btn-info disabled').removeClass('btn-default');
$('input[name="transaction_type"]').val(transactionType);
} else {
btn.removeClass('btn-info disabled').addClass('btn-default');
}

View File

@ -1239,6 +1239,7 @@ return [
'repeat_forever' => 'Repeat forever',
'repeat_until_date' => 'Repeat until date',
'repeat_times' => 'Repeat a number of times',
'recurring_skips_one' => 'Every other',
'recurring_skips_more' => 'Skips :count occurrences',
'store_new_recurrence' => 'Store recurring transaction',
];

View File

@ -112,6 +112,7 @@ return [
'amount_zero' => 'The total amount cannot be zero',
'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.',
'secure_password' => 'This is not a secure password. Please try again. For more information, visit http://bit.ly/FF3-password-security',
'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions',
'attributes' => [
'email' => 'email address',
'description' => 'description',

View File

@ -16,7 +16,7 @@
<h3 class="box-title">{{ 'mandatory_for_recurring'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.text('name') }}
{{ ExpandedForm.text('title') }}
{{ ExpandedForm.date('first_date',null, {helpText: trans('firefly.help_first_date')}) }}
{{ ExpandedForm.select('repetition_type', [], null, {helpText: trans('firefly.change_date_other_options')}) }}
{{ ExpandedForm.number('skip', 0) }}
@ -78,6 +78,7 @@
</div>
</div>
</div>
<input type="hidden" name="transaction_type" value="">
{# end of three buttons#}
{{ ExpandedForm.text('transaction_description') }}
@ -125,7 +126,7 @@
{{ ExpandedForm.text('tags') }}
{# RELATE THIS TRANSFER TO A PIGGY BANK #}
{{ ExpandedForm.select('piggy_bank_id', [], '0') }}
{{ ExpandedForm.select('piggy_bank_id', piggies, 0) }}
</div>
</div>
</div>

View File

@ -83,7 +83,16 @@
<td>
<ul>
{% for rep in rt.repetitions %}
<li>{{ rep.description }}</li>
<li>{{ rep.description }}
{% if rep.repetition_skip == 1 %}
({{ trans('firefly.recurring_skips_one')|lower }})
{% endif %}
{% if rep.repetition_skip > 1 %}
({{ trans('firefly.recurring_skips_more', {count: rep.repetition_skip})|lower }})
{% endif %}
</li>
{% endfor %}
</ul>
</td>

View File

@ -42,7 +42,14 @@
<ul>
{% for rep in array.repetitions %}
<li>{{ rep.description }}
<li>
{{ rep.description }}
{% if rep.repetition_skip == 1 %}
({{ trans('firefly.recurring_skips_one')|lower }})
{% endif %}
{% if rep.repetition_skip > 1 %}
({{ trans('firefly.recurring_skips_more', {count: rep.repetition_skip})|lower }})
{% endif %}
<ul>
{% for occ in rep.occurrences %}
<li>{{ occ.formatLocalized(trans('config.month_and_date_day')) }}</li>