diff --git a/.deploy/docker/entrypoint.sh b/.deploy/docker/entrypoint.sh index 17d62270c3..d95e3c4626 100755 --- a/.deploy/docker/entrypoint.sh +++ b/.deploy/docker/entrypoint.sh @@ -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 diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 59b59b8bdd..6a72b8ab29 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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 = [ diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index d9199b0ab1..bb5baf3cbe 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -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( [ diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index e953e039d1..c0b6acaa5d 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -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 * diff --git a/app/Handlers/Events/VersionCheckEventHandler.php b/app/Handlers/Events/VersionCheckEventHandler.php index 2cecd97947..2b7b9a1f0f 100644 --- a/app/Handlers/Events/VersionCheckEventHandler.php +++ b/app/Handlers/Events/VersionCheckEventHandler.php @@ -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'); diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index b2eb90dc1a..a68b4cbec1 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -768,6 +768,14 @@ class JournalCollector implements JournalCollectorInterface return $this; } + /** + * @return EloquentBuilder + */ + public function getQuery(): EloquentBuilder + { + return $this->query; + } + /** * @param Collection $set * diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/JournalCollectorInterface.php index b710326175..b9c29ff4bf 100644 --- a/app/Helpers/Collector/JournalCollectorInterface.php +++ b/app/Helpers/Collector/JournalCollectorInterface.php @@ -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 */ diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index db68a5c841..ad44631ba9 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -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); } diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index 92d0760119..41fb8cdae3 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -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')); } /** diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index 3b0a444d23..a5e7b81036 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -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); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 993d3273e5..dcca1c2d3f 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -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')); - } } diff --git a/app/Http/Controllers/Recurring/CreateController.php b/app/Http/Controllers/Recurring/CreateController.php new file mode 100644 index 0000000000..52e12bf7c7 --- /dev/null +++ b/app/Http/Controllers/Recurring/CreateController.php @@ -0,0 +1,96 @@ +. + */ + +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')); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Recurring/EditController.php b/app/Http/Controllers/Recurring/EditController.php new file mode 100644 index 0000000000..6ef2b0beb2 --- /dev/null +++ b/app/Http/Controllers/Recurring/EditController.php @@ -0,0 +1,66 @@ +. + */ + +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')); + } + + +} \ No newline at end of file diff --git a/app/Http/Controllers/Recurring/IndexController.php b/app/Http/Controllers/Recurring/IndexController.php new file mode 100644 index 0000000000..ed25962773 --- /dev/null +++ b/app/Http/Controllers/Recurring/IndexController.php @@ -0,0 +1,203 @@ +. + */ + +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); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 95cd7d7464..7ad5d34f99 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -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) { diff --git a/app/Http/Requests/SplitJournalFormRequest.php b/app/Http/Requests/SplitJournalFormRequest.php index 207754df5b..bc989b5f7a 100644 --- a/app/Http/Requests/SplitJournalFormRequest.php +++ b/app/Http/Requests/SplitJournalFormRequest.php @@ -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, diff --git a/app/Import/Converter/Amount.php b/app/Import/Converter/Amount.php index 21b3769c0c..79a0c218e5 100644 --- a/app/Import/Converter/Amount.php +++ b/app/Import/Converter/Amount.php @@ -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]) { diff --git a/app/Import/Storage/ImportArrayStorage.php b/app/Import/Storage/ImportArrayStorage.php index 2754ad94df..c7edae2e6f 100644 --- a/app/Import/Storage/ImportArrayStorage.php +++ b/app/Import/Storage/ImportArrayStorage.php @@ -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); } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index bf4c01ff12..3da9e56add 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -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 { diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 60f80da5f3..c4bd59d5c8 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -31,6 +31,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Budget. + * @property int $id + * @property string $name */ class Budget extends Model { diff --git a/app/Models/Category.php b/app/Models/Category.php index 068c12934b..7bc567f2b7 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -31,6 +31,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Category. + * + * @property string $name + * @property int $id */ class Category extends Model { diff --git a/app/Models/Note.php b/app/Models/Note.php index a6588c8564..f2cc1ddb20 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -26,6 +26,8 @@ use Illuminate\Database\Eloquent\Model; /** * Class Note. + * + * @property string $text */ class Note extends Model { diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index e5b8c30a37..92819a5d28 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -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 diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php new file mode 100644 index 0000000000..9d321a4a25 --- /dev/null +++ b/app/Models/Recurrence.php @@ -0,0 +1,159 @@ +. + */ + +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); + } + +} \ No newline at end of file diff --git a/app/Models/RecurrenceMeta.php b/app/Models/RecurrenceMeta.php new file mode 100644 index 0000000000..a448e015de --- /dev/null +++ b/app/Models/RecurrenceMeta.php @@ -0,0 +1,49 @@ +. + */ + +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); + } + +} \ No newline at end of file diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php new file mode 100644 index 0000000000..64a28e1309 --- /dev/null +++ b/app/Models/RecurrenceRepetition.php @@ -0,0 +1,53 @@ +. + */ + +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); + } +} \ No newline at end of file diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php new file mode 100644 index 0000000000..482f16151a --- /dev/null +++ b/app/Models/RecurrenceTransaction.php @@ -0,0 +1,105 @@ +. + */ + +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); + } +} \ No newline at end of file diff --git a/app/Models/RecurrenceTransactionMeta.php b/app/Models/RecurrenceTransactionMeta.php new file mode 100644 index 0000000000..3c9d04a730 --- /dev/null +++ b/app/Models/RecurrenceTransactionMeta.php @@ -0,0 +1,49 @@ +. + */ + +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); + } + +} \ No newline at end of file diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index df124b1041..2a9ffa0a69 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -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); } diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 829d5e9876..e54c7513a6 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -30,6 +30,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * Class TransactionCurrency. * * @property string $code + * @property string $symbol * @property int $decimal_places * */ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 352e357d22..9e0a82dcc5 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -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 => [ diff --git a/app/Providers/RecurringServiceProvider.php b/app/Providers/RecurringServiceProvider.php new file mode 100644 index 0000000000..d41f54e743 --- /dev/null +++ b/app/Providers/RecurringServiceProvider.php @@ -0,0 +1,63 @@ +. + */ +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; + } + ); + } + +} diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 6f71187a31..4dc900bb95 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -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 { diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 1c55997a76..dc6bfe9b26 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -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; diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 401d53d4b1..99ef2b7f28 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -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. * diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 9b22a8d3b8..f1ee19124a 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -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. * diff --git a/app/Repositories/Recurring/RecurringRepository.php b/app/Repositories/Recurring/RecurringRepository.php new file mode 100644 index 0000000000..2bf58c2066 --- /dev/null +++ b/app/Repositories/Recurring/RecurringRepository.php @@ -0,0 +1,226 @@ +. + */ + +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; + } +} \ No newline at end of file diff --git a/app/Repositories/Recurring/RecurringRepositoryInterface.php b/app/Repositories/Recurring/RecurringRepositoryInterface.php new file mode 100644 index 0000000000..f0e7646827 --- /dev/null +++ b/app/Repositories/Recurring/RecurringRepositoryInterface.php @@ -0,0 +1,84 @@ +. + */ + +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; + +} \ No newline at end of file diff --git a/app/Services/Currency/FixerIOv2.php b/app/Services/Currency/FixerIOv2.php index f44e917eb4..b5cf3cde09 100644 --- a/app/Services/Currency/FixerIOv2.php +++ b/app/Services/Currency/FixerIOv2.php @@ -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', ''); diff --git a/app/Services/Internal/Support/TransactionServiceTrait.php b/app/Services/Internal/Support/TransactionServiceTrait.php index 06fe1fe99c..d0cb0a3f8f 100644 --- a/app/Services/Internal/Support/TransactionServiceTrait.php +++ b/app/Services/Internal/Support/TransactionServiceTrait.php @@ -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); diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index 0e0be45819..d6d34aa8ed 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -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()); } } diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index a9ce98af63..bdda187930 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -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(); diff --git a/app/Support/Import/Routine/File/ImportableConverter.php b/app/Support/Import/Routine/File/ImportableConverter.php index 5084f24743..73c3963354 100644 --- a/app/Support/Import/Routine/File/ImportableConverter.php +++ b/app/Support/Import/Routine/File/ImportableConverter.php @@ -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; } diff --git a/app/Support/Import/Routine/File/MappedValuesValidator.php b/app/Support/Import/Routine/File/MappedValuesValidator.php index 8526b2b1f7..e9229a275a 100644 --- a/app/Support/Import/Routine/File/MappedValuesValidator.php +++ b/app/Support/Import/Routine/File/MappedValuesValidator.php @@ -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; } } diff --git a/app/Transformers/RecurrenceTransformer.php b/app/Transformers/RecurrenceTransformer.php new file mode 100644 index 0000000000..713928ac3b --- /dev/null +++ b/app/Transformers/RecurrenceTransformer.php @@ -0,0 +1,256 @@ +. + */ + +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; + } + +} \ No newline at end of file diff --git a/app/User.php b/app/User.php index 2ffd076912..28525e3ab4 100644 --- a/app/User.php +++ b/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. diff --git a/config/app.php b/config/app.php index 953ed046ee..5127530c6d 100644 --- a/config/app.php +++ b/config/app.php @@ -98,6 +98,7 @@ return [ FireflyIII\Providers\SearchServiceProvider::class, FireflyIII\Providers\TagServiceProvider::class, FireflyIII\Providers\AdminServiceProvider::class, + FireflyIII\Providers\RecurringServiceProvider::class, ], diff --git a/config/firefly.php b/config/firefly.php index c0fe9ed9ea..7721649b9b 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -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, diff --git a/database/migrations/2018_04_29_174524_changes_for_v474.php b/database/migrations/2018_04_29_174524_changes_for_v474.php index cdc6208f35..c9c79e6e82 100644 --- a/database/migrations/2018_04_29_174524_changes_for_v474.php +++ b/database/migrations/2018_04_29_174524_changes_for_v474.php @@ -37,7 +37,6 @@ class ChangesForV474 extends Migration */ public function down() { - // } /** diff --git a/database/migrations/2018_06_08_200526_changes_for_v475.php b/database/migrations/2018_06_08_200526_changes_for_v475.php new file mode 100644 index 0000000000..db5cfbebe7 --- /dev/null +++ b/database/migrations/2018_06_08_200526_changes_for_v475.php @@ -0,0 +1,130 @@ +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'); + } + ); + + + } +} diff --git a/public/js/ff/recurring/create.js b/public/js/ff/recurring/create.js new file mode 100644 index 0000000000..083f42f9aa --- /dev/null +++ b/public/js/ff/recurring/create.js @@ -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 . + */ + +/** 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($('