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\Exceptions\FireflyException;
use FireflyIII\Support\Cronjobs\AutoBudgetCronjob; use FireflyIII\Support\Cronjobs\AutoBudgetCronjob;
use FireflyIII\Support\Cronjobs\RecurringCronjob; use FireflyIII\Support\Cronjobs\RecurringCronjob;
use FireflyIII\Support\Cronjobs\BillWarningCronjob;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use InvalidArgumentException; use InvalidArgumentException;
use Log; use Log;
@ -66,7 +67,7 @@ class Cron extends Command
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$this->error(sprintf('"%s" is not a valid date', $this->option('date'))); $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. * Fire recurring transaction cron job.
@ -90,6 +91,17 @@ class Cron extends Command
$this->error($e->getMessage()); $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.'); $this->info('More feedback on the cron jobs can be found in the log files.');
return 0; 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); Log::debug(sprintf('Now in %s', __METHOD__), $data);
$factory = app(TransactionCurrencyFactory::class); $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); app('amount')->getDefaultCurrencyByUser($this->user);
try { try {
@ -82,7 +82,7 @@ class BillFactory
} }
if (array_key_exists('notes', $data)) { if (array_key_exists('notes', $data)) {
$this->updateNote($bill, (string)$data['notes']); $this->updateNote($bill, (string) $data['notes']);
} }
$objectGroupTitle = $data['object_group_title'] ?? ''; $objectGroupTitle = $data['object_group_title'] ?? '';
if ('' !== $objectGroupTitle) { if ('' !== $objectGroupTitle) {
@ -93,7 +93,7 @@ class BillFactory
} }
} }
// try also with ID: // try also with ID:
$objectGroupId = (int)($data['object_group_id'] ?? 0); $objectGroupId = (int) ($data['object_group_id'] ?? 0);
if (0 !== $objectGroupId) { if (0 !== $objectGroupId) {
$objectGroup = $this->findObjectGroupById($objectGroupId); $objectGroup = $this->findObjectGroupById($objectGroupId);
if (null !== $objectGroup) { if (null !== $objectGroup) {
@ -113,8 +113,8 @@ class BillFactory
*/ */
public function find(?int $billId, ?string $billName): ?Bill public function find(?int $billId, ?string $billName): ?Bill
{ {
$billId = (int)$billId; $billId = (int) $billId;
$billName = (string)$billName; $billName = (string) $billName;
$bill = null; $bill = null;
// first find by ID: // first find by ID:
if ($billId > 0) { 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 public function store(BillStoreRequest $request): RedirectResponse
{ {
$billData = $request->getBillData(); $billData = $request->getBillData();
$billData['active'] = true; $billData['active'] = true;
try { try {
$bill = $this->repository->store($billData); $bill = $this->repository->store($billData);

View File

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

View File

@ -136,8 +136,9 @@ class IndexController extends Controller
// summarise per currency / per group. // summarise per currency / per group.
$sums = $this->getSums($bills); $sums = $this->getSums($bills);
$totals = $this->getTotals($sums); $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' => '', 'currency_code' => '',
'amount_max' => $this->string('amount_max'), 'amount_max' => $this->string('amount_max'),
'date' => $this->getCarbonDate('date'), 'date' => $this->getCarbonDate('date'),
'end_date' => $this->getCarbonDate('bill_end_date'),
'extension_date' => $this->getCarbonDate('extension_date'),
'repeat_freq' => $this->string('repeat_freq'), 'repeat_freq' => $this->string('repeat_freq'),
'skip' => $this->integer('skip'), 'skip' => $this->integer('skip'),
'notes' => $this->stringWithNewlines('notes'), 'notes' => $this->stringWithNewlines('notes'),
@ -68,6 +70,8 @@ class BillStoreRequest extends FormRequest
'amount_max' => 'required|numeric|gt:0|max:1000000000', 'amount_max' => 'required|numeric|gt:0|max:1000000000',
'transaction_currency_id' => 'required|exists:transaction_currencies,id', 'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date', 'date' => 'required|date',
'bill_end_date' => 'nullable|date',
'extension_date' => 'nullable|date',
'repeat_freq' => sprintf('required|in:%s', join(',', config('firefly.bill_periods'))), 'repeat_freq' => sprintf('required|in:%s', join(',', config('firefly.bill_periods'))),
'skip' => 'required|integer|gte:0|lte:31', 'skip' => 'required|integer|gte:0|lte:31',
'active' => 'boolean', 'active' => 'boolean',

View File

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

View File

@ -130,7 +130,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
// find budget limit: // find budget limit:
$budgetLimit = $this->findBudgetLimit($autoBudget->budget, $start, $end); $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. // that's easy: create one.
// do nothing else. // do nothing else.
$this->createBudgetLimit($autoBudget, $start, $end); $this->createBudgetLimit($autoBudget, $start, $end);
@ -139,7 +139,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
return; 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, // budget limit exists already,
$this->createRollover($autoBudget); $this->createRollover($autoBudget);
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
@ -277,7 +277,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
$repository = app(OperationsRepositoryInterface::class); $repository = app(OperationsRepositoryInterface::class);
$repository->setUser($autoBudget->budget->user); $repository->setUser($autoBudget->budget->user);
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency); $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'; $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)); 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), 'type' => strtolower($recurrence->transactionType->type),
'date' => $date, 'date' => $date,
'user' => $recurrence->user_id, 'user' => $recurrence->user_id,
'currency_id' => (int)$transaction->transaction_currency_id, 'currency_id' => (int) $transaction->transaction_currency_id,
'currency_code' => null, 'currency_code' => null,
'description' => $transactions->first()->description, 'description' => $transactions->first()->description,
'amount' => $transaction->amount, 'amount' => $transaction->amount,
@ -447,9 +447,9 @@ class CreateRecurringTransactions implements ShouldQueue
'foreign_amount' => $transaction->foreign_amount, 'foreign_amount' => $transaction->foreign_amount,
'reconciled' => false, 'reconciled' => false,
'identifier' => $index, 'identifier' => $index,
'recurrence_id' => (int)$recurrence->id, 'recurrence_id' => (int) $recurrence->id,
'order' => $index, '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), 'tags' => $this->repository->getTags($transaction),
'piggy_bank_id' => $this->repository->getPiggyBank($transaction), 'piggy_bank_id' => $this->repository->getPiggyBank($transaction),
'piggy_bank_name' => null, 'piggy_bank_name' => null,

View File

@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Jobs; namespace FireflyIII\Jobs;
use Log;
use FireflyIII\Models\WebhookMessage; use FireflyIII\Models\WebhookMessage;
use FireflyIII\Services\Webhook\WebhookSenderInterface; use FireflyIII\Services\Webhook\WebhookSenderInterface;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -32,6 +31,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Log;
/** /**
* Class SendWebhookMessage * 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; use Queueable, SerializesModels;
/** @var string Email address of admin */ public string $email;
public $email; public string $ipAddress;
/** @var string IP address of admin */
public $ipAddress;
/** /**
* ConfirmEmailChangeMail constructor. * 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; use Queueable, SerializesModels;
/** @var string IP address of user */ public string $ipAddress;
public $ipAddress; public string $newEmail;
/** @var string New email address */ public string $oldEmail;
public $newEmail; public string $uri;
/** @var string Old email address */
public $oldEmail;
/** @var string Confirmation link */
public $uri;
/** /**
* ConfirmEmailChangeMail constructor. * ConfirmEmailChangeMail constructor.
@ -70,6 +66,6 @@ class ConfirmEmailChangeMail extends Mailable
public function build(): self public function build(): self
{ {
return $this->view('emails.confirm-email-change-html')->text('emails.confirm-email-change-text') 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\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\UserChangedEmail; use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Mail\OAuthTokenCreatedMail; use FireflyIII\Mail\OAuthTokenCreatedMail;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\PiggyBankRepetition;
@ -134,6 +135,11 @@ class EventServiceProvider extends ServiceProvider
UpdatedAccount::class => [ UpdatedAccount::class => [
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit', '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)) { if (array_key_exists('active', $data)) {
$bill->active = $data['active']; $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->match = 'EMPTY';
$bill->automatch = true; $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->getMetaField(),
$this->hasRole(), $this->hasRole(),
$this->getRootSearchOperator(), $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. * Will return true if the user is of role X.
* *

View File

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

View File

@ -24,82 +24,97 @@ declare(strict_types=1);
return [ return [
// common items // common items
'greeting' => 'Hi there,', 'greeting' => 'Hi there,',
'closing' => 'Beep boop,', 'closing' => 'Beep boop,',
'signature' => 'The Firefly III Mail Robot', 'signature' => 'The Firefly III Mail Robot',
'footer_ps' => 'PS: This message was sent because a request from IP :ipAddress triggered it.', 'footer_ps' => 'PS: This message was sent because a request from IP :ipAddress triggered it.',
// admin test // admin test
'admin_test_subject' => 'A test message from your Firefly III installation', 'admin_test_subject' => 'A test message from your Firefly III installation',
'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.', 'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.',
// new IP // new IP
'login_from_new_ip' => 'New login on Firefly III', 'login_from_new_ip' => 'New login on Firefly III',
'new_ip_body' => 'Firefly III detected a new login on your account from an unknown IP address. If you never logged in from the IP address below, or it has been more than six months ago, Firefly III will warn you.', 'new_ip_body' => 'Firefly III detected a new login on your account from an unknown IP address. If you never logged in from the IP address below, or it has been more than six months ago, Firefly III will warn you.',
'new_ip_warning' => 'If you recognize this IP address or the login, you can ignore this message. If you didn\'t login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!', 'new_ip_warning' => 'If you recognize this IP address or the login, you can ignore this message. If you didn\'t login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!',
'ip_address' => 'IP address', 'ip_address' => 'IP address',
'host_name' => 'Host', 'host_name' => 'Host',
'date_time' => 'Date + time', 'date_time' => 'Date + time',
// access token created // access token created
'access_token_created_subject' => 'A new access token was created', 'access_token_created_subject' => 'A new access token was created',
'access_token_created_body' => 'Somebody (hopefully you) just created a new Firefly III API Access Token for your user account.', 'access_token_created_body' => 'Somebody (hopefully you) just created a new Firefly III API Access Token for your user account.',
'access_token_created_explanation' => 'With this token, they can access <strong>all</strong> of your financial records through the Firefly III API.', 'access_token_created_explanation' => 'With this token, they can access <strong>all</strong> of your financial records through the Firefly III API.',
'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url.', 'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url.',
// registered // registered
'registered_subject' => 'Welcome to Firefly III!', 'registered_subject' => 'Welcome to Firefly III!',
'registered_welcome' => 'Welcome to <a style="color:#337ab7" href=":address">Firefly III</a>. Your registration has made it, and this email is here to confirm it. Yay!', 'registered_welcome' => 'Welcome to <a style="color:#337ab7" href=":address">Firefly III</a>. Your registration has made it, and this email is here to confirm it. Yay!',
'registered_pw' => 'If you have forgotten your password already, please reset it using <a style="color:#337ab7" href=":address/password/reset">the password reset tool</a>.', 'registered_pw' => 'If you have forgotten your password already, please reset it using <a style="color:#337ab7" href=":address/password/reset">the password reset tool</a>.',
'registered_help' => 'There is a help-icon in the top right corner of each page. If you need help, click it!', 'registered_help' => 'There is a help-icon in the top right corner of each page. If you need help, click it!',
'registered_doc_html' => 'If you haven\'t already, please read the <a style="color:#337ab7" href="https://docs.firefly-iii.org/about-firefly-iii/personal-finances">grand theory</a>.', 'registered_doc_html' => 'If you haven\'t already, please read the <a style="color:#337ab7" href="https://docs.firefly-iii.org/about-firefly-iii/personal-finances">grand theory</a>.',
'registered_doc_text' => 'If you haven\'t already, please read the first use guide and the full description.', 'registered_doc_text' => 'If you haven\'t already, please read the first use guide and the full description.',
'registered_closing' => 'Enjoy!', 'registered_closing' => 'Enjoy!',
'registered_firefly_iii_link' => 'Firefly III:', 'registered_firefly_iii_link' => 'Firefly III:',
'registered_pw_reset_link' => 'Password reset:', 'registered_pw_reset_link' => 'Password reset:',
'registered_doc_link' => 'Documentation:', 'registered_doc_link' => 'Documentation:',
// email change // email change
'email_change_subject' => 'Your Firefly III email address has changed', 'email_change_subject' => 'Your Firefly III email address has changed',
'email_change_body_to_new' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this message, please ignore and delete it.', 'email_change_body_to_new' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this message, please ignore and delete it.',
'email_change_body_to_old' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this to happen, you <strong>must</strong> follow the "undo"-link below to protect your account!', 'email_change_body_to_old' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this to happen, you <strong>must</strong> follow the "undo"-link below to protect your account!',
'email_change_ignore' => 'If you initiated this change, you may safely ignore this message.', 'email_change_ignore' => 'If you initiated this change, you may safely ignore this message.',
'email_change_old' => 'The old email address was: :email', 'email_change_old' => 'The old email address was: :email',
'email_change_old_strong' => 'The old email address was: <strong>:email</strong>', 'email_change_old_strong' => 'The old email address was: <strong>:email</strong>',
'email_change_new' => 'The new email address is: :email', 'email_change_new' => 'The new email address is: :email',
'email_change_new_strong' => 'The new email address is: <strong>:email</strong>', 'email_change_new_strong' => 'The new email address is: <strong>:email</strong>',
'email_change_instructions' => 'You cannot use Firefly III until you confirm this change. Please follow the link below to do so.', 'email_change_instructions' => 'You cannot use Firefly III until you confirm this change. Please follow the link below to do so.',
'email_change_undo_link' => 'To undo the change, follow this link:', 'email_change_undo_link' => 'To undo the change, follow this link:',
// OAuth token created // OAuth token created
'oauth_created_subject' => 'A new OAuth client has been created', 'oauth_created_subject' => 'A new OAuth client has been created',
'oauth_created_body' => 'Somebody (hopefully you) just created a new Firefly III API OAuth Client for your user account. It\'s labeled ":name" and has callback URL <span style="font-family: monospace;">:url</span>.', 'oauth_created_body' => 'Somebody (hopefully you) just created a new Firefly III API OAuth Client for your user account. It\'s labeled ":name" and has callback URL <span style="font-family: monospace;">:url</span>.',
'oauth_created_explanation' => 'With this client, they can access <strong>all</strong> of your financial records through the Firefly III API.', 'oauth_created_explanation' => 'With this client, they can access <strong>all</strong> of your financial records through the Firefly III API.',
'oauth_created_undo' => 'If this wasn\'t you, please revoke this client as soon as possible at :url.', 'oauth_created_undo' => 'If this wasn\'t you, please revoke this client as soon as possible at :url.',
// reset password // reset password
'reset_pw_subject' => 'Your password reset request', 'reset_pw_subject' => 'Your password reset request',
'reset_pw_instructions' => 'Somebody tried to reset your password. If it was you, please follow the link below to do so.', 'reset_pw_instructions' => 'Somebody tried to reset your password. If it was you, please follow the link below to do so.',
'reset_pw_warning' => '<strong>PLEASE</strong> verify that the link actually goes to the Firefly III you expect it to go!', 'reset_pw_warning' => '<strong>PLEASE</strong> verify that the link actually goes to the Firefly III you expect it to go!',
// error // error
'error_subject' => 'Caught an error in Firefly III', 'error_subject' => 'Caught an error in Firefly III',
'error_intro' => 'Firefly III v:version ran into an error: <span style="font-family: monospace;">:errorMessage</span>.', 'error_intro' => 'Firefly III v:version ran into an error: <span style="font-family: monospace;">:errorMessage</span>.',
'error_type' => 'The error was of type ":class".', 'error_type' => 'The error was of type ":class".',
'error_timestamp' => 'The error occurred on/at: :time.', 'error_timestamp' => 'The error occurred on/at: :time.',
'error_location' => 'This error occurred in file "<span style="font-family: monospace;">:file</span>" on line :line with code :code.', 'error_location' => 'This error occurred in file "<span style="font-family: monospace;">:file</span>" on line :line with code :code.',
'error_user' => 'The error was encountered by user #:id, <a href="mailto::email">:email</a>.', 'error_user' => 'The error was encountered by user #:id, <a href="mailto::email">:email</a>.',
'error_no_user' => 'There was no user logged in for this error or no user was detected.', 'error_no_user' => 'There was no user logged in for this error or no user was detected.',
'error_ip' => 'The IP address related to this error is: :ip', 'error_ip' => 'The IP address related to this error is: :ip',
'error_url' => 'URL is: :url', 'error_url' => 'URL is: :url',
'error_user_agent' => 'User agent: :userAgent', 'error_user_agent' => 'User agent: :userAgent',
'error_stacktrace' => 'The full stacktrace is below. If you think this is a bug in Firefly III, you can forward this message to <a href="mailto:james@firefly-iii.org?subject=BUG!">james@firefly-iii.org</a>. This can help fix the bug you just encountered.', 'error_stacktrace' => 'The full stacktrace is below. If you think this is a bug in Firefly III, you can forward this message to <a href="mailto:james@firefly-iii.org?subject=BUG!">james@firefly-iii.org</a>. This can help fix the bug you just encountered.',
'error_github_html' => 'If you prefer, you can also open a new issue on <a href="https://github.com/firefly-iii/firefly-iii/issues">GitHub</a>.', 'error_github_html' => 'If you prefer, you can also open a new issue on <a href="https://github.com/firefly-iii/firefly-iii/issues">GitHub</a>.',
'error_github_text' => 'If you prefer, you can also open a new issue on https://github.com/firefly-iii/firefly-iii/issues.', 'error_github_text' => 'If you prefer, you can also open a new issue on https://github.com/firefly-iii/firefly-iii/issues.',
'error_stacktrace_below' => 'The full stacktrace is below:', 'error_stacktrace_below' => 'The full stacktrace is below:',
'error_headers' => 'The following headers may also be relevant:', 'error_headers' => 'The following headers may also be relevant:',
// report new journals // report new journals
'new_journals_subject' => 'Firefly III has created a new transaction|Firefly III has created :count new transactions', '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:', '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).', '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', 'bill_related_rules' => 'Rules related to this bill',
'repeats' => 'Repeats', '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', 'connected_journals' => 'Connected transactions',
'auto_match_on' => 'Automatically matched by Firefly III', 'auto_match_on' => 'Automatically matched by Firefly III',
'auto_match_off' => 'Not automatically matched by Firefly III', 'auto_match_off' => 'Not automatically matched by Firefly III',

View File

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

View File

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

View File

@ -27,6 +27,8 @@
{{ ExpandedForm.amountNoCurrency('amount_max') }} {{ ExpandedForm.amountNoCurrency('amount_max') }}
{{ ExpandedForm.date('date',bill.date.format('Y-m-d')) }} {{ ExpandedForm.date('date',bill.date.format('Y-m-d')) }}
{{ ExpandedForm.select('repeat_freq',periods) }} {{ ExpandedForm.select('repeat_freq',periods) }}
{{ ExpandedForm.integer('skip') }}
</div> </div>
</div> </div>
@ -38,9 +40,11 @@
<h3 class="box-title">{{ 'optionalFields'|_ }}</h3> <h3 class="box-title">{{ 'optionalFields'|_ }}</h3>
</div> </div>
<div class="box-body"> <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.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
{{ ExpandedForm.integer('skip') }}
{{ ExpandedForm.objectGroup() }} {{ ExpandedForm.objectGroup() }}
{# only correct way to do active checkbox #} {# only correct way to do active checkbox #}
{{ ExpandedForm.checkbox('active', 1) }} {{ 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> </thead>
{% for objectGroupOrder, objectGroup in bills %} {% for objectGroupOrder, objectGroup in bills %}
{% if objectGroup.bills|length > 0 %} {% 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> <tr>
<td class="hidden-sm hidden-xs">&nbsp;</td> <td class="hidden-sm hidden-xs">&nbsp;</td>
<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> <td colspan="6"><small>{{ objectGroup.object_group_title }}</small></td>
</tr> </tr>
{% for entry in objectGroup.bills %} {% 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"> <td class="hidden-sm hidden-xs">
<span class="fa fa-fw fa-bars bill-handle"></span> <span class="fa fa-fw fa-bars bill-handle"></span>
</td> </td>
<td class="hidden-sm hidden-xs"> <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 <div class="btn-group btn-group-xs edit_tr_buttons"><a href="{{ route('bills.edit',entry.id) }}"
class="fa fa-fw fa-pencil"></span></a><a href="{{ route('bills.delete',entry.id) }}" class="btn btn-danger btn-xs"><span 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> class="fa fa-fw fa-trash-o"></span></a></div>
</td> </td>
<td> <td>
@ -126,6 +131,26 @@
{% if entry.skip > 0 %} {% if entry.skip > 0 %}
{{ 'skips_over'|_ }} {{ entry.skip }} {{ 'skips_over'|_ }} {{ entry.skip }}
{% endif %} {% 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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -150,7 +175,8 @@
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title --> <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>
<td style="text-align: right;"> <!-- amount --> <td style="text-align: right;"> <!-- amount -->
{{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }} {{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }}
@ -165,43 +191,44 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if totals|length > 0 %} {% if totals|length > 0 %}
<tfoot> <tfoot>
<tr> <tr>
<td colspan="8" style="border-top:1px #777 solid;"></td> <td colspan="8" style="border-top:1px #777 solid;"></td>
</tr> </tr>
{% for sum in totals %} {% for sum in totals %}
{% if '0' != sum.avg %} {% if '0' != sum.avg %}
<tr> <tr>
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title --> <td colspan="2" style="text-align: right;"> <!-- title -->
<small>{{ 'sum'|_ }} ({{ sum.currency_name }}) ({{ 'active_exp_bills_only_total'|_ }})</small> <small>{{ 'sum'|_ }} ({{ sum.currency_name }}) ({{ 'active_exp_bills_only_total'|_ }})</small>
</td> </td>
<td style="text-align: right;"> <!-- amount --> <td style="text-align: right;"> <!-- amount -->
{{ formatAmountBySymbol(sum.avg, sum.currency_symbol, sum.currency_decimal_places) }} {{ formatAmountBySymbol(sum.avg, sum.currency_symbol, sum.currency_decimal_places) }}
</td> </td>
<td>&nbsp;</td> <!-- paid in period --> <td>&nbsp;</td> <!-- paid in period -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- next expected match --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- next expected match -->
<td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats --> <td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats -->
</tr> </tr>
{% endif %} {% endif %}
{% if '0' != sum.per_period %} {% if '0' != sum.per_period %}
<tr> <tr>
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title --> <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 }})
</td> ({{ 'active_bills_only_total'|_ }})</small>
<td style="text-align: right;"> <!-- amount --> </td>
{{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }} <td style="text-align: right;"> <!-- amount -->
</td> {{ formatAmountBySymbol(sum.per_period, sum.currency_symbol, sum.currency_decimal_places) }}
<td>&nbsp;</td> <!-- paid in period --> </td>
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- next expected match --> <td>&nbsp;</td> <!-- paid in period -->
<td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats --> <td class="hidden-sm hidden-xs">&nbsp;</td> <!-- next expected match -->
</tr> <td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats -->
{% endif %} </tr>
{% endfor %} {% endif %}
</tfoot> {% endfor %}
</tfoot>
{% endif %} {% endif %}
</table> </table>