New cron job for bills.

This commit is contained in:
James Cole 2022-03-28 12:23:46 +02:00
parent e5a08d2cf1
commit f2849c8058
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
29 changed files with 714 additions and 142 deletions

View File

@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Cronjobs\AutoBudgetCronjob;
use FireflyIII\Support\Cronjobs\RecurringCronjob;
use FireflyIII\Support\Cronjobs\BillWarningCronjob;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Log;
@ -66,7 +67,7 @@ class Cron extends Command
} catch (InvalidArgumentException $e) {
$this->error(sprintf('"%s" is not a valid date', $this->option('date')));
}
$force = (bool)$this->option('force');
$force = (bool) $this->option('force');
/*
* Fire recurring transaction cron job.
@ -90,6 +91,17 @@ class Cron extends Command
$this->error($e->getMessage());
}
/*
* Fire bill warning cron job
*/
try {
$this->billWarningCronJob($force, $date);
} catch (FireflyException $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$this->error($e->getMessage());
}
$this->info('More feedback on the cron jobs can be found in the log files.');
return 0;
@ -150,4 +162,32 @@ class Cron extends Command
}
}
/**
* @param bool $force
* @param Carbon|null $date
*
*/
private function billWarningCronJob(bool $force, ?Carbon $date): void
{
$autoBudget = new BillWarningCronjob;
$autoBudget->setForce($force);
// set date in cron job:
if (null !== $date) {
$autoBudget->setDate($date);
}
$autoBudget->fire();
if ($autoBudget->jobErrored) {
$this->error(sprintf('Error in "bill warnings" cron: %s', $autoBudget->message));
}
if ($autoBudget->jobFired) {
$this->error(sprintf('"Send bill warnings" cron fired: %s', $autoBudget->message));
}
if ($autoBudget->jobSucceeded) {
$this->error(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message));
}
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* DestroyedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
use FireflyIII\Models\Bill;
use Illuminate\Queue\SerializesModels;
/**
* Class WarnUserAboutBill.
*
* @codeCoverageIgnore
*/
class WarnUserAboutBill extends Event
{
use SerializesModels;
public Bill $bill;
public string $field;
public int $diff;
/**
* @param Bill $bill
* @param string $field
* @param int $diff
*/
public function __construct(Bill $bill, string $field, int $diff)
{
$this->bill = $bill;
$this->field = $field;
$this->diff = $diff;
}
}

View File

