diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index d7297ff629..e4b0012969 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -46,10 +46,15 @@ class Cron extends Command protected $signature = 'firefly-iii:cron {--F|force : Force the cron job(s) to execute.} {--date= : Set the date in YYYY-MM-DD to make Firefly III think that\'s the current date.} + {--download-cer : Download exchange rates. Other tasks will be skipped unless also requested.} + {--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.} + {--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.} + {--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.} '; public function handle(): int { + $doAll = !$this->option('download-cer') && !$this->option('create-recurring') && !$this->option('create-auto-budgets') && !$this->option('send-bill-warnings'); $date = null; try { @@ -60,7 +65,7 @@ class Cron extends Command $force = (bool)$this->option('force'); // @phpstan-ignore-line // Fire exchange rates cron job. - if (true === config('cer.download_enabled')) { + if (true === config('cer.download_enabled') && ($doAll || $this->option('download-cer'))) { try { $this->exchangeRatesCronJob($force, $date); } catch (FireflyException $e) { @@ -71,30 +76,36 @@ class Cron extends Command } // Fire recurring transaction cron job. - try { - $this->recurringCronJob($force, $date); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - $this->friendlyError($e->getMessage()); + if($doAll || $this->option('create-recurring')) { + try { + $this->recurringCronJob($force, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + $this->friendlyError($e->getMessage()); + } } // Fire auto-budget cron job: - try { - $this->autoBudgetCronJob($force, $date); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - $this->friendlyError($e->getMessage()); + if($doAll || $this->option('create-auto-budgets')) { + try { + $this->autoBudgetCronJob($force, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + $this->friendlyError($e->getMessage()); + } } // Fire bill warning cron job - try { - $this->billWarningCronJob($force, $date); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - $this->friendlyError($e->getMessage()); + if($doAll || $this->option('send-bill-warnings')) { + try { + $this->billWarningCronJob($force, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + $this->friendlyError($e->getMessage()); + } } $this->friendlyInfo('More feedback on the cron jobs can be found in the log files.'); diff --git a/app/Http/Controllers/Recurring/ShowController.php b/app/Http/Controllers/Recurring/ShowController.php index c8f9634de8..d4cce6f651 100644 --- a/app/Http/Controllers/Recurring/ShowController.php +++ b/app/Http/Controllers/Recurring/ShowController.php @@ -88,6 +88,7 @@ class ShowController extends Controller $groups = $this->recurring->getTransactions($recurrence); $today = today(config('app.timezone')); $array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null; + $array['journal_count'] = $this->recurring->getJournalCount($recurrence); // transform dates back to Carbon objects and expand information foreach ($array['repetitions'] as $index => $repetition) { diff --git a/app/Jobs/CreateRecurringTransactions.php b/app/Jobs/CreateRecurringTransactions.php index aa9cd864c0..c7f4eb292d 100644 --- a/app/Jobs/CreateRecurringTransactions.php +++ b/app/Jobs/CreateRecurringTransactions.php @@ -177,10 +177,11 @@ class CreateRecurringTransactions implements ShouldQueue // has repeated X times. $journalCount = $this->repository->getJournalCount($recurrence); if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions && false === $this->force) { - app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $recurrence->repetitions)); + app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $journalCount)); return false; } + app('log')->debug(sprintf('Recurrence #%d has run %d times, max is %d times.', $recurrence->id, $journalCount, $recurrence->repetitions)); // is no longer running if ($this->repeatUntilHasPassed($recurrence)) { @@ -202,8 +203,8 @@ class CreateRecurringTransactions implements ShouldQueue sprintf( 'Recurrence #%d is set to run on %s, and today\'s date is %s. Skipped.', $recurrence->id, - $recurrence->first_date->format('Y-m-d'), - $this->date->format('Y-m-d') + $recurrence->first_date->format('Y-m-d H:i:s'), + $this->date->format('Y-m-d H:i:s') ) ); @@ -244,9 +245,10 @@ class CreateRecurringTransactions implements ShouldQueue private function hasNotStartedYet(Recurrence $recurrence): bool { $startDate = $this->getStartDate($recurrence); - app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d'))); + app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d H:i:s'))); + app('log')->debug(sprintf('Ask date is %s', $this->date->format('Y-m-d H:i:s'))); - return $startDate->gt($this->date); + return $startDate->gte($this->date); } /** diff --git a/app/Repositories/Recurring/RecurringRepository.php b/app/Repositories/Recurring/RecurringRepository.php index a607a57e79..d0d3847c7f 100644 --- a/app/Repositories/Recurring/RecurringRepository.php +++ b/app/Repositories/Recurring/RecurringRepository.php @@ -66,21 +66,20 @@ class RecurringRepository implements RecurringRepositoryInterface // if not, loop set and try to read the recurrence_date. If it matches start or end, return it as well. $set = TransactionJournalMeta::where(static function (Builder $q1) use ($recurrence): void { - $q1->where('name', 'recurrence_id'); - $q1->where('data', json_encode((string)$recurrence->id)); - })->get(['journal_meta.transaction_journal_id']); + $q1->where('name', 'recurrence_id'); + $q1->where('data', json_encode((string) $recurrence->id)); + })->get(['journal_meta.transaction_journal_id']); // there are X journals made for this recurrence. Any of them meant for today? foreach ($set as $journalMeta) { $count = TransactionJournalMeta::where(static function (Builder $q2) use ($date): void { - $string = (string)$date; + $string = (string) $date; app('log')->debug(sprintf('Search for date: %s', json_encode($string))); $q2->where('name', 'recurrence_date'); $q2->where('data', json_encode($string)); }) - ->where('transaction_journal_id', $journalMeta->transaction_journal_id) - ->count() - ; + ->where('transaction_journal_id', $journalMeta->transaction_journal_id) + ->count(); if ($count > 0) { app('log')->debug(sprintf('Looks like journal #%d was already created', $journalMeta->transaction_journal_id)); @@ -97,12 +96,11 @@ class RecurringRepository implements RecurringRepositoryInterface public function get(): Collection { return $this->user->recurrences() - ->with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions']) - ->orderBy('active', 'DESC') - ->orderBy('transaction_type_id', 'ASC') - ->orderBy('title', 'ASC') - ->get() - ; + ->with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions']) + ->orderBy('active', 'DESC') + ->orderBy('transaction_type_id', 'ASC') + ->orderBy('title', 'ASC') + ->get(); } /** @@ -128,10 +126,9 @@ class RecurringRepository implements RecurringRepositoryInterface { // grab ALL recurring transactions: return Recurrence::with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions']) - ->orderBy('active', 'DESC') - ->orderBy('title', 'ASC') - ->get() - ; + ->orderBy('active', 'DESC') + ->orderBy('title', 'ASC') + ->get(); } public function getBillId(RecurrenceTransaction $recTransaction): ?int @@ -141,7 +138,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var RecurrenceTransactionMeta $meta */ foreach ($recTransaction->recurrenceTransactionMeta as $meta) { if ('bill_id' === $meta->name) { - $return = (int)$meta->value; + $return = (int) $meta->value; } } @@ -158,7 +155,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var RecurrenceTransactionMeta $meta */ foreach ($recTransaction->recurrenceTransactionMeta as $meta) { if ('budget_id' === $meta->name) { - $return = (int)$meta->value; + $return = (int) $meta->value; } } @@ -175,7 +172,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var RecurrenceTransactionMeta $meta */ foreach ($recTransaction->recurrenceTransactionMeta as $meta) { if ('category_id' === $meta->name) { - $return = (int)$meta->value; + $return = (int) $meta->value; } } @@ -192,7 +189,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var RecurrenceTransactionMeta $meta */ foreach ($recTransaction->recurrenceTransactionMeta as $meta) { if ('category_name' === $meta->name) { - $return = (string)$meta->value; + $return = (string) $meta->value; } } @@ -204,20 +201,21 @@ class RecurringRepository implements RecurringRepositoryInterface */ public function getJournalCount(Recurrence $recurrence, ?Carbon $start = null, ?Carbon $end = null): int { + Log::debug(sprintf('Now in getJournalCount(#%d, "%s", "%s")', $recurrence->id, $start?->format('Y-m-d H:i:s'), $end?->format('Y-m-d H:i:s'))); $query = TransactionJournal::leftJoin('journal_meta', 'journal_meta.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.user_id', $recurrence->user_id) - ->whereNull('transaction_journals.deleted_at') - ->where('journal_meta.name', 'recurrence_id') - ->where('journal_meta.data', '"'.$recurrence->id.'"') - ; + ->where('transaction_journals.user_id', $recurrence->user_id) + ->whereNull('transaction_journals.deleted_at') + ->where('journal_meta.name', 'recurrence_id') + ->where('journal_meta.data', '"' . $recurrence->id . '"'); if (null !== $start) { $query->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')); } if (null !== $end) { $query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00')); } - - return $query->count('transaction_journals.id'); + $count = $query->count('transaction_journals.id'); + Log::debug(sprintf('Count is %d', $count)); + return $count; } /** @@ -226,11 +224,10 @@ class RecurringRepository implements RecurringRepositoryInterface public function getJournalIds(Recurrence $recurrence): array { return TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') - ->where('transaction_journals.user_id', $this->user->id) - ->where('journal_meta.name', '=', 'recurrence_id') - ->where('journal_meta.data', '=', json_encode((string)$recurrence->id)) - ->get(['journal_meta.transaction_journal_id'])->pluck('transaction_journal_id')->toArray() - ; + ->where('transaction_journals.user_id', $this->user->id) + ->where('journal_meta.name', '=', 'recurrence_id') + ->where('journal_meta.data', '=', json_encode((string) $recurrence->id)) + ->get(['journal_meta.transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); } /** @@ -241,7 +238,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var null|Note $note */ $note = $recurrence->notes()->first(); - return (string)$note?->text; + return (string) $note?->text; } public function getPiggyBank(RecurrenceTransaction $transaction): ?int @@ -251,7 +248,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** @var RecurrenceTransactionMeta $metaEntry */ foreach ($meta as $metaEntry) { if ('piggy_bank_id' === $metaEntry->name) { - return (int)$metaEntry->value; + return (int) $metaEntry->value; } } @@ -278,30 +275,28 @@ class RecurringRepository implements RecurringRepositoryInterface public function getTransactionPaginator(Recurrence $recurrence, int $page, int $pageSize): LengthAwarePaginator { $journalMeta = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') - ->whereNull('transaction_journals.deleted_at') - ->where('transaction_journals.user_id', $this->user->id) - ->where('name', 'recurrence_id') - ->where('data', json_encode((string)$recurrence->id)) - ->get()->pluck('transaction_journal_id')->toArray() - ; + ->whereNull('transaction_journals.deleted_at') + ->where('transaction_journals.user_id', $this->user->id) + ->where('name', 'recurrence_id') + ->where('data', json_encode((string) $recurrence->id)) + ->get()->pluck('transaction_journal_id')->toArray(); $search = []; foreach ($journalMeta as $journalId) { - $search[] = (int)$journalId; + $search[] = (int) $journalId; } /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($recurrence->user); $collector->withCategoryInformation()->withBudgetInformation()->setLimit($pageSize)->setPage($page) - ->withAccountInformation() - ; + ->withAccountInformation(); $collector->setJournalIds($search); return $collector->getPaginatedGroups(); } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -311,23 +306,22 @@ class RecurringRepository implements RecurringRepositoryInterface public function getTransactions(Recurrence $recurrence): Collection { $journalMeta = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') - ->whereNull('transaction_journals.deleted_at') - ->where('transaction_journals.user_id', $this->user->id) - ->where('name', 'recurrence_id') - ->where('data', json_encode((string)$recurrence->id)) - ->get()->pluck('transaction_journal_id')->toArray() - ; + ->whereNull('transaction_journals.deleted_at') + ->where('transaction_journals.user_id', $this->user->id) + ->where('name', 'recurrence_id') + ->where('data', json_encode((string) $recurrence->id)) + ->get()->pluck('transaction_journal_id')->toArray(); $search = []; foreach ($journalMeta as $journalId) { - $search[] = (int)$journalId; + $search[] = (int) $journalId; } if (0 === count($search)) { return new Collection(); } /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($recurrence->user); $collector->withCategoryInformation()->withBudgetInformation()->withAccountInformation(); @@ -442,51 +436,51 @@ class RecurringRepository implements RecurringRepositoryInterface if (is_array($language)) { $language = 'en_US'; } - $language = (string)$language; + $language = (string) $language; if ('daily' === $repetition->repetition_type) { - return (string)trans('firefly.recurring_daily', [], $language); + return (string) trans('firefly.recurring_daily', [], $language); } if ('weekly' === $repetition->repetition_type) { $dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language); if ($repetition->repetition_skip > 0) { - return (string)trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language); + return (string) trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language); } - return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language); + return (string) trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language); } if ('monthly' === $repetition->repetition_type) { if ($repetition->repetition_skip > 0) { - return (string)trans( + return (string) trans( 'firefly.recurring_monthly_skip', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1], $language ); } - return (string)trans( + return (string) trans( 'firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $language ); } if ('ndom' === $repetition->repetition_type) { - $parts = explode(',', $repetition->repetition_moment); + $parts = explode(',', $repetition->repetition_moment); // first part is number of week, second is weekday. $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language); - return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language); + return (string) trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language); } if ('yearly' === $repetition->repetition_type) { - $today = today(config('app.timezone'))->endOfYear(); - $repDate = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment); + $today = today(config('app.timezone'))->endOfYear(); + $repDate = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment); if (null === $repDate) { $repDate = clone $today; } - $diffInYears = (int)$today->diffInYears($repDate, true); + $diffInYears = (int) $today->diffInYears($repDate, true); $repDate->addYears($diffInYears); // technically not necessary. - $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); + $string = $repDate->isoFormat((string) trans('config.month_and_day_no_year_js')); - return (string)trans('firefly.recurring_yearly', ['date' => $string], $language); + return (string) trans('firefly.recurring_yearly', ['date' => $string], $language); } return ''; @@ -499,8 +493,7 @@ class RecurringRepository implements RecurringRepositoryInterface $search->whereLike('recurrences.title', sprintf('%%%s%%', $query)); } $search - ->orderBy('recurrences.title', 'ASC') - ; + ->orderBy('recurrences.title', 'ASC'); return $search->take($limit)->get(['id', 'title', 'description']); } @@ -520,16 +513,16 @@ class RecurringRepository implements RecurringRepositoryInterface public function totalTransactions(Recurrence $recurrence, RecurrenceRepetition $repetition): int { // if repeat = null just return 0. - if (null === $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) { + if (null === $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) { return 0; } // expect X transactions then stop. Return that number - if (null === $recurrence->repeat_until && 0 !== (int)$recurrence->repetitions) { - return (int)$recurrence->repetitions; + if (null === $recurrence->repeat_until && 0 !== (int) $recurrence->repetitions) { + return (int) $recurrence->repetitions; } // need to calculate, this depends on the repetition: - if (null !== $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) { + if (null !== $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) { $occurrences = $this->getOccurrencesInRange($repetition, $recurrence->first_date ?? today(), $recurrence->repeat_until); return count($occurrences); @@ -546,7 +539,7 @@ class RecurringRepository implements RecurringRepositoryInterface $occurrences = []; $mutator = clone $start; $mutator->startOfDay(); - $skipMod = $repetition->repetition_skip + 1; + $skipMod = $repetition->repetition_skip + 1; app('log')->debug(sprintf('Calculating occurrences for rep type "%s"', $repetition->repetition_type)); app('log')->debug(sprintf('Mutator is now: %s', $mutator->format('Y-m-d'))); diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index 6fd241104d..c497e18c2a 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -77,9 +77,7 @@ class RecurringCronjob extends AbstractCronjob { app('log')->info(sprintf('Will now fire recurring cron job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); - /** @var CreateRecurringTransactions $job */ - $job = app(CreateRecurringTransactions::class); - $job->setDate($this->date); + $job = new CreateRecurringTransactions($this->date); $job->setForce($this->force); $job->handle(); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 8e64ca992e..5213deacde 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2627,6 +2627,7 @@ return [ 'no_bills_create_default' => 'Create a bill', // recurring transactions + 'recurrence_max_count' => 'This recurring transactions will be created at most :max time(s), and has been created :count time(s) already.', 'create_right_now' => 'Create right now', 'no_new_transaction_in_recurrence' => 'No new transaction was created. Perhaps it was already fired for this date?', 'recurrences' => 'Recurring transactions', diff --git a/resources/views/recurring/show.twig b/resources/views/recurring/show.twig index f43a6ee23a..c01c874cbd 100644 --- a/resources/views/recurring/show.twig +++ b/resources/views/recurring/show.twig @@ -22,6 +22,17 @@

{{ 'transaction_journal_meta'|_ }}

+ {% if array.nr_of_repetitions > 0 %} +

+ {% if array.journal_count >= array.nr_of_repetitions %} + {{ trans('firefly.recurrence_max_count', {count: array.journal_count, max: array.nr_of_repetitions}) }} + {% endif %} + {% if array.journal_count < array.nr_of_repetitions %} + {{ trans('firefly.recurrence_max_count', {count: array.journal_count, max: array.nr_of_repetitions}) }} + {% endif %} +

+ {% endif %} +

{{ 'description'|_ }}: {{ array.description }}

{% if array.active == false %}