First batch of code for recurring transactions #1469

This commit is contained in:
James Cole 2018-06-10 16:59:03 +02:00
parent 35a5ec78c3
commit 6743d99d9b
29 changed files with 2242 additions and 48 deletions

View File

@ -0,0 +1,89 @@
<?php
/**
* CreateController.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\Controllers\Recurring;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use Illuminate\Http\Request;
/**
*
* Class CreateController
*/
class CreateController extends Controller
{
/** @var BudgetRepositoryInterface */
private $budgets;
/** @var RecurringRepositoryInterface */
private $recurring;
/**
*
*/
public function __construct()
{
parent::__construct();
// translations:
$this->middleware(
function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-paint-brush');
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);
return $next($request);
}
);
}
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function create(Request $request)
{
// todo refactor to expandedform method.
$budgets = app('expandedform')->makeSelectListWithEmpty($this->budgets->getActiveBudgets());
$defaultCurrency = app('amount')->getDefaultCurrency();
$tomorrow = new Carbon;
$tomorrow->addDay();
// flash some data:
$preFilled = [
'first_date' => $tomorrow->format('Y-m-d'),
'transaction_type' => 'withdrawal',
'active' => $request->old('active') ?? true,
'apply_rules' => $request->old('apply_rules') ?? true,
];
$request->session()->flash('preFilled', $preFilled);
return view('recurring.create', compact('tomorrow', 'preFilled', 'defaultCurrency','budgets'));
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* EditController.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\Controllers\Recurring;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Recurrence;
/**
*
* Class EditController
*/
class EditController extends Controller
{
/**
*
*/
public function __construct()
{
parent::__construct();
// translations:
$this->middleware(
function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-paint-brush');
app('view')->share('title', trans('firefly.recurrences'));
app('view')->share('subTitle', trans('firefly.recurrences'));
$this->recurring = app(RecurringRepositoryInterface::class);
return $next($request);
}
);
}
/**
* @param Recurrence $recurrence
*/
public function edit(Recurrence $recurrence) {
return view('recurring.edit', compact('recurrence'));
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* IndexController.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\Controllers\Recurring;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Transformers\RecurrenceTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Response;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
*
* Class IndexController
*/
class IndexController extends Controller
{
/** @var RecurringRepositoryInterface */
private $recurring;
/**
*
*/
public function __construct()
{
parent::__construct();
// translations:
$this->middleware(
function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-paint-brush');
app('view')->share('title', trans('firefly.recurrences'));
$this->recurring = app(RecurringRepositoryInterface::class);
return $next($request);
}
);
}
/**
* @param Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function index(Request $request)
{
$page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$collection = $this->recurring->getActive();
// TODO: split collection into pages
$transformer = new RecurrenceTransformer(new ParameterBag);
$recurring = [];
/** @var Recurrence $recurrence */
foreach ($collection as $recurrence) {
$array = $transformer->transform($recurrence);
$array['first_date'] = new Carbon($array['first_date']);
$array['latest_date'] = null === $array['latest_date'] ? null : new Carbon($array['latest_date']);
$recurring[] = $array;
}
return view('recurring.index', compact('recurring', 'page', 'pageSize'));
}
/**
* @param Recurrence $recurrence
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function show(Recurrence $recurrence)
{
$transformer = new RecurrenceTransformer(new ParameterBag);
$array = $transformer->transform($recurrence);
// transform dates back to Carbon objects:
foreach ($array['repetitions'] as $index => $repetition) {
foreach ($repetition['occurrences'] as $item => $occurrence) {
$array['repetitions'][$index]['occurrences'][$item] = new Carbon($occurrence);
}
}
$subTitle = trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]);
return view('recurring.show', compact('recurrence', 'subTitle', 'array'));
}
/**
* @param Request $request
*
* @return JsonResponse
*/
public function suggest(Request $request): JsonResponse
{
$today = new Carbon;
$date = Carbon::createFromFormat('Y-m-d', $request->get('date'));
$result = [];
if ($date > $today) {
$weekly = sprintf('weekly,%s', $date->dayOfWeekIso);
$monthly = sprintf('monthly,%s', $date->day);
$dayOfWeek = trans(sprintf('config.dow_%s', $date->dayOfWeekIso));
$ndom = sprintf('ndom,%s,%s', $date->weekOfMonth, $date->dayOfWeekIso);
$yearly = sprintf('yearly,%s', $date->format('Y-m-d'));
$yearlyDate = $date->formatLocalized(trans('config.month_and_day_no_year'));
$result = [
'daily' => trans('firefly.recurring_daily'),
$weekly => trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek]),
$monthly => trans('firefly.recurring_monthly', ['dayOfMonth' => $date->day]),
$ndom => trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $date->weekOfMonth]),
$yearly => trans('firefly.recurring_yearly', ['date' => $yearlyDate]),
];
}
return Response::json($result);
}
}

159
app/Models/Recurrence.php Normal file
View File