@ -51,7 +51,7 @@ class BillFactory
{
Log::debug(sprintf('Now in %s', __METHOD__), $data);
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find((int)($data['currency_id'] ?? null), (string)($data['currency_code'] ?? null)) ??
$currency = $factory->find((int) ($data['currency_id'] ?? null), (string) ($data['currency_code'] ?? null)) ??
app('amount')->getDefaultCurrencyByUser($this->user);
try {
@ -82,7 +82,7 @@ class BillFactory
}
if (array_key_exists('notes', $data)) {
$this->updateNote($bill, (string)$data['notes']);
$this->updateNote($bill, (string) $data['notes']);
}
$objectGroupTitle = $data['object_group_title'] ?? '';
if ('' !== $objectGroupTitle) {
@ -93,7 +93,7 @@ class BillFactory
}
}
// try also with ID:
$objectGroupId = (int)($data['object_group_id'] ?? 0);
$objectGroupId = (int) ($data['object_group_id'] ?? 0);
if (0 !== $objectGroupId) {
$objectGroup = $this->findObjectGroupById($objectGroupId);
if (null !== $objectGroup) {
@ -113,8 +113,8 @@ class BillFactory
*/
public function find(?int $billId, ?string $billName): ?Bill
{
$billId = (int)$billId;
$billName = (string)$billName;
$billId = (int) $billId;
$billName = (string) $billName;
$bill = null;
// first find by ID:
if ($billId > 0) {

View File

@ -0,0 +1,41 @@
<?php
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Mail\BillWarningMail;
use Log;
use Mail;
/**
* Class BillEventHandler
*/
class BillEventHandler
{
/**
* @param WarnUserAboutBill $event
* @return void
*/
public function warnAboutBill(WarnUserAboutBill $event): void
{
$bill = $event->bill;
$field = $event->field;
$diff = $event->diff;
$user = $bill->user;
$address = $user->email;
$ipAddress = request()?->ip();
// see if user has alternative email address:
$pref = app('preferences')->getForUser($user, 'remote_guard_alt_email');
if (null !== $pref) {
$address = $pref->data;
}
// send message:
Mail::to($address)->send(new BillWarningMail($bill, $field, $diff, $ipAddress));
Log::debug('warnAboutBill');
}
}

View File

@ -102,6 +102,7 @@ class CreateController extends Controller
public function store(BillStoreRequest $request): RedirectResponse
{
$billData = $request->getBillData();
$billData['active'] = true;
try {
$bill = $this->repository->store($billData);

View File

@ -53,7 +53,7 @@ class EditController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string)trans('firefly.bills'));
app('view')->share('title', (string) trans('firefly.bills'));
app('view')->share('mainTitleIcon', 'fa-calendar-o');
$this->attachments = app(AttachmentHelperInterface::class);
$this->repository = app(BillRepositoryInterface::class);
@ -78,10 +78,10 @@ class EditController extends Controller
$billPeriods = config('firefly.bill_periods');
foreach ($billPeriods as $current) {
$periods[$current] = (string)trans('firefly.' . $current);
$periods[$current] = (string) trans('firefly.' . $current);
}
$subTitle = (string)trans('firefly.edit_bill', ['name' => $bill->name]);
$subTitle = (string) trans('firefly.edit_bill', ['name' => $bill->name]);
// put previous url in session if not redirect from store (not "return_to_edit").
if (true !== session('bills.edit.fromUpdate')) {
@ -89,8 +89,8 @@ class EditController extends Controller
}
$currency = app('amount')->getDefaultCurrency();
$bill->amount_min = round((float)$bill->amount_min, $currency->decimal_places);
$bill->amount_max = round((float)$bill->amount_max, $currency->decimal_places);
$bill->amount_min = round((float) $bill->amount_min, $currency->decimal_places);
$bill->amount_max = round((float) $bill->amount_max, $currency->decimal_places);
$rules = $this->repository->getRulesForBill($bill);
$defaultCurrency = app('amount')->getDefaultCurrency();
@ -98,9 +98,11 @@ class EditController extends Controller
$hasOldInput = null !== $request->old('_token');
$preFilled = [
'bill_end_date' => $bill->end_date,
'extension_date' => $bill->extension_date,
'notes' => $this->repository->getNoteText($bill),
'transaction_currency_id' => $bill->transaction_currency_id,
'active' => $hasOldInput ? (bool)$request->old('active') : $bill->active,
'active' => $hasOldInput ? (bool) $request->old('active') : $bill->active,
'object_group' => $bill->objectGroups->first() ? $bill->objectGroups->first()->title : '',
];
@ -123,7 +125,7 @@ class EditController extends Controller
$billData = $request->getBillData();
$bill = $this->repository->update($bill, $billData);
$request->session()->flash('success', (string)trans('firefly.updated_bill', ['name' => $bill->name]));
$request->session()->flash('success', (string) trans('firefly.updated_bill', ['name' => $bill->name]));
app('preferences')->mark();
/** @var array $files */
@ -132,7 +134,7 @@ class EditController extends Controller
$this->attachments->saveAttachmentsForModel($bill, $files);
}
if (null !== $files && auth()->user()->hasRole('demo')) {
session()->flash('info', (string)trans('firefly.no_att_demo_user'));
session()->flash('info', (string) trans('firefly.no_att_demo_user'));
}
// flash messages
@ -141,7 +143,7 @@ class EditController extends Controller
}
$redirect = redirect($this->getPreviousUri('bills.edit.uri'));
if (1 === (int)$request->get('return_to_edit')) {
if (1 === (int) $request->get('return_to_edit')) {
$request->session()->put('bills.edit.fromUpdate', true);

View File

@ -136,8 +136,9 @@ class IndexController extends Controller
// summarise per currency / per group.
$sums = $this->getSums($bills);
$totals = $this->getTotals($sums);
$today = now()->startOfDay();
return view('bills.index', compact('bills', 'sums', 'total', 'totals'));
return view('bills.index', compact('bills', 'sums', 'total', 'totals','today'));
}
/**

View File

@ -47,6 +47,8 @@ class BillStoreRequest extends FormRequest
'currency_code' => '',
'amount_max' => $this->string('amount_max'),
'date' => $this->getCarbonDate('date'),
'end_date' => $this->getCarbonDate('bill_end_date'),
'extension_date' => $this->getCarbonDate('extension_date'),
'repeat_freq' => $this->string('repeat_freq'),
'skip' => $this->integer('skip'),
'notes' => $this->stringWithNewlines('notes'),
@ -68,6 +70,8 @@ class BillStoreRequest extends FormRequest
'amount_max' => 'required|numeric|gt:0|max:1000000000',
'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date',
'bill_end_date' => 'nullable|date',
'extension_date' => 'nullable|date',
'repeat_freq' => sprintf('required|in:%s', join(',', config('firefly.bill_periods'))),
'skip' => 'required|integer|gte:0|lte:31',
'active' => 'boolean',

View File

@ -48,6 +48,8 @@ class BillUpdateRequest extends FormRequest
'currency_code' => '',
'amount_max' => $this->string('amount_max'),
'date' => $this->getCarbonDate('date'),
'end_date' => $this->getCarbonDate('bill_end_date'),
'extension_date' => $this->getCarbonDate('extension_date'),
'repeat_freq' => $this->string('repeat_freq'),
'skip' => $this->integer('skip'),
'notes' => $this->stringWithNewlines('notes'),
@ -72,6 +74,8 @@ class BillUpdateRequest extends FormRequest
'amount_max' => 'required|numeric|gt:0|max:1000000000',
'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date',
'bill_end_date' => 'nullable|date',
'extension_date' => 'nullable|date',
'repeat_freq' => sprintf('required|in:%s', join(',', config('firefly.bill_periods'))),
'skip' => 'required|integer|gte:0|lte:31',
'active' => 'boolean',

View File

@ -130,7 +130,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
// find budget limit:
$budgetLimit = $this->findBudgetLimit($autoBudget->budget, $start, $end);
if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_RESET === (int)$autoBudget->auto_budget_type) {
if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_RESET === (int) $autoBudget->auto_budget_type) {
// that's easy: create one.
// do nothing else.
$this->createBudgetLimit($autoBudget, $start, $end);
@ -139,7 +139,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
return;
}
if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ROLLOVER === (int)$autoBudget->auto_budget_type) {
if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ROLLOVER === (int) $autoBudget->auto_budget_type) {
// budget limit exists already,
$this->createRollover($autoBudget);
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
@ -277,7 +277,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
$repository = app(OperationsRepositoryInterface::class);
$repository->setUser($autoBudget->budget->user);
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency);
$currencyId = (int)$autoBudget->transaction_currency_id;
$currencyId = (int) $autoBudget->transaction_currency_id;
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));

View File

@ -430,7 +430,7 @@ class CreateRecurringTransactions implements ShouldQueue
'type' => strtolower($recurrence->transactionType->type),
'date' => $date,
'user' => $recurrence->user_id,
'currency_id' => (int)$transaction->transaction_currency_id,
'currency_id' => (int) $transaction->transaction_currency_id,
'currency_code' => null,
'description' => $transactions->first()->description,
'amount' => $transaction->amount,
@ -447,9 +447,9 @@ class CreateRecurringTransactions implements ShouldQueue
'foreign_amount' => $transaction->foreign_amount,
'reconciled' => false,
'identifier' => $index,
'recurrence_id' => (int)$recurrence->id,
'recurrence_id' => (int) $recurrence->id,
'order' => $index,
'notes' => (string)trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]),
'notes' => (string) trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]),
'tags' => $this->repository->getTags($transaction),
'piggy_bank_id' => $this->repository->getPiggyBank($transaction),
'piggy_bank_name' => null,

View File

@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Log;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Services\Webhook\WebhookSenderInterface;
use Illuminate\Bus\Queueable;
@ -32,6 +31,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Log;
/**
* Class SendWebhookMessage

153
app/Jobs/WarnAboutBills.php Normal file
View File

@ -0,0 +1,153 @@
<?php
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Models\Bill;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Log;
/**
* Class WarnAboutBills
*/
class WarnAboutBills implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private Carbon $date;
private bool $force;
/**
* Create a new job instance.
*
* @codeCoverageIgnore
*
* @param Carbon|null $date
*/
public function __construct(?Carbon $date)
{
if (null !== $date) {
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
if (null === $date) {
$newDate = new Carbon;
$newDate->startOfDay();
$this->date = $newDate;
}
$this->force = false;
Log::debug(sprintf('Created new WarnAboutBills("%s")', $this->date->format('Y-m-d')));
}
/**
* Execute the job.
*/
public function handle(): void
{
Log::debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y')));
$bills = Bill::all();
/** @var Bill $bill */
foreach ($bills as $bill) {
Log::debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name));
if ($this->hasDateFields($bill)) {
if ($this->needsWarning($bill, 'end_date')) {
$this->sendWarning($bill, 'end_date');
}
if ($this->needsWarning($bill, 'extension_date')) {
$this->sendWarning($bill, 'extension_date');
}
}
}
Log::debug('Done with handle()');
// clear cache:
app('preferences')->mark();
}
/**
* @param Bill $bill
* @return bool
*/
private function hasDateFields(Bill $bill): bool
{
if (false === $bill->active) {
Log::debug('Bill is not active.');
return false;
}
if (null === $bill->end_date && null === $bill->extension_date) {
Log::debug('Bill has no date fields.');
return false;
}
return true;
}
/**
* @param Bill $bill
* @param string $field
* @return bool
*/
private function needsWarning(Bill $bill, string $field): bool
{
if (null === $bill->$field) {
return false;
}
$diff = $this->getDiff($bill, $field);
$list = config('firefly.bill_reminder_periods');
Log::debug(sprintf('Difference in days for field "%s" ("%s") is %d day(s)', $field, $bill->$field->format('Y-m-d'), $diff));
if (in_array($diff, $list, true)) {
return true;
}
return false;
}
/**
* @param Bill $bill
* @param string $field
* @return int
*/
private function getDiff(Bill $bill, string $field): int
{
$today = clone $this->date;
$carbon = clone $bill->$field;
return $today->diffInDays($carbon, false);
}
/**
* @param Bill $bill
* @param string $field
* @return void
*/
private function sendWarning(Bill $bill, string $field): void
{
$diff = $this->getDiff($bill, $field);
Log::debug('Will now send warning!');
event(new WarnUserAboutBill($bill, $field, $diff));
}
/**
* @param Carbon $date
*/
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
/**
* @param bool $force
*/
public function setForce(bool $force): void
{
$this->force = $force;
}
}

View File

@ -37,10 +37,8 @@ class AdminTestMail extends Mailable
{
use Queueable, SerializesModels;
/** @var string Email address of admin */
public $email;
/** @var string IP address of admin */
public $ipAddress;
public string $email;
public string $ipAddress;
/**
* ConfirmEmailChangeMail constructor.

View File

@ -0,0 +1,52 @@
<?php
namespace FireflyIII\Mail;
use FireflyIII\Models\Bill;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class BillWarningMail extends Mailable
{
use Queueable, SerializesModels;
public Bill $bill;
public string $field;
public int $diff;
public string $ipAddress;
/**
* ConfirmEmailChangeMail constructor.
*
* @param Bill $bill
* @param string $field
* @param int $diff
* @param string $ipAddress
*/
public function __construct(Bill $bill, string $field, int $diff, string $ipAddress)
{
$this->bill = $bill;
$this->field = $field;
$this->diff = $diff;
$this->ipAddress = $ipAddress;
}
/**
* Build the message.
*
* @return $this
*/
public function build(): self
{
$subject = (string) trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]);
if (0 === $this->diff) {
$subject = (string) trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]);
}
return $this
->view('emails.bill-warning-html')
->text('emails.bill-warning-text')
->subject($subject);
}
}

