mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Merge branch 'develop' of https://github.com/firefly-iii/firefly-iii into develop
* 'develop' of https://github.com/firefly-iii/firefly-iii: Various updated code for recurring transactions. Don't report authentication exceptions. Fix various bugs in the import routine, discovered by Doug. Prevent index error Spelcheck plz [skip ci] Set demo user back to English at login. Add calendar view. Demo text for recurring. Capital sensitive [skip ci] Various code related to the recurring transactions. Fix missing variable Some code optimalisations. First batch of code for recurring transactions #1469 Fix #1434 Invalidate cache right after storing data #1478 Fix #1475 Remove the option to check for updates from Sandstorm installations. Fix #1474
This commit is contained in:
commit
aa05f7a2d2
@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# make sure we own the volumes:
|
||||
chown -R www-data:www-data -R $FIREFLY_PATH/storage/export $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/logs $FIREFLY_PATH/storage/cache
|
||||
chmod -R 775 $FIREFLY_PATH/storage/export $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/logs $FIREFLY_PATH/storage/cache
|
||||
chown -R www-data:www-data -R $FIREFLY_PATH/storage/export $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/logs $FIREFLY_PATH/storage/framework/cache
|
||||
chmod -R 775 $FIREFLY_PATH/storage/export $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/upload $FIREFLY_PATH/storage/logs $FIREFLY_PATH/storage/framework/cache
|
||||
|
||||
# remove any lingering files that may break upgrades:
|
||||
rm -f $FIREFLY_PATH/storage/logs/laravel.log
|
||||
|
@ -133,18 +133,13 @@ class Handler extends ExceptionHandler
|
||||
if (
|
||||
// if the user wants us to mail:
|
||||
$doMailError === true
|
||||
&& ((
|
||||
// and if is one of these error instances
|
||||
$exception instanceof FireflyException
|
||||
|| $exception instanceof ErrorException
|
||||
|| $exception instanceof OAuthServerException
|
||||
&& (
|
||||
// and if is one of these error instances
|
||||
$exception instanceof FireflyException
|
||||
|| $exception instanceof ErrorException
|
||||
|| $exception instanceof OAuthServerException
|
||||
|
||||
)
|
||||
|| (
|
||||
// or this one, but it's a JSON exception.
|
||||
$exception instanceof AuthenticationException
|
||||
&& Request::expectsJson() === true
|
||||
))
|
||||
)
|
||||
) {
|
||||
// then, send email
|
||||
$userData = [
|
||||
|
@ -26,6 +26,7 @@ namespace FireflyIII\Factory;
|
||||
|
||||
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
@ -105,6 +106,16 @@ class TransactionFactory
|
||||
$destinationType = $this->accountType($journal, 'destination');
|
||||
Log::debug(sprintf('Expect source destination to be of type %s', $destinationType));
|
||||
$destinationAccount = $this->findAccount($destinationType, $data['destination_id'], $data['destination_name']);
|
||||
|
||||
Log::debug(sprintf('Source type is "%s", destination type is "%s"', $sourceType, $destinationType));
|
||||
// throw big fat error when source type === dest type
|
||||
if ($sourceAccount->accountType->type === $destinationAccount->accountType->type) {
|
||||
throw new FireflyException(sprintf('Source and destination account cannot be both of the type "%s"', $destinationAccount->accountType->type));
|
||||
}
|
||||
if ($sourceAccount->accountType->type !== AccountType::ASSET && $destinationAccount->accountType->type !== AccountType::ASSET) {
|
||||
throw new FireflyException('At least one of the accounts must be an asset account.');
|
||||
}
|
||||
|
||||
// first make a "negative" (source) transaction based on the data in the array.
|
||||
$source = $this->create(
|
||||
[
|
||||
|
@ -108,6 +108,27 @@ class UserEventHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Login $event
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function demoUserBackToEnglish(Login $event): bool
|
||||
{
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $event->user;
|
||||
if ($repository->hasRole($user, 'demo')) {
|
||||
// set user back to English.
|
||||
app('preferences')->setForUser($user, 'language', 'en_US');
|
||||
app('preferences')->mark();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserChangedEmail $event
|
||||
*
|
||||
|
@ -41,7 +41,7 @@ class VersionCheckEventHandler
|
||||
/**
|
||||
* @param RequestedVersionCheckStatus $event
|
||||
*/
|
||||
public function checkForUpdates(RequestedVersionCheckStatus $event)
|
||||
public function checkForUpdates(RequestedVersionCheckStatus $event): void
|
||||
{
|
||||
// in Sandstorm, cannot check for updates:
|
||||
$sandstorm = 1 === (int)getenv('SANDSTORM');
|
||||
|
@ -768,6 +768,14 @@ class JournalCollector implements JournalCollectorInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EloquentBuilder
|
||||
*/
|
||||
public function getQuery(): EloquentBuilder
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $set
|
||||
*
|
||||
|
@ -27,6 +27,7 @@ use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@ -35,6 +36,7 @@ use Illuminate\Support\Collection;
|
||||
*/
|
||||
interface JournalCollectorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
*
|
||||
@ -78,6 +80,11 @@ interface JournalCollectorInterface
|
||||
*/
|
||||
public function getPaginatedJournals(): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* @return EloquentBuilder
|
||||
*/
|
||||
public function getQuery(): EloquentBuilder;
|
||||
|
||||
/**
|
||||
* @return JournalCollectorInterface
|
||||
*/
|
||||
|
@ -34,7 +34,6 @@ use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
@ -52,8 +51,6 @@ class AccountController extends Controller
|
||||
{
|
||||
/** @var CurrencyRepositoryInterface */
|
||||
private $currencyRepos;
|
||||
/** @var JournalRepositoryInterface */
|
||||
private $journalRepos;
|
||||
/** @var AccountRepositoryInterface */
|
||||
private $repository;
|
||||
|
||||
@ -72,7 +69,6 @@ class AccountController extends Controller
|
||||
|
||||
$this->repository = app(AccountRepositoryInterface::class);
|
||||
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
$this->journalRepos = app(JournalRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
@ -51,8 +51,9 @@ class HomeController extends Controller
|
||||
{
|
||||
$title = (string)trans('firefly.administration');
|
||||
$mainTitleIcon = 'fa-hand-spock-o';
|
||||
$sandstorm = 1 === (int)getenv('SANDSTORM');
|
||||
|
||||
return view('admin.index', compact('title', 'mainTitleIcon'));
|
||||
return view('admin.index', compact('title', 'mainTitleIcon','sandstorm'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,13 +23,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers;
|
||||
|
||||
use Artisan;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Middleware\IsDemoUser;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Log;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Preferences;
|
||||
use Route as RouteFacade;
|
||||
|
||||
/**
|
||||
* Class DebugController
|
||||
@ -45,6 +50,52 @@ class DebugController extends Controller
|
||||
$this->middleware(IsDemoUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function displayError()
|
||||
{
|
||||
Log::debug('This is a test message at the DEBUG level.');
|
||||
Log::info('This is a test message at the INFO level.');
|
||||
Log::notice('This is a test message at the NOTICE level.');
|
||||
Log::warning('This is a test message at the WARNING level.');
|
||||
Log::error('This is a test message at the ERROR level.');
|
||||
Log::critical('This is a test message at the CRITICAL level.');
|
||||
Log::alert('This is a test message at the ALERT level.');
|
||||
Log::emergency('This is a test message at the EMERGENCY level.');
|
||||
throw new FireflyException('A very simple test error.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function flush(Request $request)
|
||||
{
|
||||
Preferences::mark();
|
||||
$request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range']);
|
||||
Log::debug('Call cache:clear...');
|
||||
Artisan::call('cache:clear');
|
||||
Log::debug('Call config:clear...');
|
||||
Artisan::call('config:clear');
|
||||
Log::debug('Call route:clear...');
|
||||
Artisan::call('route:clear');
|
||||
Log::debug('Call twig:clean...');
|
||||
try {
|
||||
Artisan::call('twig:clean');
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Exception $e) {
|
||||
// don't care
|
||||
Log::debug('Called twig:clean.');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
Log::debug('Call view:clear...');
|
||||
Artisan::call('view:clear');
|
||||
Log::debug('Done! Redirecting...');
|
||||
|
||||
return redirect(route('index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
@ -120,6 +171,61 @@ class DebugController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function routes(): string
|
||||
{
|
||||
$set = RouteFacade::getRoutes();
|
||||
$ignore = ['chart.', 'javascript.', 'json.', 'report-data.', 'popup.', 'debugbar.', 'attachments.download', 'attachments.preview',
|
||||
'bills.rescan', 'budgets.income', 'currencies.def', 'error', 'flush', 'help.show', 'import.file',
|
||||
'login', 'logout', 'password.reset', 'profile.confirm-email-change', 'profile.undo-email-change',
|
||||
'register', 'report.options', 'routes', 'rule-groups.down', 'rule-groups.up', 'rules.up', 'rules.down',
|
||||
'rules.select', 'search.search', 'test-flash', 'transactions.link.delete', 'transactions.link.switch',
|
||||
'two-factor.lost', 'reports.options', 'debug', 'import.create-job', 'import.download', 'import.start', 'import.status.json',
|
||||
'preferences.delete-code', 'rules.test-triggers', 'piggy-banks.remove-money', 'piggy-banks.add-money',
|
||||
'accounts.reconcile.transactions', 'accounts.reconcile.overview', 'export.download',
|
||||
'transactions.clone', 'two-factor.index', 'api.v1', 'installer.','attachments.view','import.create',
|
||||
'import.job.download','import.job.start','import.job.status.json','import.job.store','recurring.events',
|
||||
'recurring.suggest'
|
||||
];
|
||||
$return = ' ';
|
||||
/** @var Route $route */
|
||||
foreach ($set as $route) {
|
||||
$name = $route->getName();
|
||||
if (null !== $name && \strlen($name) > 0 && \in_array('GET', $route->methods(), true)) {
|
||||
|
||||
$found = false;
|
||||
foreach ($ignore as $string) {
|
||||
if (!(false === stripos($name, $string))) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found === false) {
|
||||
$return .= 'touch ' . $route->getName() . '.md;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function testFlash(Request $request)
|
||||
{
|
||||
$request->session()->flash('success', 'This is a success message.');
|
||||
$request->session()->flash('info', 'This is an info message.');
|
||||
$request->session()->flash('warning', 'This is a warning.');
|
||||
$request->session()->flash('error', 'This is an error!');
|
||||
|
||||
return redirect(route('home'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Some common combinations.
|
||||
*
|
||||
@ -151,7 +257,7 @@ class DebugController extends Controller
|
||||
private function collectPackages(): array
|
||||
{
|
||||
$packages = [];
|
||||
$file = realpath(__DIR__ . '/../../../vendor/composer/installed.json');
|
||||
$file = \dirname(__DIR__, 3) . '/vendor/composer/installed.json';
|
||||
if (!($file === false) && file_exists($file)) {
|
||||
// file exists!
|
||||
$content = file_get_contents($file);
|
||||
|
@ -99,52 +99,7 @@ class HomeController extends Controller
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function displayError()
|
||||
{
|
||||
Log::debug('This is a test message at the DEBUG level.');
|
||||
Log::info('This is a test message at the INFO level.');
|
||||
Log::notice('This is a test message at the NOTICE level.');
|
||||
Log::warning('This is a test message at the WARNING level.');
|
||||
Log::error('This is a test message at the ERROR level.');
|
||||
Log::critical('This is a test message at the CRITICAL level.');
|
||||
Log::alert('This is a test message at the ALERT level.');
|
||||
Log::emergency('This is a test message at the EMERGENCY level.');
|
||||
throw new FireflyException('A very simple test error.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function flush(Request $request)
|
||||
{
|
||||
Preferences::mark();
|
||||
$request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range']);
|
||||
Log::debug('Call cache:clear...');
|
||||
Artisan::call('cache:clear');
|
||||
Log::debug('Call config:clear...');
|
||||
Artisan::call('config:clear');
|
||||
Log::debug('Call route:clear...');
|
||||
Artisan::call('route:clear');
|
||||
Log::debug('Call twig:clean...');
|
||||
try {
|
||||
Artisan::call('twig:clean');
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Exception $e) {
|
||||
// don't care
|
||||
Log::debug('Called twig:clean.');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
Log::debug('Call view:clear...');
|
||||
Artisan::call('view:clear');
|
||||
Log::debug('Done! Redirecting...');
|
||||
|
||||
return redirect(route('index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccountRepositoryInterface $repository
|
||||
@ -193,56 +148,4 @@ class HomeController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function routes()
|
||||
{
|
||||
$set = RouteFacade::getRoutes();
|
||||
$ignore = ['chart.', 'javascript.', 'json.', 'report-data.', 'popup.', 'debugbar.', 'attachments.download', 'attachments.preview',
|
||||
'bills.rescan', 'budgets.income', 'currencies.def', 'error', 'flush', 'help.show', 'import.file',
|
||||
'login', 'logout', 'password.reset', 'profile.confirm-email-change', 'profile.undo-email-change',
|
||||
'register', 'report.options', 'routes', 'rule-groups.down', 'rule-groups.up', 'rules.up', 'rules.down',
|
||||
'rules.select', 'search.search', 'test-flash', 'transactions.link.delete', 'transactions.link.switch',
|
||||
'two-factor.lost', 'reports.options', 'debug', 'import.create-job', 'import.download', 'import.start', 'import.status.json',
|
||||
'preferences.delete-code', 'rules.test-triggers', 'piggy-banks.remove-money', 'piggy-banks.add-money',
|
||||
'accounts.reconcile.transactions', 'accounts.reconcile.overview', 'export.download',
|
||||
'transactions.clone', 'two-factor.index',
|
||||
];
|
||||
$return = ' ';
|
||||
/** @var Route $route */
|
||||
foreach ($set as $route) {
|
||||
$name = $route->getName();
|
||||
if (null !== $name && \in_array('GET', $route->methods()) && \strlen($name) > 0) {
|
||||
|
||||
$found = false;
|
||||
foreach ($ignore as $string) {
|
||||
if (!(false === stripos($name, $string))) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found === false) {
|
||||
$return .= 'touch ' . $route->getName() . '.md;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function testFlash(Request $request)
|
||||
{
|
||||
$request->session()->flash('success', 'This is a success message.');
|
||||
$request->session()->flash('info', 'This is an info message.');
|
||||
$request->session()->flash('warning', 'This is a warning.');
|
||||
$request->session()->flash('error', 'This is an error!');
|
||||
|
||||
return redirect(route('home'));
|
||||
}
|
||||
}
|
||||
|
96
app/Http/Controllers/Recurring/CreateController.php
Normal file
96
app/Http/Controllers/Recurring/CreateController.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?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();
|
||||
|
||||
// types of repetitions:
|
||||
$typesOfRepetitions = [
|
||||
'forever' => trans('firefly.repeat_forever'),
|
||||
'until_date' => trans('firefly.repeat_until_date'),
|
||||
'times' => trans('firefly.repeat_times'),
|
||||
];
|
||||
|
||||
// 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','typesOfRepetitions', 'defaultCurrency', 'budgets'));
|
||||
}
|
||||
|
||||
}
|
66
app/Http/Controllers/Recurring/EditController.php
Normal file
66
app/Http/Controllers/Recurring/EditController.php
Normal 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'));
|
||||
}
|
||||
|
||||
|
||||
}
|
203
app/Http/Controllers/Recurring/IndexController.php
Normal file
203
app/Http/Controllers/Recurring/IndexController.php
Normal file
@ -0,0 +1,203 @@
|
||||
<?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\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\RecurrenceRepetition;
|
||||
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
|
||||
*
|
||||
* @throws FireflyException
|
||||
* @return JsonResponse
|
||||
*/
|
||||
function events(RecurringRepositoryInterface $repository, Request $request): JsonResponse
|
||||
{
|
||||
$return = [];
|
||||
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
|
||||
$endsAt = (string)$request->get('ends');
|
||||
$repetitionType = explode(',', $request->get('type'))[0];
|
||||
$repetitionMoment = '';
|
||||
|
||||
switch ($repetitionType) {
|
||||
default:
|
||||
throw new FireflyException(sprintf('Cannot handle repetition type "%s"', $repetitionType));
|
||||
case 'daily':
|
||||
break;
|
||||
case 'weekly':
|
||||
case 'monthly':
|
||||
$repetitionMoment = explode(',', $request->get('type'))[1] ?? '1';
|
||||
break;
|
||||
case 'ndom':
|
||||
$repetitionMoment = explode(',', $request->get('type'))[1] ?? '1,1';
|
||||
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:
|
||||
switch ($endsAt) {
|
||||
default:
|
||||
throw new FireflyException(sprintf('Cannot generate events for "%s"', $endsAt));
|
||||
case 'forever':
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
|
||||
return Response::json($return);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
@ -34,6 +34,7 @@ use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Transformers\TransactionTransformer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
@ -96,6 +97,9 @@ class TransactionController extends Controller
|
||||
if ($end < $start) {
|
||||
[$start, $end] = [$end, $start];
|
||||
}
|
||||
|
||||
$path = route('transactions.index', [$what, $start->format('Y-m-d'), $end->format('Y-m-d')]);
|
||||
|
||||
$startStr = $start->formatLocalized($this->monthAndDayFormat);
|
||||
$endStr = $end->formatLocalized($this->monthAndDayFormat);
|
||||
$subTitle = trans('firefly.title_' . $what . '_between', ['start' => $startStr, 'end' => $endStr]);
|
||||
@ -150,9 +154,9 @@ class TransactionController extends Controller
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function reconcile(Request $request)
|
||||
public function reconcile(Request $request): JsonResponse
|
||||
{
|
||||
$transactionIds = $request->get('transactions');
|
||||
foreach ($transactionIds as $transactionId) {
|
||||
|
@ -91,7 +91,7 @@ class SplitJournalFormRequest extends Request
|
||||
'currency_id' => $this->integer('journal_currency_id'),
|
||||
'currency_code' => null,
|
||||
'description' => $transaction['transaction_description'] ?? '',
|
||||
'amount' => $transaction['amount'],
|
||||
'amount' => $transaction['amount'] ?? '',
|
||||
'budget_id' => (int)($transaction['budget_id'] ?? 0.0),
|
||||
'budget_name' => null,
|
||||
'category_id' => null,
|
||||
|
@ -109,6 +109,11 @@ class Amount implements ConverterInterface
|
||||
*/
|
||||
private function stripAmount(string $value): string
|
||||
{
|
||||
if (0 === strpos($value, '--')) {
|
||||
$value = substr($value, 2);
|
||||
}
|
||||
|
||||
|
||||
$str = preg_replace('/[^\-\(\)\.\,0-9 ]/', '', $value);
|
||||
$len = \strlen($str);
|
||||
if ('(' === $str[0] && ')' === $str[$len - 1]) {
|
||||
|
@ -108,6 +108,8 @@ class ImportArrayStorage
|
||||
$this->setStatus('rules_applied');
|
||||
}
|
||||
|
||||
app('preferences')->mark();
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
@ -301,7 +303,7 @@ class ImportArrayStorage
|
||||
'existing' => $existingId,
|
||||
'description' => $transaction['description'] ?? '',
|
||||
'amount' => $transaction['transactions'][0]['amount'] ?? 0,
|
||||
'date' => isset($transaction['date']) ? $transaction['date'] : '',
|
||||
'date' => $transaction['date'] ?? '',
|
||||
]
|
||||
);
|
||||
|
||||
@ -411,7 +413,14 @@ class ImportArrayStorage
|
||||
$store['date'] = Carbon::createFromFormat('Y-m-d', $store['date']);
|
||||
$store['description'] = $store['description'] === '' ? '(empty description)' : $store['description'];
|
||||
// store the journal.
|
||||
$journal = $this->journalRepos->store($store);
|
||||
try {
|
||||
$journal = $this->journalRepos->store($store);
|
||||
} catch(FireflyException $e) {
|
||||
Log::error($e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
$this->repository->addErrorMessage($this->importJob, sprintf('Row #%d could not be imported. %s', $index, $e->getMessage()));
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Stored as journal #%d', $journal->id));
|
||||
$collection->push($journal);
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
* @property int $transaction_currency_id
|
||||
* @property string $amount_min
|
||||
* @property string $amount_max
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
*/
|
||||
class Bill extends Model
|
||||
{
|
||||
|
@ -31,6 +31,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Class Budget.
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
*/
|
||||
class Budget extends Model
|
||||
{
|
||||
|
@ -31,6 +31,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Class Category.
|
||||
*
|
||||
* @property string $name
|
||||
* @property int $id
|
||||
*/
|
||||
class Category extends Model
|
||||
{
|
||||
|
@ -26,6 +26,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class Note.
|
||||
*
|
||||
* @property string $text
|
||||
*/
|
||||
class Note extends Model
|
||||
{
|
||||
|
@ -36,6 +36,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
* @property Carbon $targetdate
|
||||
* @property Carbon $startdate
|
||||
* @property string $targetamount
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
*
|
||||
*/
|
||||
class PiggyBank extends Model
|
||||
|
159
app/Models/Recurrence.php
Normal file
159
app/Models/Recurrence.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
49
app/Models/RecurrenceMeta.php
Normal file
49
app/Models/RecurrenceMeta.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
53
app/Models/RecurrenceRepetition.php
Normal file
53
app/Models/RecurrenceRepetition.php
Normal 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);
|
||||
}
|
||||
}
|
105
app/Models/RecurrenceTransaction.php
Normal file
105
app/Models/RecurrenceTransaction.php
Normal 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);
|
||||
}
|
||||
}
|
49
app/Models/RecurrenceTransactionMeta.php
Normal file
49
app/Models/RecurrenceTransactionMeta.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -25,6 +25,7 @@ namespace FireflyIII\Models;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@ -252,18 +253,18 @@ class Transaction extends Model
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function transactionCurrency()
|
||||
public function transactionCurrency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TransactionCurrency::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function transactionJournal()
|
||||
public function transactionJournal(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TransactionJournal::class);
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
* Class TransactionCurrency.
|
||||
*
|
||||
* @property string $code
|
||||
* @property string $symbol
|
||||
* @property int $decimal_places
|
||||
*
|
||||
*/
|
||||
|
@ -64,6 +64,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
// is a User related event.
|
||||
Login::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
|
||||
|
||||
],
|
||||
RequestedVersionCheckStatus::class => [
|
||||
|
63
app/Providers/RecurringServiceProvider.php
Normal file
63
app/Providers/RecurringServiceProvider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -24,6 +24,7 @@ namespace FireflyIII\Repositories\Journal;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Factory\TransactionJournalFactory;
|
||||
use FireflyIII\Factory\TransactionJournalMetaFactory;
|
||||
use FireflyIII\Models\Account;
|
||||
@ -738,8 +739,7 @@ class JournalRepository implements JournalRepositoryInterface
|
||||
*
|
||||
* @return TransactionJournal
|
||||
*
|
||||
* @throws \FireflyIII\Exceptions\FireflyException
|
||||
* @throws \FireflyIII\Exceptions\FireflyException
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function store(array $data): TransactionJournal
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Repositories\Journal;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\Note;
|
||||
use FireflyIII\Models\Transaction;
|
||||
@ -325,7 +326,7 @@ interface JournalRepositoryInterface
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*
|
||||
* @throws FireflyException
|
||||
* @return TransactionJournal
|
||||
*/
|
||||
public function store(array $data): TransactionJournal;
|
||||
|
@ -203,6 +203,21 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $piggyBankId
|
||||
*
|
||||
* @return PiggyBank|null
|
||||
*/
|
||||
public function findNull(int $piggyBankId): ?PiggyBank
|
||||
{
|
||||
$piggyBank = $this->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']);
|
||||
if (null !== $piggyBank) {
|
||||
return $piggyBank;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current amount saved in piggy bank.
|
||||
*
|
||||
|
@ -102,11 +102,17 @@ interface PiggyBankRepositoryInterface
|
||||
|
||||
/**
|
||||
* @param int $piggyBankid
|
||||
*
|
||||
* @deprecated
|
||||
* @return PiggyBank
|
||||
*/
|
||||
public function find(int $piggyBankid): PiggyBank;
|
||||
|
||||
/**
|
||||
* @param int $piggyBankId
|
||||
* @return PiggyBank|null
|
||||
*/
|
||||
public function findNull(int $piggyBankId): ?PiggyBank;
|
||||
|
||||
/**
|
||||
* Find by name or return NULL.
|
||||
*
|
||||
|
226
app/Repositories/Recurring/RecurringRepository.php
Normal file
226
app/Repositories/Recurring/RecurringRepository.php
Normal 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 getXOccurrences(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;
|
||||
}
|
||||
}
|
84
app/Repositories/Recurring/RecurringRepositoryInterface.php
Normal file
84
app/Repositories/Recurring/RecurringRepositoryInterface.php
Normal 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 getXOccurrences(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;
|
||||
|
||||
}
|
@ -49,13 +49,13 @@ class FixerIOv2 implements ExchangeRateInterface
|
||||
public function getRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate
|
||||
{
|
||||
// create new exchange rate with default values.
|
||||
// create new currency exchange rate object:
|
||||
$rate = 0;
|
||||
$exchangeRate = new CurrencyExchangeRate;
|
||||
$exchangeRate->user()->associate($this->user);
|
||||
$exchangeRate->fromCurrency()->associate($fromCurrency);
|
||||
$exchangeRate->toCurrency()->associate($toCurrency);
|
||||
$exchangeRate->date = $date;
|
||||
$exchangeRate->rate = 0;
|
||||
$exchangeRate->rate = $rate;
|
||||
|
||||
// get API key
|
||||
$apiKey = env('FIXER_API_KEY', '');
|
||||
|
@ -190,6 +190,7 @@ trait TransactionServiceTrait
|
||||
*/
|
||||
protected function findCategory(?int $categoryId, ?string $categoryName): ?Category
|
||||
{
|
||||
Log::debug(sprintf('Going to find or create category #%d, with name "%s"', $categoryId, $categoryName));
|
||||
/** @var CategoryFactory $factory */
|
||||
$factory = app(CategoryFactory::class);
|
||||
$factory->setUser($this->user);
|
||||
|
@ -24,7 +24,6 @@ namespace FireflyIII\Support;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Collection;
|
||||
use Preferences as Prefs;
|
||||
|
||||
/**
|
||||
* Class CacheProperties.
|
||||
@ -44,7 +43,7 @@ class CacheProperties
|
||||
$this->properties = new Collection;
|
||||
if (auth()->check()) {
|
||||
$this->addProperty(auth()->user()->id);
|
||||
$this->addProperty(Prefs::lastActivity());
|
||||
$this->addProperty(app('preferences')->lastActivity());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,9 @@ class ImportTransaction
|
||||
$this->budgetName = $columnValue->getValue();
|
||||
break;
|
||||
case 'category-id':
|
||||
$this->categoryId = $this->getMappedValue($columnValue);
|
||||
$value = $this->getMappedValue($columnValue);
|
||||
Log::debug(sprintf('Set category ID to %d in ImportTransaction object', $value));
|
||||
$this->categoryId = $value;
|
||||
break;
|
||||
case 'category-name':
|
||||
$this->categoryName = $columnValue->getValue();
|
||||
|
@ -279,11 +279,14 @@ class ImportableConverter
|
||||
*/
|
||||
private function verifyObjectId(string $key, int $objectId): ?int
|
||||
{
|
||||
|
||||
if (isset($this->mappedValues[$key]) && \in_array($objectId, $this->mappedValues[$key], true)) {
|
||||
Log::debug(sprintf('verifyObjectId(%s, %d) is valid!',$key, $objectId));
|
||||
return $objectId;
|
||||
}
|
||||
|
||||
return null;
|
||||
Log::debug(sprintf('verifyObjectId(%s, %d) is NOT in the list, but it could still be valid.',$key, $objectId));
|
||||
return $objectId;
|
||||
}
|
||||
|
||||
|
||||
|
@ -87,6 +87,7 @@ class MappedValuesValidator
|
||||
$return = [];
|
||||
Log::debug('Now in validateMappedValues()');
|
||||
foreach ($mappings as $role => $values) {
|
||||
Log::debug(sprintf('Now at role "%s"', $role));
|
||||
$values = array_unique($values);
|
||||
if (\count($values) > 0) {
|
||||
switch ($role) {
|
||||
@ -115,9 +116,11 @@ class MappedValuesValidator
|
||||
$return[$role] = $valid;
|
||||
break;
|
||||
case 'category-id':
|
||||
Log::debug('Going to validate these category ids: ', $values);
|
||||
$set = $this->catRepos->getByIds($values);
|
||||
$valid = $set->pluck('id')->toArray();
|
||||
$return[$role] = $valid;
|
||||
Log::debug('Valid category IDs are: ', $valid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
256
app/Transformers/RecurrenceTransformer.php
Normal file
256
app/Transformers/RecurrenceTransformer.php
Normal 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->getXOccurrences($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;
|
||||
}
|
||||
|
||||
}
|
12
app/User.php
12
app/User.php
@ -36,6 +36,7 @@ use FireflyIII\Models\ExportJob;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\Preference;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\Role;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
@ -291,6 +292,17 @@ class User extends Authenticatable
|
||||
return $this->hasMany(Preference::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* Link to recurring transactions.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function recurrences(): HasMany
|
||||
{
|
||||
return $this->hasMany(Recurrence::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* Link to roles.
|
||||
|
@ -98,6 +98,7 @@ return [
|
||||
FireflyIII\Providers\SearchServiceProvider::class,
|
||||
FireflyIII\Providers\TagServiceProvider::class,
|
||||
FireflyIII\Providers\AdminServiceProvider::class,
|
||||
FireflyIII\Providers\RecurringServiceProvider::class,
|
||||
|
||||
|
||||
],
|
||||
|
@ -271,6 +271,7 @@ return [
|
||||
'piggyBank' => \FireflyIII\Models\PiggyBank::class,
|
||||
'tj' => \FireflyIII\Models\TransactionJournal::class,
|
||||
'tag' => \FireflyIII\Models\Tag::class,
|
||||
'recurrence' => \FireflyIII\Models\Recurrence::class,
|
||||
'rule' => \FireflyIII\Models\Rule::class,
|
||||
'ruleGroup' => \FireflyIII\Models\RuleGroup::class,
|
||||
'exportJob' => \FireflyIII\Models\ExportJob::class,
|
||||
|
@ -37,7 +37,6 @@ class ChangesForV474 extends Migration
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
|
130
database/migrations/2018_06_08_200526_changes_for_v475.php
Normal file
130
database/migrations/2018_06_08_200526_changes_for_v475.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?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->smallInteger('repetitions', false, true);
|
||||
|
||||
$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');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
221
public/js/ff/recurring/create.js
vendored
Normal file
221
public/js/ff/recurring/create.js
vendored
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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();
|
||||
respondToRepetitionEnd();
|
||||
$('.switch-button').on('click', switchTransactionType);
|
||||
$('#ffInput_repetition_end').on('change', respondToRepetitionEnd);
|
||||
$('#ffInput_first_date').on('change', respondToFirstDateChange);
|
||||
|
||||
$('#calendar-link').on('click', showRepCalendar);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function showRepCalendar() {
|
||||
|
||||
// pre-append URL with repetition info:
|
||||
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 += '&reps=' + $('#ffInput_repetitions').val();
|
||||
|
||||
|
||||
$('#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
|
||||
});
|
||||
$('#calendarModal').modal('show');
|
||||
return false;
|
||||
}
|
||||
|
||||
function respondToRepetitionEnd() {
|
||||
var obj = $('#ffInput_repetition_end');
|
||||
var value = obj.val();
|
||||
switch (value) {
|
||||
case 'forever':
|
||||
$('#repeat_until_holder').hide();
|
||||
$('#repetitions_holder').hide();
|
||||
break;
|
||||
case 'until_date':
|
||||
$('#repeat_until_holder').show();
|
||||
$('#repetitions_holder').hide();
|
||||
break;
|
||||
case 'times':
|
||||
$('#repeat_until_holder').hide();
|
||||
$('#repetitions_holder').show();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
2
public/js/ff/transactions/single/create.js
vendored
2
public/js/ff/transactions/single/create.js
vendored
@ -142,7 +142,7 @@ function updateLayout() {
|
||||
$('#subTitle').text(title[what]);
|
||||
$('.breadcrumb .active').text(breadcrumbs[what]);
|
||||
$('.breadcrumb li:nth-child(2)').html('<a href="' + middleCrumbUrl[what] + '">' + middleCrumbName[what] + '</a>');
|
||||
$('#transaction-btn').text(button[what]);
|
||||
$('.transaction-btn').text(button[what]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
1293
public/lib/fc/fullcalendar.css
vendored
Normal file
1293
public/lib/fc/fullcalendar.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
15010
public/lib/fc/fullcalendar.js
vendored
Normal file
15010
public/lib/fc/fullcalendar.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
public/lib/fc/fullcalendar.min.css
vendored
Normal file
5
public/lib/fc/fullcalendar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
public/lib/fc/fullcalendar.min.js
vendored
Normal file
12
public/lib/fc/fullcalendar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
176
public/lib/fc/fullcalendar.print.css
vendored
Normal file
176
public/lib/fc/fullcalendar.print.css
vendored
Normal file
@ -0,0 +1,176 @@
|
||||
/*!
|
||||
* FullCalendar v3.9.0
|
||||
* Docs & License: https://fullcalendar.io/
|
||||
* (c) 2018 Adam Shaw
|
||||
*/
|
||||
/*!
|
||||
* FullCalendar v3.9.0 Print Stylesheet
|
||||
* Docs & License: https://fullcalendar.io/
|
||||
* (c) 2018 Adam Shaw
|
||||
*/
|
||||
/*
|
||||
* Include this stylesheet on your page to get a more printer-friendly calendar.
|
||||
* When including this stylesheet, use the media='print' attribute of the <link> tag.
|
||||
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
|
||||
*/
|
||||
.fc {
|
||||
max-width: 100% !important; }
|
||||
|
||||
/* Global Event Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-event {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
page-break-inside: avoid; }
|
||||
|
||||
.fc-event .fc-resizer {
|
||||
display: none; }
|
||||
|
||||
/* Table & Day-Row Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc th,
|
||||
.fc td,
|
||||
.fc hr,
|
||||
.fc thead,
|
||||
.fc tbody,
|
||||
.fc-row {
|
||||
border-color: #ccc !important;
|
||||
background: #fff !important; }
|
||||
|
||||
/* kill the overlaid, absolutely-positioned components */
|
||||
/* common... */
|
||||
.fc-bg,
|
||||
.fc-bgevent-skeleton,
|
||||
.fc-highlight-skeleton,
|
||||
.fc-helper-skeleton,
|
||||
.fc-bgevent-container,
|
||||
.fc-business-container,
|
||||
.fc-highlight-container,
|
||||
.fc-helper-container {
|
||||
display: none; }
|
||||
|
||||
/* don't force a min-height on rows (for DayGrid) */
|
||||
.fc tbody .fc-row {
|
||||
height: auto !important;
|
||||
/* undo height that JS set in distributeHeight */
|
||||
min-height: 0 !important;
|
||||
/* undo the min-height from each view's specific stylesheet */ }
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton {
|
||||
position: static;
|
||||
/* undo .fc-rigid */
|
||||
padding-bottom: 0 !important;
|
||||
/* use a more border-friendly method for this... */ }
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td {
|
||||
/* only works in newer browsers */
|
||||
padding-bottom: 1em;
|
||||
/* ...gives space within the skeleton. also ensures min height in a way */ }
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton table {
|
||||
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
|
||||
making it look more like 3em. for other browers, it will already be this tall */
|
||||
height: 1em; }
|
||||
|
||||
/* Undo month-view event limiting. Display all events and hide the "more" links
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-more-cell,
|
||||
.fc-more {
|
||||
display: none !important; }
|
||||
|
||||
.fc tr.fc-limited {
|
||||
display: table-row !important; }
|
||||
|
||||
.fc td.fc-limited {
|
||||
display: table-cell !important; }
|
||||
|
||||
.fc-popover {
|
||||
display: none;
|
||||
/* never display the "more.." popover in print mode */ }
|
||||
|
||||
/* TimeGrid Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
/* undo the min-height 100% trick used to fill the container's height */
|
||||
.fc-time-grid {
|
||||
min-height: 0 !important; }
|
||||
|
||||
/* don't display the side axis at all ("all-day" and time cells) */
|
||||
.fc-agenda-view .fc-axis {
|
||||
display: none; }
|
||||
|
||||
/* don't display the horizontal lines */
|
||||
.fc-slats,
|
||||
.fc-time-grid hr {
|
||||
/* this hr is used when height is underused and needs to be filled */
|
||||
display: none !important;
|
||||
/* important overrides inline declaration */ }
|
||||
|
||||
/* let the container that holds the events be naturally positioned and create real height */
|
||||
.fc-time-grid .fc-content-skeleton {
|
||||
position: static; }
|
||||
|
||||
/* in case there are no events, we still want some height */
|
||||
.fc-time-grid .fc-content-skeleton table {
|
||||
height: 4em; }
|
||||
|
||||
/* kill the horizontal spacing made by the event container. event margins will be done below */
|
||||
.fc-time-grid .fc-event-container {
|
||||
margin: 0 !important; }
|
||||
|
||||
/* TimeGrid *Event* Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
/* naturally position events, vertically stacking them */
|
||||
.fc-time-grid .fc-event {
|
||||
position: static !important;
|
||||
margin: 3px 2px !important; }
|
||||
|
||||
/* for events that continue to a future day, give the bottom border back */
|
||||
.fc-time-grid .fc-event.fc-not-end {
|
||||
border-bottom-width: 1px !important; }
|
||||
|
||||
/* indicate the event continues via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-end:after {
|
||||
content: "..."; }
|
||||
|
||||
/* for events that are continuations from previous days, give the top border back */
|
||||
.fc-time-grid .fc-event.fc-not-start {
|
||||
border-top-width: 1px !important; }
|
||||
|
||||
/* indicate the event is a continuation via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-start:before {
|
||||
content: "..."; }
|
||||
|
||||
/* time */
|
||||
/* undo a previous declaration and let the time text span to a second line */
|
||||
.fc-time-grid .fc-event .fc-time {
|
||||
white-space: normal !important; }
|
||||
|
||||
/* hide the the time that is normally displayed... */
|
||||
.fc-time-grid .fc-event .fc-time span {
|
||||
display: none; }
|
||||
|
||||
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
|
||||
.fc-time-grid .fc-event .fc-time:after {
|
||||
content: attr(data-full); }
|
||||
|
||||
/* Vertical Scroller & Containers
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
/* kill the scrollbars and allow natural height */
|
||||
.fc-scroller,
|
||||
.fc-day-grid-container,
|
||||
.fc-time-grid-container {
|
||||
/* */
|
||||
overflow: visible !important;
|
||||
height: auto !important; }
|
||||
|
||||
/* kill the horizontal border/padding used to compensate for scrollbars */
|
||||
.fc-row {
|
||||
border: 0 !important;
|
||||
margin: 0 !important; }
|
||||
|
||||
/* Button Controls
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
.fc-button-group,
|
||||
.fc button {
|
||||
display: none;
|
||||
/* don't display any button-related controls */ }
|
9
public/lib/fc/fullcalendar.print.min.css
vendored
Normal file
9
public/lib/fc/fullcalendar.print.min.css
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* FullCalendar v3.9.0
|
||||
* Docs & License: https://fullcalendar.io/
|
||||
* (c) 2018 Adam Shaw
|
||||
*//*!
|
||||
* FullCalendar v3.9.0 Print Stylesheet
|
||||
* Docs & License: https://fullcalendar.io/
|
||||
* (c) 2018 Adam Shaw
|
||||
*/.fc-bg,.fc-bgevent-container,.fc-bgevent-skeleton,.fc-business-container,.fc-event .fc-resizer,.fc-helper-container,.fc-helper-skeleton,.fc-highlight-container,.fc-highlight-skeleton{display:none}.fc tbody .fc-row,.fc-time-grid{min-height:0!important}.fc-time-grid .fc-event.fc-not-end:after,.fc-time-grid .fc-event.fc-not-start:before{content:"..."}.fc{max-width:100%!important}.fc-event{background:#fff!important;color:#000!important;page-break-inside:avoid}.fc hr,.fc tbody,.fc td,.fc th,.fc thead,.fc-row{border-color:#ccc!important;background:#fff!important}.fc tbody .fc-row{height:auto!important}.fc tbody .fc-row .fc-content-skeleton{position:static;padding-bottom:0!important}.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td{padding-bottom:1em}.fc tbody .fc-row .fc-content-skeleton table{height:1em}.fc-more,.fc-more-cell{display:none!important}.fc tr.fc-limited{display:table-row!important}.fc td.fc-limited{display:table-cell!important}.fc-agenda-view .fc-axis,.fc-popover{display:none}.fc-slats,.fc-time-grid hr{display:none!important}.fc button,.fc-button-group,.fc-time-grid .fc-event .fc-time span{display:none}.fc-time-grid .fc-content-skeleton{position:static}.fc-time-grid .fc-content-skeleton table{height:4em}.fc-time-grid .fc-event-container{margin:0!important}.fc-time-grid .fc-event{position:static!important;margin:3px 2px!important}.fc-time-grid .fc-event.fc-not-end{border-bottom-width:1px!important}.fc-time-grid .fc-event.fc-not-start{border-top-width:1px!important}.fc-time-grid .fc-event .fc-time{white-space:normal!important}.fc-time-grid .fc-event .fc-time:after{content:attr(data-full)}.fc-day-grid-container,.fc-scroller,.fc-time-grid-container{overflow:visible!important;height:auto!important}.fc-row{border:0!important;margin:0!important}
|
@ -23,20 +23,29 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'html_language' => 'en',
|
||||
'locale' => 'en, English, en_US, en_US.utf8, en_US.UTF-8',
|
||||
'month' => '%B %Y',
|
||||
'month_and_day' => '%B %e, %Y',
|
||||
'date_time' => '%B %e, %Y, @ %T',
|
||||
'specific_day' => '%e %B %Y',
|
||||
'week_in_year' => 'Week %W, %Y',
|
||||
'year' => '%Y',
|
||||
'half_year' => '%B %Y',
|
||||
'month_js' => 'MMMM YYYY',
|
||||
'month_and_day_js' => 'MMMM Do, YYYY',
|
||||
'date_time_js' => 'MMMM Do, YYYY, @ HH:mm:ss',
|
||||
'specific_day_js' => 'D MMMM YYYY',
|
||||
'week_in_year_js' => '[Week] w, YYYY',
|
||||
'year_js' => 'YYYY',
|
||||
'half_year_js' => 'Q YYYY',
|
||||
'html_language' => 'en',
|
||||
'locale' => 'en, English, en_US, en_US.utf8, en_US.UTF-8',
|
||||
'month' => '%B %Y',
|
||||
'month_and_day' => '%B %e, %Y',
|
||||
'month_and_date_day' => '%A %B %e, %Y',
|
||||
'month_and_day_no_year' => '%B %e',
|
||||
'date_time' => '%B %e, %Y, @ %T',
|
||||
'specific_day' => '%e %B %Y',
|
||||
'week_in_year' => 'Week %W, %Y',
|
||||
'year' => '%Y',
|
||||
'half_year' => '%B %Y',
|
||||
'month_js' => 'MMMM YYYY',
|
||||
'month_and_day_js' => 'MMMM Do, YYYY',
|
||||
'date_time_js' => 'MMMM Do, YYYY, @ HH:mm:ss',
|
||||
'specific_day_js' => 'D MMMM 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',
|
||||
];
|
||||
|
@ -34,4 +34,6 @@ return [
|
||||
'transactions-index' => 'These expenses, deposits and transfers are not particularly imaginative. They have been generated automatically.',
|
||||
'piggy-banks-index' => 'As you can see, there are three piggy banks. Use the plus and minus buttons to influence the amount of money in each piggy bank. Click the name of the piggy bank to see the administration for each piggy bank.',
|
||||
'import-index' => 'Any CSV file can be imported into Firefly III. It also supports importing data from bunq and Spectre. Other banks and financial aggregators will be implemented in the future. As a demo-user however, you can only see the "fake"-provider in action. It will generate some random transactions to show you how the process works.',
|
||||
'recurring-index' => 'Please note that this feature is under active development and may not work as expected.',
|
||||
'recurring-create' => 'Please note that this feature is under active development and may not work as expected.',
|
||||
];
|
||||
|
@ -820,7 +820,7 @@ return [
|
||||
'language' => 'Language',
|
||||
'new_savings_account' => ':bank_name savings account',
|
||||
'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:
|
||||
'yourAccounts' => 'Your accounts',
|
||||
@ -1206,4 +1206,39 @@ 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_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:',
|
||||
'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',
|
||||
'click_for_calendar' => 'Click here for a calendar that shows you when the transaction would repeat.',
|
||||
'repeat_forever' => 'Repeat forever',
|
||||
'repeat_until_date' => 'Repeat until date',
|
||||
'repeat_times' => 'Repeat a number of times',
|
||||
|
||||
|
||||
];
|
||||
|
@ -216,11 +216,22 @@ return [
|
||||
'country_code' => 'Country code',
|
||||
'provider_code' => 'Bank or data-provider',
|
||||
|
||||
'due_date' => 'Due date',
|
||||
'payment_date' => 'Payment date',
|
||||
'invoice_date' => 'Invoice date',
|
||||
'internal_reference' => 'Internal reference',
|
||||
'inward' => 'Inward description',
|
||||
'outward' => 'Outward description',
|
||||
'rule_group_id' => 'Rule group',
|
||||
'due_date' => 'Due date',
|
||||
'payment_date' => 'Payment date',
|
||||
'invoice_date' => 'Invoice date',
|
||||
'internal_reference' => 'Internal reference',
|
||||
'inward' => 'Inward description',
|
||||
'outward' => 'Outward description',
|
||||
'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',
|
||||
'repetition_end' => 'Repetition ends',
|
||||
'repetitions' => 'Repetitions',
|
||||
'calendar' => 'Calendar',
|
||||
|
||||
];
|
||||
|
@ -123,4 +123,9 @@ return [
|
||||
'spectre_last_use' => 'Last login',
|
||||
'spectre_status' => 'Status',
|
||||
'bunq_payment_id' => 'bunq payment ID',
|
||||
'repetitions' => 'Repetitions',
|
||||
'title' => 'Title',
|
||||
'transaction_s' => 'Transaction(s)',
|
||||
'field' => 'Field',
|
||||
'value' => 'Value',
|
||||
];
|
||||
|
@ -14,7 +14,9 @@
|
||||
<ul>
|
||||
<li><a href="{{ route('admin.configuration.index') }}">{{ 'firefly_instance_configuration'|_ }}</a></li>
|
||||
<li><a href="{{ route('admin.links.index') }}">{{ 'journal_link_configuration'|_ }}</a></li>
|
||||
{% if not sandstorm %}
|
||||
<li><a href="{{ route('admin.update-check') }}">{{ 'update_check_title'|_ }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@
|
||||
{{ 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.integer('skip',0) }}
|
||||
{{ ExpandedForm.checkbox('active',1,true) }}
|
||||
{{ ExpandedForm.checkbox('active',1, true) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
1
resources/views/demo/recurring/index.twig
Normal file
1
resources/views/demo/recurring/index.twig
Normal file
@ -0,0 +1 @@
|
||||
{{ trans('demo.recurring-index') }}
|
1
resources/views/demo/recurring/recurring-create.twig
Normal file
1
resources/views/demo/recurring/recurring-create.twig
Normal file
@ -0,0 +1 @@
|
||||
{{ trans('demo.recurring-create') }}
|
@ -98,6 +98,10 @@
|
||||
<a href="{{ route('rules.index') }}">
|
||||
<i class="fa fa-random fa-fw"></i> {{ 'rules'|_ }}</a>
|
||||
</li>
|
||||
<li class="{{ activeRoutePartial('recurring') }}">
|
||||
<a href="{{ route('recurring.index') }}">
|
||||
<i class="fa fa-paint-brush fa-fw"></i> {{ 'recurrences'|_ }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
|
211
resources/views/recurring/create.twig
Normal file
211
resources/views/recurring/create.twig
Normal file
@ -0,0 +1,211 @@
|
||||
{% 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() }}"/>
|
||||
{# row with recurrence information #}
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||
{# mandatory recurrence stuff #}
|
||||
<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.select('repetition_type', [], null, {helpText: trans('firefly.change_date_other_options')}) }}
|
||||
{{ ExpandedForm.number('skip', 0) }}
|
||||
{{ ExpandedForm.select('repetition_end', typesOfRepetitions) }}
|
||||
{{ ExpandedForm.date('repeat_until',null) }}
|
||||
{{ ExpandedForm.number('repetitions',null) }}
|
||||
|
||||
{# calendar in popup #}
|
||||
<div class="form-group" id="calendar_holder">
|
||||
<label for="ffInput_calendar" class="col-sm-4 control-label">{{ trans('form.calendar') }}</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static" id="ffInput_calendar">
|
||||
<a href="#" id="calendar-link">{{ 'click_for_calendar'|_ }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||
{# optional recurrence stuff #}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||
{# mandatory transaction information #}
|
||||
<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>
|
||||
{# 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>
|
||||
{# end of three buttons#}
|
||||
|
||||
{{ 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>
|
||||
|
||||
<div class="col-lg-6 col-md-6 col-sm-12">
|
||||
{# optional transaction information #}
|
||||
<div class="box">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{# row with submit stuff. #}
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-lg-offset-6 lg-md-offset-6">
|
||||
<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>
|
||||
{#
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
#}
|
||||
</form>
|
||||
|
||||
{# calendar modal #}
|
||||
<div class="modal fade" id="calendarModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Calendar view yay</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="recurring_calendar" style="max-width: 400px;margin: 0 auto;">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% 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" src="lib/fc/fullcalendar.min.js?v={{ FF_VERSION }}"></script>
|
||||
<script type="text/javascript">
|
||||
var transactionType = "{{ preFilled.transaction_type }}";
|
||||
var suggestUri = "{{ route('recurring.suggest') }}";
|
||||
var eventsUri = "{{ route('recurring.events') }}";
|
||||
</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">
|
||||
<link href="lib/fc/fullcalendar.min.css?v={{ FF_VERSION }}" type="text/css" rel="stylesheet" media="all">
|
||||
{% endblock %}
|
120
resources/views/recurring/index.twig
Normal file
120
resources/views/recurring/index.twig
Normal 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"> </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>
|
||||
→
|
||||
<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 %}
|
190
resources/views/recurring/show.twig
Normal file
190
resources/views/recurring/show.twig
Normal 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 %}
|
@ -60,7 +60,7 @@
|
||||
{{ ExpandedForm.date('date', preFilled.date|default(phpdate('Y-m-d'))) }}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" id="transaction-btn" class="btn btn-success pull-right">
|
||||
<button type="submit" class="transaction-btn btn btn-success pull-right">
|
||||
{{ trans('form.store_new_'~what) }}
|
||||
</button>
|
||||
</div>
|
||||
@ -196,7 +196,7 @@
|
||||
{{ ExpandedForm.optionsList('create','transaction') }}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" id="transaction-btn" class="btn btn-success pull-right">
|
||||
<button type="submit" class="transaction-btn btn btn-success pull-right">
|
||||
{{ trans('form.store_new_'~what) }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -33,6 +33,7 @@ use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\ImportJob;
|
||||
use FireflyIII\Models\LinkType;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
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
|
||||
Breadcrumbs::register(
|
||||
'rules.index',
|
||||
|
@ -65,10 +65,10 @@ Route::group(
|
||||
*/
|
||||
Route::group(
|
||||
['middleware' => 'user-simple-auth', 'namespace' => 'FireflyIII\Http\Controllers'], function () {
|
||||
Route::get('error', ['uses' => 'HomeController@displayError', 'as' => 'error']);
|
||||
Route::get('error', ['uses' => 'DebugController@displayError', 'as' => 'error']);
|
||||
Route::any('logout', ['uses' => 'Auth\LoginController@logout', 'as' => 'logout']);
|
||||
Route::get('flush', ['uses' => 'HomeController@flush', 'as' => 'flush']);
|
||||
Route::get('routes', ['uses' => 'HomeController@routes', 'as' => 'routes']);
|
||||
Route::get('flush', ['uses' => 'DebugController@flush', 'as' => 'flush']);
|
||||
Route::get('routes', ['uses' => 'DebugController@routes', 'as' => 'routes']);
|
||||
Route::get('debug', 'DebugController@index')->name('debug');
|
||||
}
|
||||
);
|
||||
@ -96,7 +96,7 @@ Route::group(
|
||||
Route::group(
|
||||
['middleware' => ['user-full-auth'], 'namespace' => 'FireflyIII\Http\Controllers'], function () {
|
||||
Route::get('/', ['uses' => 'HomeController@index', 'as' => 'index']);
|
||||
Route::get('/flash', ['uses' => 'HomeController@testFlash', 'as' => 'test-flash']);
|
||||
Route::get('/flash', ['uses' => 'DebugController@testFlash', 'as' => 'test-flash']);
|
||||
Route::get('/home', ['uses' => 'HomeController@index', 'as' => 'home']);
|
||||
Route::post('/daterange', ['uses' => 'HomeController@dateRange', 'as' => 'daterange']);
|
||||
}
|
||||
@ -466,28 +466,6 @@ Route::group(
|
||||
|
||||
// download config:
|
||||
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,24 @@ 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('events', ['uses' => 'IndexController@events', 'as' => 'events']);
|
||||
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
|
||||
*/
|
||||
|
@ -22,6 +22,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Controllers;
|
||||
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
|
||||
use Log;
|
||||
use Tests\TestCase;
|
||||
|
||||
@ -43,6 +45,34 @@ class DebugControllerTest extends TestCase
|
||||
Log::debug(sprintf('Now in %s.', \get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::displayError
|
||||
*/
|
||||
public function testDisplayError(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('error'));
|
||||
$response->assertStatus(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::flush
|
||||
*/
|
||||
public function testFlush(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('flush'));
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::index
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::__construct
|
||||
@ -56,4 +86,32 @@ class DebugControllerTest extends TestCase
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::routes()
|
||||
*/
|
||||
public function testRoutes(): void
|
||||
{
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('routes'));
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\DebugController::testFlash
|
||||
*/
|
||||
public function testTestFlash(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('test-flash'));
|
||||
$response->assertStatus(302);
|
||||
$response->assertSessionHas('success');
|
||||
$response->assertSessionHas('info');
|
||||
$response->assertSessionHas('warning');
|
||||
$response->assertSessionHas('error');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -100,34 +100,6 @@ class HomeControllerTest extends TestCase
|
||||
$response->assertSessionHas('warning', '91 days of data may take a while to load.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::displayError
|
||||
*/
|
||||
public function testDisplayError(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('error'));
|
||||
$response->assertStatus(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::flush
|
||||
*/
|
||||
public function testFlush(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('flush'));
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::index
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::__construct
|
||||
@ -187,31 +159,5 @@ class HomeControllerTest extends TestCase
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::routes()
|
||||
*/
|
||||
public function testRoutes(): void
|
||||
{
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('routes'));
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FireflyIII\Http\Controllers\HomeController::testFlash
|
||||
*/
|
||||
public function testTestFlash(): void
|
||||
{
|
||||
// mock stuff
|
||||
$journalRepos = $this->mock(JournalRepositoryInterface::class);
|
||||
$journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal);
|
||||
|
||||
$this->be($this->user());
|
||||
$response = $this->get(route('test-flash'));
|
||||
$response->assertStatus(302);
|
||||
$response->assertSessionHas('success');
|
||||
$response->assertSessionHas('info');
|
||||
$response->assertSessionHas('warning');
|
||||
$response->assertSessionHas('error');
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +156,13 @@ class AmountTest extends TestCase
|
||||
'(33.52)' => '-33.52',
|
||||
'€(63.12)' => '-63.12',
|
||||
'($182.77)' => '-182.77',
|
||||
|
||||
// double minus because why the hell not
|
||||
'--0.03881677' => '0.03881677',
|
||||
'--0.33' => '0.33',
|
||||
'--$1.23' => '1.23',
|
||||
'--63 5212.4440' => '635212.444',
|
||||
'--,2' => '0.2',
|
||||
];
|
||||
foreach ($values as $value => $expected) {
|
||||
$converter = new Amount;
|
||||
|
Loading…
Reference in New Issue
Block a user