@ -0,0 +1,159 @@
<?php
/**
* Recurrence.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\Models;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class Recurrence
*
* @property int $id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $user_id
* @property int $transaction_type_id
* @property int $transaction_currency_id
* @property string $title
* @property string $description
* @property \Carbon\Carbon $first_date
* @property \Carbon\Carbon $repeat_until
* @property \Carbon\Carbon $latest_date
* @property string $repetition_type
* @property string $repetition_moment
* @property int $repetition_skip
* @property bool $active
* @property bool $apply_rules
* @property \FireflyIII\User $user
* @property \Illuminate\Support\Collection $recurrenceRepetitions
* @property \Illuminate\Support\Collection $recurrenceMeta
* @property \Illuminate\Support\Collection $recurrenceTransactions
* @property \FireflyIII\Models\TransactionType $transactionType
*
*/
class Recurrence extends Model
{
/**
* The attributes that should be casted to native types.
*
* @var array
*/
protected $casts
= [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'first_date' => 'date',
'latest_date' => 'date',
'active' => 'bool',
'apply_rules' => 'bool',
];
protected $table = 'recurrences';
/**
* @param string $value
*
* @return Recurrence
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public static function routeBinder(string $value): Recurrence
{
if (auth()->check()) {
$recurrenceId = (int)$value;
$recurrence = auth()->user()->recurrences()->find($recurrenceId);
if (null !== $recurrence) {
return $recurrence;
}
}
throw new NotFoundHttpException;
}
/**
* @codeCoverageIgnore
* Get all of the notes.
*/
public function notes()
{
return $this->morphMany(Note::class, 'noteable');
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function recurrenceMeta(): HasMany
{
return $this->hasMany(RecurrenceMeta::class);
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function recurrenceRepetitions(): HasMany
{
return $this->hasMany(RecurrenceRepetition::class);
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function recurrenceTransactions(): HasMany
{
return $this->hasMany(RecurrenceTransaction::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function transactionCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function transactionType(): BelongsTo
{
return $this->belongsTo(TransactionType::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* RecurrenceMeta.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\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class RecurrenceMeta
*
* @property string $name
* @property string $value
*/
class RecurrenceMeta extends Model
{
protected $table = 'recurrences_meta';
/**
* @return BelongsTo
* @codeCoverageIgnore
*/
public function recurrence(): BelongsTo
{
return $this->belongsTo(Recurrence::class);
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* RecurrenceRepetition.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\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class RecurrenceRepetition
*
* @property string $repetition_type
* @property string $repetition_moment
* @property int $repetition_skip
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $deleted_at
* @property \Carbon\Carbon $updated_at
* @property int $id
*/
class RecurrenceRepetition extends Model
{
protected $table = 'recurrences_repetitions';
/**
* @return BelongsTo
* @codeCoverageIgnore
*/
public function recurrence(): BelongsTo
{
return $this->belongsTo(Recurrence::class);
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* RecurrenceTransaction.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\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
*
* Class RecurrenceTransaction
*
* @property int $transaction_currency_id,
* @property int $foreign_currency_id
* @property int $source_account_id
* @property int $destination_account_id
* @property string $amount
* @property string $foreign_amount
* @property string $description
* @property \FireflyIII\Models\TransactionCurrency $transactionCurrency
* @property \FireflyIII\Models\TransactionCurrency $foreignCurrency
* @property \FireflyIII\Models\Account $sourceAccount
* @property \FireflyIII\Models\Account $destinationAccount
* @property \Illuminate\Support\Collection $recurrenceTransactionMeta
*/
class RecurrenceTransaction extends Model
{
protected $table = 'recurrences_transactions';
/**
* @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function destinationAccount(): BelongsTo
{
return $this->belongsTo(Account::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function foreignCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class);
}
/**
* @return BelongsTo
* @codeCoverageIgnore
*/
public function recurrence(): BelongsTo
{
return $this->belongsTo(Recurrence::class);
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function recurrenceTransactionMeta(): HasMany
{
return $this->hasMany(recurrenceTransactionMeta::class,'rt_id');
}
/**
* @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function sourceAccount(): BelongsTo
{
return $this->belongsTo(Account::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function transactionCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class);
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* RecurrenceMeta.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\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class RecurrenceTransactionMeta
*
* @property string $name
* @property string $value
*/
class RecurrenceTransactionMeta extends Model
{
protected $table = 'rt_meta';
/**
* @return BelongsTo
* @codeCoverageIgnore
*/
public function recurrenceTransaction(): BelongsTo
{
return $this->belongsTo(RecurrenceTransaction::class);
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* RecurringServiceProvider.php
* Copyright (c) 2017 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\Providers;
use FireflyIII\Repositories\Recurring\RecurringRepository;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
/**
* @codeCoverageIgnore
* Class RecurringServiceProvider.
*/
class RecurringServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot(): void
{
}
/**
* Register the application services.
*/
public function register(): void
{
$this->app->bind(
RecurringRepositoryInterface::class,
function (Application $app) {
/** @var RecurringRepositoryInterface $repository */
$repository = app(RecurringRepository::class);
if ($app->auth->check()) {
$repository->setUser(auth()->user());
}
return $repository;
}
);
}
}

View File

@ -0,0 +1,226 @@
<?php
/**
* RecurringRepository.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\Repositories\Recurring;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Note;
use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
*
* Class RecurringRepository
*/
class RecurringRepository implements RecurringRepositoryInterface
{
/** @var User */
private $user;
/**
* Returns all of the user's recurring transactions.
*
* @return Collection
*/
public function getActive(): Collection
{
return $this->user->recurrences()->with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions'])->where(
'active', 1
)->get();
}
/**
* Get the notes.
*
* @param Recurrence $recurrence
*
* @return string
*/
public function getNoteText(Recurrence $recurrence): string
{
/** @var Note $note */
$note = $recurrence->notes()->first();
if (null !== $note) {
return (string)$note->text;
}
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 getOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count = 5): array
{
$return = [];
$mutator = clone $date;
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++) {
$mutator->addDay();
$return[] = clone $mutator;
}
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);
for ($i = 0; $i < $count; $i++) {
$return[] = clone $mutator;
$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++) {
$domCorrected = min($dayOfMonth, $mutator->daysInMonth);
$mutator->day = $domCorrected;
$return[] = clone $mutator;
$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++) {
$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;
$mutator->endOfMonth()->addDay();
}
break;
case 'yearly':
$date = new Carbon($repetition->repetition_moment);
$date->year = $mutator->year;
if ($mutator > $date) {
$date->addYear();
}
for ($i = 0; $i < $count; $i++) {
$obj = clone $date;
$obj->addYears($i);
$return[] = $obj;
}
break;
}
return $return;
}
/**
* Parse the repetition in a string that is user readable.
*
* @param RecurrenceRepetition $repetition
*
* @return string
* @throws FireflyException
*/
public function repetitionDescription(RecurrenceRepetition $repetition): string
{
/** @var Preference $pref */
$pref = app('preferences')->getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
$language = $pref->data;
switch ($repetition->repetition_type) {
default:
throw new FireflyException(sprintf('Cannot translate recurring transaction repetition type "%s"', $repetition->repetition_type));
break;
case 'daily':
return trans('firefly.recurring_daily', [], $language);
break;
case 'weekly':
$dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language);
return trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
break;
case 'monthly':
// format a date:
return trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment], $language);
break;
case 'ndom':
$parts = explode(',', $repetition->repetition_moment);
// first part is number of week, second is weekday.
$dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language);
return trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
break;
case 'yearly':
//
$today = new Carbon;
$today->endOfYear();
$repDate = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment);
$diffInYears = $today->diffInYears($repDate);
$repDate->addYears($diffInYears); // technically not necessary.
$string = $repDate->formatLocalized(trans('config.month_and_day_no_year'));
return trans('firefly.recurring_yearly', ['date' => $string], $language);
break;
}
}
/**
* Set user for in repository.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* RecurringRepositoryInterface.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\Repositories\Recurring;
use Carbon\Carbon;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Interface RecurringRepositoryInterface
*
* @package FireflyIII\Repositories\Recurring
*/
interface RecurringRepositoryInterface
{
/**
* Returns all of the user's recurring transactions.
*
* @return Collection
*/
public function getActive(): Collection;
/**
* Get the notes.
*
* @param Recurrence $recurrence
*
* @return string
*/
public function getNoteText(Recurrence $recurrence): string;
/**
* Calculate the next X iterations starting on the date given in $date.
* Returns an array of Carbon objects.
*
* @param RecurrenceRepetition $repetition
* @param Carbon $date
* @param int $count
*
* @return array
*/
public function getOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count = 5): array;
/**
* Parse the repetition in a string that is user readable.
*
* @param RecurrenceRepetition $repetition
*
* @return string
*/
public function repetitionDescription(RecurrenceRepetition $repetition): string;
/**
* Set user for in repository.
*
* @param User $user
*/
public function setUser(User $user): void;
}

View File

@ -0,0 +1,256 @@
<?php
/**
* RecurringTransactionTransformer.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\Transformers;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\CategoryFactory;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceMeta;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\RecurrenceTransactionMeta;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use League\Fractal\Resource\Item;
use League\Fractal\TransformerAbstract;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
*
* Class RecurringTransactionTransformer
*/
class RecurrenceTransformer extends TransformerAbstract
{
/** @noinspection ClassOverridesFieldOfSuperClassInspection */
/**
* List of resources possible to include.
*
* @var array
*/
protected $availableIncludes = ['user'];
/**
* List of resources to automatically include
*
* @var array
*/
protected $defaultIncludes = [];
/** @var ParameterBag */
protected $parameters;
/** @var RecurringRepositoryInterface */
protected $repository;
public function __construct(ParameterBag $parameters)
{
$this->repository = app(RecurringRepositoryInterface::class);
$this->parameters = $parameters;
}
/**
* Include user data in end result.
*
* @codeCoverageIgnore
*
* @param Recurrence $recurrence
*
*
* @return Item
*/
public function includeUser(Recurrence $recurrence): Item
{
return $this->item($recurrence->user, new UserTransformer($this->parameters), 'user');
}
/**
* Transform the piggy bank.
*
* @param Recurrence $recurrence
*
* @return array
* @throws FireflyException
*/
public function transform(Recurrence $recurrence): array
{
$this->repository->setUser($recurrence->user);
$return = [
'id' => (int)$recurrence->id,
'updated_at' => $recurrence->updated_at->toAtomString(),
'created_at' => $recurrence->created_at->toAtomString(),
'transaction_type_id' => $recurrence->transaction_type_id,
'transaction_type' => $recurrence->transactionType->type,
'title' => $recurrence->title,
'description' => $recurrence->description,
'first_date' => $recurrence->first_date->format('Y-m-d'),
'latest_date' => null === $recurrence->latest_date ? null : $recurrence->latest_date->format('Y-m-d'),
'repeat_until' => null === $recurrence->repeat_until ? null : $recurrence->repeat_until->format('Y-m-d'),
'apply_rules' => $recurrence->apply_rules,
'active' => $recurrence->active,
'notes' => $this->repository->getNoteText($recurrence),
'repetitions' => [],
'transactions' => [],
'meta' => [],
'links' => [
[
'rel' => 'self',
'uri' => '/recurring/' . $recurrence->id,
],
],
];
$fromDate = $recurrence->latest_date ?? $recurrence->first_date;
// date in the past? use today:
$today = new Carbon;
$fromDate = $fromDate->lte($today) ? $today : $fromDate;
/** @var RecurrenceRepetition $repetition */
foreach ($recurrence->recurrenceRepetitions as $repetition) {
$repetitionArray = [
'id' => $repetition->id,
'updated_at' => $repetition->updated_at->toAtomString(),
'created_at' => $repetition->created_at->toAtomString(),
'repetition_type' => $repetition->repetition_type,
'repetition_moment' => $repetition->repetition_moment,
'repetition_skip' => (int)$repetition->repetition_skip,
'description' => $this->repository->repetitionDescription($repetition),
'occurrences' => [],
];
// get the (future) occurrences for this specific type of repetition:
$occurrences = $this->repository->getOccurrences($repetition, $fromDate, 5);
/** @var Carbon $carbon */
foreach ($occurrences as $carbon) {
$repetitionArray['occurrences'][] = $carbon->format('Y-m-d');
}
$return['repetitions'][] = $repetitionArray;
}
unset($repetitionArray);
// get all transactions:
/** @var RecurrenceTransaction $transaction */
foreach ($recurrence->recurrenceTransactions as $transaction) {
$transactionArray = [
'currency_id' => $transaction->transaction_currency_id,
'currency_code' => $transaction->transactionCurrency->code,
'currency_symbol' => $transaction->transactionCurrency->symbol,
'currency_dp' => $transaction->transactionCurrency->decimal_places,
'foreign_currency_id' => $transaction->foreign_currency_id,
'source_account_id' => $transaction->source_account_id,
'source_account_name' => $transaction->sourceAccount->name,
'destination_account_id' => $transaction->destination_account_id,
'destination_account_name' => $transaction->destinationAccount->name,
'amount' => $transaction->amount,
'foreign_amount' => $transaction->foreign_amount,
'description' => $transaction->description,
'meta' => [],
];
if (null !== $transaction->foreign_currency_id) {
$transactionArray['foreign_currency_code'] = $transaction->foreignCurrency->code;
$transactionArray['foreign_currency_symbol'] = $transaction->foreignCurrency->symbol;
$transactionArray['foreign_currency_dp'] = $transaction->foreignCurrency->decimal_places;
}
// get meta data for each transaction:
/** @var RecurrenceTransactionMeta $transactionMeta */
foreach ($transaction->recurrenceTransactionMeta as $transactionMeta) {
$transactionMetaArray = [
'name' => $transactionMeta->name,
'value' => $transactionMeta->value,
];
switch ($transactionMeta->name) {
default:
throw new FireflyException(sprintf('Recurrence transformer cannot handle transaction meta-field "%s"', $transactionMeta->name));
case 'category_name':
/** @var CategoryFactory $factory */
$factory = app(CategoryFactory::class);
$factory->setUser($recurrence->user);
$category = $factory->findOrCreate(null, $transactionMeta->value);
if (null !== $category) {
$transactionMetaArray['category_id'] = $category->id;
$transactionMetaArray['category_name'] = $category->name;
}
break;
case 'budget_id':
/** @var BudgetRepositoryInterface $repository */
$repository = app(BudgetRepositoryInterface::class);
$budget = $repository->findNull((int)$transactionMeta->value);
if (null !== $budget) {
$transactionMetaArray['budget_id'] = $budget->id;
$transactionMetaArray['budget_name'] = $budget->name;
}
break;
}
// store transaction meta data in transaction
$transactionArray['meta'][] = $transactionMetaArray;
}
// store transaction in recurrence array.
$return['transactions'][] = $transactionArray;
}
// get all meta data for recurrence itself
/** @var RecurrenceMeta $recurrenceMeta */
foreach ($recurrence->recurrenceMeta as $recurrenceMeta) {
$recurrenceMetaArray = [
'name' => $recurrenceMeta->name,
'value' => $recurrenceMeta->value,
];
switch ($recurrenceMeta->name) {
default:
throw new FireflyException(sprintf('Recurrence transformer cannot handle meta-field "%s"', $recurrenceMeta->name));
case 'tags':
$recurrenceMetaArray['tags'] = explode(',', $recurrenceMeta->value);
break;
case 'notes':
break;
case 'bill_id':
/** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class);
$bill = $repository->find((int)$recurrenceMeta->value);
if (null !== $bill) {
$recurrenceMetaArray['bill_id'] = $bill->id;
$recurrenceMetaArray['bill_name'] = $bill->name;
}
break;
case 'piggy_bank_id':
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$piggy = $repository->findNull((int)$recurrenceMeta->value);
if (null !== $piggy) {
$recurrenceMetaArray['piggy_bank_id'] = $piggy->id;
$recurrenceMetaArray['piggy_bank_name'] = $piggy->name;
}
break;
}
// store meta date in recurring array
$return['meta'][] = $recurrenceMetaArray;
}
return $return;
}
}

View File

@ -36,6 +36,7 @@ use FireflyIII\Models\ExportJob;
use FireflyIII\Models\ImportJob; use FireflyIII\Models\ImportJob;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\Role; use FireflyIII\Models\Role;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleGroup;
@ -291,6 +292,17 @@ class User extends Authenticatable
return $this->hasMany(Preference::class); return $this->hasMany(Preference::class);
} }
/**
* @codeCoverageIgnore
* Link to recurring transactions.
*
* @return HasMany
*/
public function recurrences(): HasMany
{
return $this->hasMany(Recurrence::class);
}
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
* Link to roles. * Link to roles.

View File

@ -98,6 +98,7 @@ return [
FireflyIII\Providers\SearchServiceProvider::class, FireflyIII\Providers\SearchServiceProvider::class,
FireflyIII\Providers\TagServiceProvider::class, FireflyIII\Providers\TagServiceProvider::class,
FireflyIII\Providers\AdminServiceProvider::class, FireflyIII\Providers\AdminServiceProvider::class,
FireflyIII\Providers\RecurringServiceProvider::class,
], ],

View File

@ -271,6 +271,7 @@ return [
'piggyBank' => \FireflyIII\Models\PiggyBank::class, 'piggyBank' => \FireflyIII\Models\PiggyBank::class,
'tj' => \FireflyIII\Models\TransactionJournal::class, 'tj' => \FireflyIII\Models\TransactionJournal::class,
'tag' => \FireflyIII\Models\Tag::class, 'tag' => \FireflyIII\Models\Tag::class,
'recurrence' => \FireflyIII\Models\Recurrence::class,
'rule' => \FireflyIII\Models\Rule::class, 'rule' => \FireflyIII\Models\Rule::class,
'ruleGroup' => \FireflyIII\Models\RuleGroup::class, 'ruleGroup' => \FireflyIII\Models\RuleGroup::class,
'exportJob' => \FireflyIII\Models\ExportJob::class, 'exportJob' => \FireflyIII\Models\ExportJob::class,

View File

@ -37,7 +37,6 @@ class ChangesForV474 extends Migration
*/ */
public function down() public function down()
{ {
//
} }
/** /**

View File

@ -0,0 +1,129 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
*
* Class ChangesForV475
*/
class ChangesForV475 extends Migration
{
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('recurrences_repetitions');
Schema::dropIfExists('recurrences_meta');
Schema::dropIfExists('rt_meta');
Schema::dropIfExists('recurrences_transactions');
Schema::dropIfExists('recurrences');
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create(
'recurrences', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('user_id', false, true);
$table->integer('transaction_type_id', false, true);
$table->string('title', 1024);
$table->text('description');
$table->date('first_date');
$table->date('repeat_until')->nullable();
$table->date('latest_date')->nullable();
$table->boolean('apply_rules')->default(true);
$table->boolean('active')->default(true);
// also separate:
// category, budget, tags, notes, bill, piggy bank
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('transaction_type_id')->references('id')->on('transaction_types')->onDelete('cascade');
}
);
Schema::create(
'recurrences_transactions', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('recurrence_id', false, true);
$table->integer('transaction_currency_id', false, true);
$table->integer('foreign_currency_id', false, true)->nullable();
$table->integer('source_account_id', false, true);
$table->integer('destination_account_id', false, true);
$table->decimal('amount', 22, 12);
$table->decimal('foreign_amount', 22, 12)->nullable();
$table->string('description', 1024);
$table->foreign('recurrence_id')->references('id')->on('recurrences')->onDelete('cascade');
$table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade');
$table->foreign('foreign_currency_id')->references('id')->on('transaction_currencies')->onDelete('set null');
$table->foreign('source_account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('destination_account_id')->references('id')->on('accounts')->onDelete('cascade');
}
);
Schema::create(
'recurrences_repetitions', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('recurrence_id', false, true);
$table->string('repetition_type', 50);
$table->string('repetition_moment', 50);
$table->smallInteger('repetition_skip', false, true);
$table->foreign('recurrence_id')->references('id')->on('recurrences')->onDelete('cascade');
}
);
Schema::create(
'recurrences_meta', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('recurrence_id', false, true);
$table->string('name', 50);
$table->text('value');
$table->foreign('recurrence_id')->references('id')->on('recurrences')->onDelete('cascade');
}
);
Schema::create(
'rt_meta', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('rt_id', false, true);
$table->string('name', 50);
$table->text('value');
$table->foreign('rt_id')->references('id')->on('recurrences_transactions')->onDelete('cascade');
}
);
}
}

170
public/js/ff/recurring/create.js vendored Normal file
View File

@ -0,0 +1,170 @@
/*
* create.js
* Copyright (c) 2017 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/>.
*/
/** global: Modernizr, currencies */
$(document).ready(function () {
"use strict";
if (!Modernizr.inputtypes.date) {
$('input[type="date"]').datepicker(
{
dateFormat: 'yy-mm-dd'
}
);
}
initializeButtons();
initializeAutoComplete();
respondToFirstDateChange();
$('.switch-button').on('click', switchTransactionType);
$('#ffInput_first_date').on('change', respondToFirstDateChange);
});
function respondToFirstDateChange() {
var obj = $('#ffInput_first_date');
var select = $('#ffInput_repetition_type');
var date = obj.val();
select.prop('disabled', true);
$.getJSON(suggestUri, {date: date}).fail(function () {
console.error('Could not load repetition suggestions');
alert('Could not load repetition suggestions');
}).done(parseRepetitionSuggestions);
}
function parseRepetitionSuggestions(data) {
var select = $('#ffInput_repetition_type');
select.empty();
for (var k in data) {
if (data.hasOwnProperty(k)) {
select.append($('<option>').val(k).attr('label', data[k]).text(data[k]));
}
}
select.removeAttr('disabled');
}
function initializeAutoComplete() {
// auto complete things:
$.getJSON('json/tags').done(function (data) {
var opt = {
typeahead: {
source: data,
afterSelect: function () {
this.$element.val("");
},
autoSelect: false,
},
autoSelect: false,
};
$('input[name="tags"]').tagsinput(
opt
);
});
if ($('input[name="destination_account_name"]').length > 0) {
$.getJSON('json/expense-accounts').done(function (data) {
$('input[name="destination_account_name"]').typeahead({source: data, autoSelect: false});
});
}
if ($('input[name="source_account_name"]').length > 0) {
$.getJSON('json/revenue-accounts').done(function (data) {
$('input[name="source_account_name"]').typeahead({source: data, autoSelect: false});
});
}
$.getJSON('json/categories').done(function (data) {
$('input[name="category"]').typeahead({source: data, autoSelect: false});
});
}
/**
*
* @param e
*/
function switchTransactionType(e) {
var target = $(e.target);
transactionType = target.data('value');
initializeButtons();
return false;
}
/**
* Loop the three buttons and do some magic.
*/
function initializeButtons() {
console.log('Now in initializeButtons()');
$.each($('.switch-button'), function (i, v) {
var btn = $(v);
console.log('Value is ' + btn.data('value'));
if (btn.data('value') === transactionType) {
btn.addClass('btn-info disabled').removeClass('btn-default');
} else {
btn.removeClass('btn-info disabled').addClass('btn-default');
}
});
updateFormFields();
}
/**
* Hide and/or show stuff when switching:
*/
function updateFormFields() {
if (transactionType === 'withdrawal') {
// hide source account name:
$('#source_account_name_holder').hide();
// show source account ID:
$('#source_account_id_holder').show();
// show destination name:
$('#destination_account_name_holder').show();
// hide destination ID:
$('#destination_account_id_holder').hide();
// show budget
$('#budget_id_holder').show();
// hide piggy bank:
$('#piggy_bank_id_holder').hide();
}
if (transactionType === 'deposit') {
$('#source_account_name_holder').show();
$('#source_account_id_holder').hide();
$('#destination_account_name_holder').hide();
$('#destination_account_id_holder').show();
$('#budget_id_holder').hide();
$('#piggy_bank_id_holder').hide();
}
if (transactionType === 'transfer') {
$('#source_account_name_holder').hide();
$('#source_account_id_holder').show();
$('#destination_account_name_holder').hide();
$('#destination_account_id_holder').show();
$('#budget_id_holder').hide();
$('#piggy_bank_id_holder').show();
}
}

View File

@ -23,20 +23,29 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'html_language' => 'en', 'html_language' => 'en',
'locale' => 'en, English, en_US, en_US.utf8, en_US.UTF-8', 'locale' => 'en, English, en_US, en_US.utf8, en_US.UTF-8',
'month' => '%B %Y', 'month' => '%B %Y',
'month_and_day' => '%B %e, %Y', 'month_and_day' => '%B %e, %Y',
'date_time' => '%B %e, %Y, @ %T', 'month_and_date_day' => '%A %B %e, %Y',
'specific_day' => '%e %B %Y', 'month_and_day_no_year' => '%B %e',
'week_in_year' => 'Week %W, %Y', 'date_time' => '%B %e, %Y, @ %T',
'year' => '%Y', 'specific_day' => '%e %B %Y',
'half_year' => '%B %Y', 'week_in_year' => 'Week %W, %Y',
'month_js' => 'MMMM YYYY', 'year' => '%Y',
'month_and_day_js' => 'MMMM Do, YYYY', 'half_year' => '%B %Y',
'date_time_js' => 'MMMM Do, YYYY, @ HH:mm:ss', 'month_js' => 'MMMM YYYY',
'specific_day_js' => 'D MMMM YYYY', 'month_and_day_js' => 'MMMM Do, YYYY',
'week_in_year_js' => '[Week] w, YYYY', 'date_time_js' => 'MMMM Do, YYYY, @ HH:mm:ss',
'year_js' => 'YYYY', 'specific_day_js' => 'D MMMM YYYY',
'half_year_js' => 'Q YYYY', 'week_in_year_js' => '[Week] w, YYYY',
'year_js' => 'YYYY',
'half_year_js' => 'Q YYYY',
'dow_1' => 'Monday',
'dow_2' => 'Tuesday',
'dow_3' => 'Wednesday',
'dow_4' => 'Thursday',
'dow_5' => 'Friday',
'dow_6' => 'Saturday',
'dow_7' => 'Sunday',
]; ];

View File

@ -820,7 +820,7 @@ return [
'language' => 'Language', 'language' => 'Language',
'new_savings_account' => ':bank_name savings account', 'new_savings_account' => ':bank_name savings account',
'cash_wallet' => 'Cash wallet', 'cash_wallet' => 'Cash wallet',
'currency_not_present' => 'If the currency you normally use is not listed do not worry. You can create your own currencies under Options > Currencies.', 'currency_not_present' => 'If the currency you normally use is not listed do not worry. You can create your own currencies under Options > Currencies.',
// home page: // home page:
'yourAccounts' => 'Your accounts', 'yourAccounts' => 'Your accounts',
@ -1206,4 +1206,35 @@ return [
'no_bills_intro_default' => 'You have no bills yet. You can create bills to keep track of regular expenses, like your rent or insurance.', 'no_bills_intro_default' => 'You have no bills yet. You can create bills to keep track of regular expenses, like your rent or insurance.',
'no_bills_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:', 'no_bills_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:',
'no_bills_create_default' => 'Create a bill', 'no_bills_create_default' => 'Create a bill',
// recurring transactions
'recurrences' => 'Recurring transactions',
'no_recurring_title_default' => 'Let\'s create a recurring transaction!',
'no_recurring_intro_default' => 'You have no recurring transactions yet. You can use these to make Firefly III automatically create transactions for you.',
'no_recurring_imperative_default' => 'This is a pretty advanced feature but it can be extremely useful. Make sure you read the documentation (?-icon in the top right corner) before you continue.',
'no_recurring_create_default' => 'Create a recurring transaction',
'make_new_recurring' => 'Create a recurring transaction',
'recurring_daily' => 'Every day',
'recurring_weekly' => 'Every week on :weekday',
'recurring_monthly' => 'Every month on the :dayOfMonth(st/nd/rd/th) day',
'recurring_ndom' => 'Every month on the :dayOfMonth(st/nd/rd/th) :weekday',
'recurring_yearly' => 'Every year on :date',
'overview_for_recurrence' => 'Overview for recurring transaction ":title"',
'warning_duplicates_repetitions' => 'In rare instances, dates appear twice in this list. This can happen when multiple repetitions collide. Firefly III will always generate one transaction per day.',
'created_transactions' => 'Related transactions',
'expected_transactions' => 'Expected transactions',
'recurring_meta_field_tags' => 'Tags',
'recurring_meta_field_notes' => 'Notes',
'recurring_meta_field_bill_id' => 'Bill',
'recurring_meta_field_piggy_bank_id' => 'Piggy bank',
'create_new_recurrence' => 'Create new recurring transaction',
'help_first_date' => 'Indicate the first expected recurrence. This must be in the future.',
'mandatory_for_recurring' => 'Mandatory recurrence information',
'mandatory_for_transaction' => 'Mandatory transaction information',
'optional_for_recurring' => 'Optional recurrence information',
'optional_for_transaction' => 'Optional transaction information',
'change_date_other_options' => 'Change the "first date" to see more options.',
'mandatory_fields_for_tranaction' => 'The values here will end up in the transaction(s) being created',
]; ];

View File

@ -216,11 +216,19 @@ return [
'country_code' => 'Country code', 'country_code' => 'Country code',
'provider_code' => 'Bank or data-provider', 'provider_code' => 'Bank or data-provider',
'due_date' => 'Due date', 'due_date' => 'Due date',
'payment_date' => 'Payment date', 'payment_date' => 'Payment date',
'invoice_date' => 'Invoice date', 'invoice_date' => 'Invoice date',
'internal_reference' => 'Internal reference', 'internal_reference' => 'Internal reference',
'inward' => 'Inward description', 'inward' => 'Inward description',
'outward' => 'Outward description', 'outward' => 'Outward description',
'rule_group_id' => 'Rule group', 'rule_group_id' => 'Rule group',
'transaction_description' => 'Transaction description',
'first_date' => 'First date',
'transaction_type' => 'Transaction type',
'repeat_until' => 'Repeat until',
'recurring_description' => 'Recurring transaction description',
'repetition_type' => 'Type of repetition',
'foreign_currency_id' => 'Foreign currency',
]; ];

View File

@ -123,4 +123,9 @@ return [
'spectre_last_use' => 'Last login', 'spectre_last_use' => 'Last login',
'spectre_status' => 'Status', 'spectre_status' => 'Status',
'bunq_payment_id' => 'bunq payment ID', 'bunq_payment_id' => 'bunq payment ID',
'repetitions' => 'Repetitions',
'title' => 'Title',
'transaction_s' => 'Transaction(s)',
'field' => 'Field',
'value' => 'Value',
]; ];

View File

@ -35,7 +35,7 @@
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }} {{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
{{ ExpandedForm.integer('skip',0) }} {{ ExpandedForm.integer('skip',0) }}
{{ ExpandedForm.checkbox('active',1,true) }} {{ ExpandedForm.checkbox('active',1, true) }}
</div> </div>
</div> </div>

View File

@ -98,6 +98,10 @@
<a href="{{ route('rules.index') }}"> <a href="{{ route('rules.index') }}">
<i class="fa fa-random fa-fw"></i> {{ 'rules'|_ }}</a> <i class="fa fa-random fa-fw"></i> {{ 'rules'|_ }}</a>
</li> </li>
<li class="{{ activeRoutePartial('recurring') }}">
<a href="{{ route('recurring.index') }}">
<i class="fa fa-paint-brush fa-fw"></i> {{ 'recurrences'|_ }}</a>
</li>
</ul> </ul>
</li> </li>

View File

@ -0,0 +1,150 @@
{% extends "./layout/default" %}
{% block breadcrumbs %}
{{ Breadcrumbs.render(Route.getCurrentRoute.getName) }}
{% endblock %}
{% block content %}
<form action="{{ route('recurring.store') }}" method="post" id="store" class="form-horizontal">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<div class="row">
<div class="col-lg-12">
<div class="col-lg-6 col-md-6 col-sm-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'mandatory_for_recurring'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.text('name') }}
{{ ExpandedForm.date('first_date',null, {helpText: trans('firefly.help_first_date')}) }}
{{ ExpandedForm.date('repeat_until',null) }}
{{ ExpandedForm.select('repetition_type', [], null, {helpText: trans('firefly.change_date_other_options')}) }}
{{ ExpandedForm.number('skip', 0) }}
{# three buttons to distinguish type of transaction#}
<div class="form-group" id="name_holder">
<label for="ffInput_type" class="col-sm-4 control-label">
{{ trans('form.transaction_type') }}
</label>
<div class="col-sm-8">
<div class="btn-group btn-group-sm">
<a href="#" class="btn btn-default switch-button" data-value="withdrawal">{{ 'withdrawal'|_ }}</a>
<a href="#" class="btn btn-default switch-button" data-value="deposit">{{ 'deposit'|_ }}</a>
<a href="#" class="btn btn-default switch-button" data-value="transfer">{{ 'transfer'|_ }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'mandatory_for_transaction'|_ }}</h3>
</div>
<div class="box-body">
<p><em>{{ 'mandatory_fields_for_tranaction'|_ }}</em></p>
{{ ExpandedForm.text('transaction_description') }}
{# transaction information (mandatory) #}
{{ ExpandedForm.currencyList('transaction_currency_id', defaultCurrency.id) }}
{{ ExpandedForm.amountNoCurrency('amount', []) }}
{# source account if withdrawal, or if transfer: #}
{{ ExpandedForm.assetAccountList('source_account_id', null, {label: trans('form.asset_source_account')}) }}
{# source account name for deposits: #}
{{ ExpandedForm.text('source_account_name', null, {label: trans('form.revenue_account')}) }}
{# destination if deposit or transfer: #}
{{ ExpandedForm.assetAccountList('destination_account_id', null, {label: trans('form.asset_destination_account')} ) }}
{# destination account name for withdrawals #}
{{ ExpandedForm.text('destination_account_name', null, {label: trans('form.expense_account')}) }}
</div>
</div>
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expected_repetitions'|_ }}</h3>
</div>
<div class="box-body">
Here.
</div>
</div>
</div>
<div class="col-lg-6 col-md-6 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'optional_for_recurring'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.textarea('recurring_description') }}
{{ ExpandedForm.checkbox('active',1) }}
{{ ExpandedForm.checkbox('apply_rules',1) }}
</div>
</div>
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'optional_for_transaction'|_ }}</h3>
</div>
<div class="box-body">
{# transaction information (optional) #}
{{ ExpandedForm.currencyList('foreign_currency_id', defaultCurrency.id) }}
{{ ExpandedForm.amountNoCurrency('foreign_amount', []) }}
{# BUDGET ONLY WHEN CREATING A WITHDRAWAL #}
{% if budgets|length > 1 %}
{{ ExpandedForm.select('budget_id', budgets, null) }}
{% else %}
{{ ExpandedForm.select('budget_id', budgets, null, {helpText: trans('firefly.no_budget_pointer')}) }}
{% endif %}
{# CATEGORY ALWAYS #}
{{ ExpandedForm.text('category') }}
{# TAGS #}
{{ ExpandedForm.text('tags') }}
{# RELATE THIS TRANSFER TO A PIGGY BANK #}
{{ ExpandedForm.select('piggy_bank_id', [], '0') }}
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'options'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.optionsList('create','recurrence') }}
</div>
<div class="box-footer">
<button type="submit" class="btn pull-right btn-success">
{{ ('store_new_recurrence')|_ }}
</button>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="js/lib/modernizr-custom.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript" src="js/lib/bootstrap3-typeahead.min.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript" src="js/lib/bootstrap-tagsinput.min.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript" src="js/lib/jquery-ui.min.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript">
var transactionType = "{{ preFilled.transaction_type }}";
var suggestUri = "{{ route('recurring.suggest') }}";
</script>
<script type="text/javascript" src="js/ff/recurring/create.js?v={{ FF_VERSION }}"></script>
{% endblock %}
{% block styles %}
<link href="css/bootstrap-tagsinput.css?v={{ FF_VERSION }}" type="text/css" rel="stylesheet" media="all">
<link href="css/jquery-ui/jquery-ui.structure.min.css?v={{ FF_VERSION }}" type="text/css" rel="stylesheet" media="all">
<link href="css/jquery-ui/jquery-ui.theme.min.css?v={{ FF_VERSION }}" type="text/css" rel="stylesheet" media="all">
{% endblock %}

View File

@ -0,0 +1,120 @@
{% extends "./layout/default" %}
{% block breadcrumbs %}
{{ Breadcrumbs.render(Route.getCurrentRoute.getName) }}
{% endblock %}
{% block content %}
<!-- block with list of recurring transaction -->
{% if recurring|length > 0 %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ 'recurrences'|_ }}
</h3>
<div class="box-tools pull-right">
<div class="btn-group">
<button class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown"><i class="fa fa-ellipsis-v"></i></button>
<ul class="dropdown-menu" role="menu">
<li><a href="{{ route('recurring.create') }}"><i class="fa fa-plus fa-fw"></i> {{ ('make_new_recurring')|_ }}</a></li>
</ul>
</div>
</div>
</div>
<div class="box-body table-responsive no-padding">
<div style="padding:8px;">
<a href="{{ route('recurring.create') }}" class="btn btn-success"><i class="fa fa-plus fa-fw"></i> {{ ('make_new_recurring')|_ }}
</a>
</div>
<!-- list of recurring here -->
<div style="padding-left:8px;">
{{ recurring.render|raw }}
</div>
<table class="table table-hover sortable">
<thead>
<tr>
<th class="hidden-sm hidden-xs" data-defaultsort="disabled">&nbsp;</th>
<th data-defaultsign="az">{{ trans('list.title') }}</th>
<th data-defaultsign="_19">{{ trans('list.transaction_s') }}</th>
<th data-defaultsort="disabled">{{ trans('list.repetitions') }}</th>
</tr>
</thead>
<tbody>
{% for rt in recurring %}
<tr>
<td class="hidden-sm hidden-xs">
<div class="btn-group btn-group-xs edit_tr_buttons">
<a class="btn btn-default btn-xs" title="{{ 'edit'|_ }}" href="{{ route('recurring.edit',rt.id) }}"><i
class="fa fa-fw fa-pencil"></i></a>
<a class="btn btn-danger btn-xs" title="{{ 'delete'|_ }}" href="{{ route('recurring.delete',rt.id) }}"><i
class="fa fa-fw fa-trash-o"></i></a>
</div>
</td>
<td data-value="{{ rt.title }}">
{{ rt.transaction_type|_ }}:
<a href="{{ route('recurring.show',rt.id) }}">{{ rt.title }}</a>
{% if rt.description|length > 0 %}
<small><br>{{ rt.description }}</small>
{% endif %}
</td>
<td data-value="0">
<ol>
{% for rtt in rt.transactions %}
<li>
{# normal amount + comma#}
{{ formatAmountBySymbol(rtt['amount'],rtt['currency_symbol'],rtt['currency_dp']) }}{% if rtt['foreign_amount'] == null %},{% endif %}
{# foreign amount + comma #}
{% if null != rtt['foreign_amount'] %}
({{ formatAmountBySymbol(rtt['foreign_amount'],rtt['foreign_currency_symbol'],rtt['foreign_currency_dp']) }}),
{% endif %}
<a href="{{ route('accounts.show', rtt['source_account_id']) }}">{{ rtt['source_account_name'] }}</a>
&rarr;
<a href="{{ route('accounts.show', rtt['destination_account_id']) }}">{{ rtt['destination_account_name'] }}</a>
</li>
{% endfor %}
</ol>
</td>
<td>
<ul>
{% for rep in rt.repetitions %}
<li>{{ rep.description }}</li>
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="padding-left:8px;">
{{ recurring.render|raw }}
</div>
</div>
<div class="box-footer">
<a href="{{ route('recurring.create') }}" class="btn btn-success"><i class="fa fa-plus fa-fw"></i> {{ ('make_new_recurring')|_ }}</a>
</div>
</div>
</div>
</div>
{% endif %}
{% if recurring|length == 0 and page == 1 %}
{% include 'partials.empty' with {what: 'default', type: 'recurring',route: route('recurring.create')} %}
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all"/>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}"></script>
{% endblock %}

View File

@ -0,0 +1,190 @@
{% extends "./layout/default" %}
{% block breadcrumbs %}
{{ Breadcrumbs.render(Route.getCurrentRoute.getName, recurrence) }}
{% endblock %}
{% block content %}
<div class="row">
<!-- basic info -->
<div class="col-lg-8 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ array.title }}
</h3>
</div>
<div class="box-body">
<p><em>{{ array.description }}</em></p>
<ul>
{% for rep in array.repetitions %}
<li>{{ rep.description }}</li>
{% endfor %}
</ul>
</div>
<div class="box-footer">
<div class="btn-group">
<a href="{{ route('recurring.edit', [array.id]) }}" class="btn btn-sm btn-default"><i class="fa fa-pencil"></i> {{ 'edit'|_ }}</a>
<a href="{{ route('recurring.delete', [array.id]) }}" class="btn btn-sm btn-danger">{{ 'delete'|_ }} <i class="fa fa-trash"></i></a>
</div>
</div>
</div>
</div>
<!-- next and previous repetitions -->
<div class="col-lg-4 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ 'expected_transactions'|_ }}
</h3>
</div>
<div class="box-body">
<ul>
{% for rep in array.repetitions %}
<li>{{ rep.description }}
<ul>
{% for occ in rep.occurrences %}
<li>{{ occ.formatLocalized(trans('config.month_and_date_day')) }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<div class="box-footer">
<small>
<em>{{ 'warning_duplicates_repetitions'|_ }}</em>
</small>
</div>
</div>
</div>
</div>
<div class="row">
<!-- transactions -->
<div class="col-lg-8 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ 'transaction_data'|_ }}
</h3>
</div>
<div class="box-body no-padding">
<table class="table table-hover sortable">
<thead>
<th data-defaultsign="az">{{ trans('list.source') }}</th>
<th data-defaultsign="az">{{ trans('list.destination') }}</th>
<th data-defaultsign="_19">{{ trans('list.amount') }}</th>
<th data-defaultsign="az">{{ trans('list.category') }}</th>
<th data-defaultsign="az">{{ trans('list.budget') }}</th>
</thead>
<tbody>
{% for transaction in array.transactions %}
<tr>
<td data-value="{{ transaction.source_account_name }}">
<a href="{{ route('accounts.show', [transaction.source_account_id]) }}">{{ transaction.source_account_name }}</a>
</td>
<td data-value="{{ transaction.destination_account_name }}">
<a href="{{ route('accounts.show', [transaction.destination_account_id]) }}">{{ transaction.destination_account_name }}</a>
</td>
<td>
{{ formatAmountBySymbol(transaction.amount,transaction.currency_symbol,transaction.currency_dp) }}
{% if null != transaction.foreign_amount %}
({{ formatAmountBySymbol(transaction.foreign_amount,transaction.foreign_currency_symbol,transaction.foreign_currency_dp) }})
{% endif %}
</td>
<td data-value="{% for meta in transaction.meta %}{% if meta.name == 'category_name' %}{{ meta.category_id }}{% endif %}{% endfor %}">
{% for meta in transaction.meta %}
{% if meta.name == 'category_name' %}
<a href="{{ route('categories.show', [meta.category_id]) }}">
{{ meta.category_name }}
</a>
{% endif %}
{% endfor %}
</td>
<td data-value="{% for meta in transaction.meta %}{% if meta.name == 'budget_id' %}{{ meta.budget_id }}{% endif %}{% endfor %}">
{% for meta in transaction.meta %}
{% if meta.name == 'budget_id' %}
<a href="{{ route('budgets.show', [meta.budget_id]) }}">
{{ meta.budget_name }}
</a>
{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- meta data -->
{% if array.meta|length > 0 %}
<div class="col-lg-4 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ 'meta_data'|_ }}
</h3>
</div>
<div class="box-body no-padding">
<table class="table table-hover sortable">
<thead>
<th style="width:30%;" data-defaultsign="az">{{ trans('list.field') }}</th>
<th data-defaultsign="az">{{ trans('list.value') }}</th>
</thead>
<tbody>
{% for meta in array.meta %}
<tr>
<td>{{ trans('firefly.recurring_meta_field_'~meta.name) }}</td>
<td>
{% if meta.name == 'tags' %}
{% for tag in meta.tags %}
<span class="label label-info">{{ tag }}</span>
{% endfor %}
{% endif %}
{% if meta.name == 'notes' %}
{{ meta.value|markdown }}
{% endif %}
{% if meta.name == 'bill_id' %}
<a href="{{ route('bills.show', [meta.bill_id]) }}">{{ meta.bill_name }}</a>
{% endif %}
{% if meta.name == 'piggy_bank_id' %}
<a href="{{ route('piggy-banks.show', [meta.piggy_bank_id]) }}">{{ meta.piggy_bank_name }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
<div class="row">
<!-- meta data -->
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
{{ 'transactions'|_ }}
</h3>
</div>
<div class="box-body">
Bla bla
</div>
</div>
</div>
</div>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all"/>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}"></script>
{% endblock %}

View File

@ -33,6 +33,7 @@ use FireflyIII\Models\Category;
use FireflyIII\Models\ImportJob; use FireflyIII\Models\ImportJob;
use FireflyIII\Models\LinkType; use FireflyIII\Models\LinkType;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\Tag; use FireflyIII\Models\Tag;
@ -761,6 +762,30 @@ try {
} }
); );
// Recurring transactions controller:
Breadcrumbs::register(
'recurring.index',
function (BreadCrumbsGenerator $breadcrumbs) {
$breadcrumbs->parent('home');
$breadcrumbs->push(trans('firefly.recurrences'), route('recurring.index'));
}
);
Breadcrumbs::register(
'recurring.show',
function (BreadCrumbsGenerator $breadcrumbs, Recurrence $recurrence) {
$breadcrumbs->parent('recurring.index');
$breadcrumbs->push($recurrence->title, route('recurring.show', [$recurrence->id]));
}
);
Breadcrumbs::register(
'recurring.create',
function (BreadCrumbsGenerator $breadcrumbs) {
$breadcrumbs->parent('recurring.index');
$breadcrumbs->push(trans('firefly.create_new_recurrence'), route('recurring.create'));
}
);
// Rules // Rules
Breadcrumbs::register( Breadcrumbs::register(
'rules.index', 'rules.index',

View File

@ -466,28 +466,6 @@ Route::group(
// download config: // download config:
Route::get('download/{importJob}', ['uses' => 'Import\IndexController@download', 'as' => 'job.download']); Route::get('download/{importJob}', ['uses' => 'Import\IndexController@download', 'as' => 'job.download']);
// import method prerequisites:
#
#
#Route::get('reset/{bank}', ['uses' => 'Import\IndexController@reset', 'as' => 'reset']);
// create the job:
#Route::get('create/{bank}', ['uses' => 'Import\IndexController@create', 'as' => 'create-job']);
// configure the job:
#Route::post('configure/{importJob}', ['uses' => 'Import\ConfigurationController@post', 'as' => 'configure.post']);
// get status of any job:
#Route::get('status/{importJob}', ['uses' => 'Import\StatusController@index', 'as' => 'status']);
#Route::get('json/{importJob}', ['uses' => 'Import\StatusController@json', 'as' => 'status.json']);
// start a job
#Route::any('start/{importJob}', ['uses' => 'Import\IndexController@start', 'as' => 'start']);
// download config
#Route::get('download/{importJob}', ['uses' => 'Import\IndexController@download', 'as' => 'download']);
} }
); );
@ -632,6 +610,23 @@ Route::group(
} }
); );
/**
* Recurring Transactions Controller
*/
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Recurring', 'prefix' => 'recurring', 'as' => 'recurring.'], function () {
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
Route::get('suggest', ['uses' => 'IndexController@suggest', 'as' => 'suggest']);
Route::get('show/{recurrence}', ['uses' => 'IndexController@show', 'as' => 'show']);
Route::get('create', ['uses' => 'CreateController@create', 'as' => 'create']);
Route::get('edit/{recurrence}', ['uses' => 'EditController@edit', 'as' => 'edit']);
Route::get('delete/{recurrence}', ['uses' => 'DeleteController@delete', 'as' => 'delete']);
Route::post('store', ['uses' => 'CreateController@store', 'as' => 'store']);
}
);
/** /**
* Report Controller * Report Controller
*/ */