View File

@ -37,14 +37,10 @@ class ConfirmEmailChangeMail extends Mailable
{
use Queueable, SerializesModels;
/** @var string IP address of user */
public $ipAddress;
/** @var string New email address */
public $newEmail;
/** @var string Old email address */
public $oldEmail;
/** @var string Confirmation link */
public $uri;
public string $ipAddress;
public string $newEmail;
public string $oldEmail;
public string $uri;
/**
* ConfirmEmailChangeMail constructor.
@ -70,6 +66,6 @@ class ConfirmEmailChangeMail extends Mailable
public function build(): self
{
return $this->view('emails.confirm-email-change-html')->text('emails.confirm-email-change-text')
->subject((string)trans('email.email_change_subject'));
->subject((string) trans('email.email_change_subject'));
}
}

View File

@ -37,6 +37,7 @@ use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Mail\OAuthTokenCreatedMail;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankRepetition;
@ -134,6 +135,11 @@ class EventServiceProvider extends ServiceProvider
UpdatedAccount::class => [
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit',
],
// bill related events:
WarnUserAboutBill::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
],
];
/**

View File

@ -166,6 +166,12 @@ class BillUpdateService
if (array_key_exists('active', $data)) {
$bill->active = $data['active'];
}
if(array_key_exists('end_date', $data)) {
$bill->end_date = $data['end_date'];
}
if(array_key_exists('extension_date', $data)) {
$bill->extension_date = $data['extension_date'];
}
$bill->match = 'EMPTY';
$bill->automatch = true;

View File

@ -0,0 +1,102 @@
<?php
/**
* RecurringCronjob.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Cronjobs;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Jobs\WarnAboutBills;
use FireflyIII\Models\Configuration;
use Log;
/**
* Class BillWarningCronjob
*/
class BillWarningCronjob extends AbstractCronjob
{
/**
* @throws FireflyException
*/
public function fire(): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Configuration $config */
$config = app('fireflyconfig')->get('last_bw_job', 0);
$lastTime = (int)$config->data;
$diff = time() - $lastTime;
$diffForHumans = Carbon::now()->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('The bill warning cron-job has never fired before.');
}
// less than half a day ago:
if ($lastTime > 0 && $diff <= 43200) {
Log::info(sprintf('It has been %s since the bill warning cron-job has fired.', $diffForHumans));
if (false === $this->force) {
Log::info('The cron-job will not fire now.');
$this->message = sprintf('It has been %s since the bill warning cron-job has fired. It will not fire now.', $diffForHumans);
$this->jobFired = false;
$this->jobErrored = false;
$this->jobSucceeded = false;
return;
}
// fire job regardless.
if (true === $this->force) {
Log::info('Execution of the bill warning cron-job has been FORCED.');
}
}
if ($lastTime > 0 && $diff > 43200) {
Log::info(sprintf('It has been %s since the bill warning cron-job has fired. It will fire now!', $diffForHumans));
}
$this->fireWarnings();
app('preferences')->mark();
}
/**
*
*/
private function fireWarnings(): void
{
Log::info(sprintf('Will now fire bill warning job task for date "%s".', $this->date->format('Y-m-d H:i:s')));
/** @var WarnAboutBills $job */
$job = app(WarnAboutBills::class);
$job->setDate($this->date);
$job->setForce($this->force);
$job->handle();
// get stuff from job:
$this->jobFired = true;
$this->jobErrored = false;
$this->jobSucceeded = true;
$this->message = 'Bill warning cron job fired successfully.';
app('fireflyconfig')->set('last_bw_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with bill warning cron job task.');
}
}

View File

@ -68,6 +68,7 @@ class General extends AbstractExtension
$this->getMetaField(),
$this->hasRole(),
$this->getRootSearchOperator(),
$this->carbonize()
];
}
@ -395,6 +396,19 @@ class General extends AbstractExtension
);
}
/**
* @return TwigFunction
*/
protected function carbonize(): TwigFunction
{
return new TwigFunction(
'carbonize',
static function (string $date): Carbon {
return new Carbon($date);
}
);
}
/**
* Will return true if the user is of role X.
*

View File

@ -221,6 +221,7 @@ return [
TransactionJournal::class,
Recurrence::class,
],
'bill_reminder_periods' => [90, 30, 14, 7, 0],
'valid_view_ranges' => ['1D', '1W', '1M', '3M', '6M', '1Y',],
'allowedMimes' => [
/* plain files */
@ -708,7 +709,7 @@ return [
'import_hash', 'import_hash_v2', 'external_id', 'original_source',
// recurring transactions
'recurrence_total', 'recurrence_count'
'recurrence_total', 'recurrence_count',
],
'webhooks' => [
'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3),

View File

@ -102,4 +102,19 @@ return [
// report new journals
'new_journals_subject' => 'Firefly III has created a new transaction|Firefly III has created :count new transactions',
'new_journals_header' => 'Firefly III has created a transaction for you. You can find it in your Firefly III installation:|Firefly III has created :count transactions for you. You can find them in your Firefly III installation:',
// bill warning
'bill_warning_subject_end_date' => 'Your bill ":name" is due to end in :diff days',
'bill_warning_subject_now_end_date' => 'Your bill ":name" is due to end TODAY',
'bill_warning_subject_extension_date' => 'Your bill ":name" is due to be extended or cancelled in :diff days',
'bill_warning_subject_now_extension_date' => 'Your bill ":name" is due to be extended or cancelled TODAY',
'bill_warning_end_date_text' => 'Your bill ":name" is due to end on :date. This moment will pass in about :diff days.',
'bill_warning_extension_date_text' => 'Your bill ":name" is due to be extended or cancelled on :date. This moment will pass in about :diff days.',
'bill_warning_end_date_text_zero' => 'Your bill ":name" is due to end on :date. This moment will pass TODAY!',
'bill_warning_extension_date_text_zero' => 'Your bill ":name" is due to be extended or cancelled on :date. This moment will pass TODAY!',
'bill_warning_please_action' => 'Please take the appropriate action.',
'bill_warning_end_date_html' => 'Your bill <strong>":name"</strong> is due to end on :date. This moment will pass in about <strong>:diff days</strong>.',
'bill_warning_extension_date_html' => 'Your bill <strong>":name"</strong> is due to be extended or cancelled on :date. This moment will pass in about <strong>:diff days</strong>.',
'bill_warning_end_date_html_zero' => 'Your bill <strong>":name"</strong> is due to end on :date. This moment will pass <strong>TODAY!</strong>',
'bill_warning_extension_date_html_zero' => 'Your bill <strong>":name"</strong> is due to be extended or cancelled on :date. This moment will pass <strong>TODAY!</strong>',
];

View File

@ -1309,6 +1309,10 @@ return [
'running_again_loss' => 'Previously linked transactions to this bill may lose their connection, if they (no longer) match the rule(s).',
'bill_related_rules' => 'Rules related to this bill',
'repeats' => 'Repeats',
'bill_end_date_help' => 'Optional field. The bill is expected to end on this date.',
'bill_extension_date_help' => 'Optional field. The bill must be extended (or cancelled) on or before this date.',
'bill_end_index_line' => 'This bill ends on :date',
'bill_extension_index_line' => 'This bill must be extended or cancelled on :date',
'connected_journals' => 'Connected transactions',
'auto_match_on' => 'Automatically matched by Firefly III',
'auto_match_off' => 'Not automatically matched by Firefly III',

View File

@ -181,6 +181,7 @@ return [
'login_name' => 'Login',
'is_owner' => 'Is admin?',
'url' => 'URL',
'bill_end_date' => 'End date',
// import
'apply_rules' => 'Apply rules',

View File

@ -22,6 +22,7 @@
{{ ExpandedForm.amountNoCurrency('amount_max') }}
{{ ExpandedForm.date('date',phpdate('Y-m-d')) }}
{{ ExpandedForm.select('repeat_freq',periods,'monthly') }}
{{ ExpandedForm.integer('skip',0) }}
</div>
</div>
@ -32,9 +33,12 @@
<h3 class="box-title">{{ 'optionalFields'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.date('bill_end_date',null, {'helpText': trans('firefly.bill_end_date_help')}) }}
{{ ExpandedForm.date('extension_date',null,{'helpText': trans('firefly.bill_extension_date_help')} ) }}
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
{{ ExpandedForm.integer('skip',0) }}
{{ ExpandedForm.objectGroup() }}
</div>
</div>

View File

@ -27,6 +27,8 @@
{{ ExpandedForm.amountNoCurrency('amount_max') }}
{{ ExpandedForm.date('date',bill.date.format('Y-m-d')) }}
{{ ExpandedForm.select('repeat_freq',periods) }}
{{ ExpandedForm.integer('skip') }}
</div>
</div>
@ -38,9 +40,11 @@
<h3 class="box-title">{{ 'optionalFields'|_ }}</h3>
</div>
<div class="box-body">
{{ ExpandedForm.date('bill_end_date',null, {'helpText': trans('firefly.bill_end_date_help')}) }}
{{ ExpandedForm.date('extension_date',null,{'helpText': trans('firefly.bill_extension_date_help')} ) }}
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
{{ ExpandedForm.integer('skip') }}
{{ ExpandedForm.objectGroup() }}
{# only correct way to do active checkbox #}
{{ ExpandedForm.checkbox('active', 1) }}

View File

@ -0,0 +1,25 @@
{% include 'emails.header-html' %}
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:13px;">
{% if field == 'end_date' and diff != 0 %}
{{ trans('email.bill_warning_end_date_html', {name: bill.name|escape, date: bill.end_date.isoFormat(trans('config.month_and_day_js')), diff: diff})|raw }}
{% endif %}
{% if field == 'extension_date' and diff != 0 %}
{{ trans('email.bill_warning_extension_date_html', {name: bill.name|escape, date: bill.extension_date.isoFormat(trans('config.month_and_day_js')), diff: diff})|raw }}
{% endif %}
{% if field == 'end_date' and diff == 0 %}
{{ trans('email.bill_warning_end_date_html_zero', {name: bill.name|escape, date: bill.end_date.isoFormat(trans('config.month_and_day_js'))})|raw }}
{% endif %}
{% if field == 'extension_date' and diff == 0 %}
{{ trans('email.bill_warning_extension_date_html_zero', {name: bill.name|escape, date: bill.end_date.isoFormat(trans('config.month_and_day_js'))})|raw }}
{% endif %}
</p>
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:13px;">
Please take the appropriate action.
</p>
{% include 'emails.footer-html' %}

View File

@ -0,0 +1,17 @@
{% include 'emails.header-text' %}
{% if field == 'end_date' and diff != 0 %}
{{ trans('email.bill_warning_end_date_text', {name: bill.name, date: bill.end_date.isoFormat(trans('config.month_and_day_js')), diff: diff})|raw }}
{% endif %}
{% if field == 'extension_date' and diff != 0 %}
{{ trans('email.bill_warning_extension_date_text', {name: bill.name|escape, date: bill.extension_date.isoFormat(trans('config.month_and_day_js')), diff: diff})|raw }}
{% endif %}
{% if field == 'end_date' and diff == 0 %}
{{ trans('email.bill_warning_end_date_text_zero', {name: bill.name|escape, date: bill.end_date.isoFormat(trans('config.month_and_day_js'))})|raw }}
{% endif %}
{% if field == 'extension_date' and diff == 0 %}
{{ trans('email.bill_warning_extension_date_text_zero', {name: bill.name|escape, date: bill.end_date.isoFormat(trans('config.month_and_day_js'))})|raw }}
{% endif %}
{{ trans('email.bill_warning_please_action') }}
{% include 'emails.footer-text' %}

View File

@ -16,20 +16,25 @@
</thead>
{% for objectGroupOrder, objectGroup in bills %}
{% if objectGroup.bills|length > 0 %}
<tbody class="bill-connected-list" {% if objectGroupOrder != 0 %}data-title="{{ objectGroup.object_group_title }}" {% else %}data-title=""{% endif %}>
<tbody class="bill-connected-list"
{% if objectGroupOrder != 0 %}data-title="{{ objectGroup.object_group_title }}"
{% else %}data-title=""{% endif %}>
<tr>
<td class="hidden-sm hidden-xs">&nbsp;</td>
<td class="hidden-sm hidden-xs">&nbsp;</td>
<td colspan="6"><small>{{ objectGroup.object_group_title }}</small></td>
</tr>
{% for entry in objectGroup.bills %}
<tr class="bill-sortable" data-id="{{ entry.id }}" data-name="{{ entry.name }}" data-order="{{ entry.order }}" data-position="{{ loop.index0 }}">
<tr class="bill-sortable" data-id="{{ entry.id }}" data-name="{{ entry.name }}"
data-order="{{ entry.order }}" data-position="{{ loop.index0 }}">
<td class="hidden-sm hidden-xs">
<span class="fa fa-fw fa-bars bill-handle"></span>
</td>
<td class="hidden-sm hidden-xs">
<div class="btn-group btn-group-xs edit_tr_buttons"><a href="{{ route('bills.edit',entry.id) }}" class="btn btn-default btn-xs"><span
class="fa fa-fw fa-pencil"></span></a><a href="{{ route('bills.delete',entry.id) }}" class="btn btn-danger btn-xs"><span
<div class="btn-group btn-group-xs edit_tr_buttons"><a href="{{ route('bills.edit',entry.id) }}"
class="btn btn-default btn-xs"><span
class="fa fa-fw fa-pencil"></span></a><a
href="{{ route('bills.delete',entry.id) }}" class="btn btn-danger btn-xs"><span
class="fa fa-fw fa-trash-o"></span></a></div>
</td>
<td>
@ -126,6 +131,26 @@
{% if entry.skip > 0 %}
{{ 'skips_over'|_ }} {{ entry.skip }}
{% endif %}
{% if entry.end_date %}
<br>
{% if carbonize(entry.end_date).lte(today) %}
<span class="text-danger">
{{ trans('firefly.bill_end_index_line', {date: carbonize(entry.end_date).isoFormat(monthAndDayFormat) }) }}
</span>
{% else %}
{{ trans('firefly.bill_end_index_line', {date: carbonize(entry.end_date).isoFormat(monthAndDayFormat) }) }}
{% endif %}
{% endif %}
{% if entry.extension_date %}
<br>
{% if carbonize(entry.extension_date).lte(today) %}
<span class="text-danger">
{{ trans('firefly.bill_extension_index_line', {date: carbonize(entry.extension_date).isoFormat(monthAndDayFormat) }) }}
</span>
{% else %}
{{ trans('firefly.bill_extension_index_line', {date: carbonize(entry.extension_date).isoFormat(monthAndDayFormat) }) }}
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
@ -150,7 +175,8 @@
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title -->
<small>{{ ('per_period_sum_'~sum.period)|_ }} ({{ sum.currency_name }}) ({{ 'active_bills_only'|_ }})</small>
<small>{{ ('per_period_sum_'~sum.period)|_ }} ({{ sum.currency_name }})
({{ 'active_bills_only'|_ }})</small>
</td>
<td style="text-align: right;"> <!-- amount -->
{{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }}
@ -190,7 +216,8 @@
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title -->
<small>{{ ('per_period_sum_'~sum.period)|_ }} ({{ sum.currency_name }}) ({{ 'active_bills_only_total'|_ }})</small>
<small>{{ ('per_period_sum_'~sum.period)|_ }} ({{ sum.currency_name }})
({{ 'active_bills_only_total'|_ }})</small>
</td>
<td style="text-align: right;"> <!-- amount -->
{{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }}