Working on scheduled reports

This commit is contained in:
Hillel Coren 2017-11-23 10:10:14 +02:00
parent 7336cd4234
commit 0b99e44907
8 changed files with 344 additions and 131 deletions

View File

@ -2,12 +2,17 @@
namespace App\Console\Commands;
use Carbon;
use Str;
use App\Models\Invoice;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Mailers\UserMailer;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Models\ScheduledReport;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use App\Jobs\ExportReportResults;
/**
* Class SendReminders.
@ -46,13 +51,14 @@ class SendReminders extends Command
* @param InvoiceRepository $invoiceRepo
* @param accountRepository $accountRepo
*/
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo)
public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, AccountRepository $accountRepo, UserMailer $userMailer)
{
parent::__construct();
$this->mailer = $mailer;
$this->invoiceRepo = $invoiceRepo;
$this->accountRepo = $accountRepo;
$this->userMailer = $userMailer;
}
public function fire()
@ -63,6 +69,28 @@ class SendReminders extends Command
config(['database.default' => $database]);
}
$this->chargeLateFees();
$this->setReminderEmails();
$this->sendScheduledReports();
$this->info('Done');
if (\Utils::isNinjaDev()) {
$this->info('Stopping early on ninja dev');
exit;
}
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendReminders [{$database}]: Finished successfully");
});
}
}
private function chargeLateFees()
{
$accounts = $this->accountRepo->findWithFees();
$this->info(count($accounts) . ' accounts found with fees');
@ -84,11 +112,13 @@ class SendReminders extends Command
}
}
}
}
private function setReminderEmails()
{
$accounts = $this->accountRepo->findWithReminders();
$this->info(count($accounts) . ' accounts found with reminders');
/** @var \App\Models\Account $account */
foreach ($accounts as $account) {
if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
continue;
@ -97,7 +127,6 @@ class SendReminders extends Command
$invoices = $this->invoiceRepo->findNeedingReminding($account);
$this->info($account->name . ': ' . count($invoices) . ' invoices found');
/** @var Invoice $invoice */
foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice)) {
$this->info('Send email: ' . $invoice->id);
@ -105,15 +134,56 @@ class SendReminders extends Command
}
}
}
}
$this->info('Done');
private function sendScheduledReports()
{
$scheduledReports = ScheduledReport::where('send_date', '=', date('Y-m-d'))->get();
$this->info(count($scheduledReports) . ' scheduled reports');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendReminders [{$database}]: Finished successfully");
});
foreach ($scheduledReports as $scheduledReport) {
$config = json_decode($scheduledReport->config);
$reportType = $config->report_type;
$reportClass = '\\App\\Ninja\\Reports\\' . Str::studly($reportType) . 'Report';
if ($config->range) {
switch ($config->range) {
case 'this_month':
$startDate = Carbon::now()->firstOfMonth()->toDateString();
$endDate = Carbon::now()->lastOfMonth()->toDateString();
break;
case 'last_month':
$startDate = Carbon::now()->subMonth()->firstOfMonth()->toDateString();
$endDate = Carbon::now()->subMonth()->lastOfMonth()->toDateString();
break;
case 'this_year':
$startDate = Carbon::now()->firstOfYear()->toDateString();
$endDate = Carbon::now()->lastOfYear()->toDateString();
break;
case 'last_year':
$startDate = Carbon::now()->subYear()->firstOfYear()->toDateString();
$endDate = Carbon::now()->subYear()->lastOfYear()->toDateString();
break;
}
} else {
$startDate = Carbon::now()->subDays($config->start_date)->toDateString();
$endDate = Carbon::now()->subDays($config->end_date)->toDateString();
}
$report = new $reportClass($startDate, $endDate, true, (array) $config);
$params = [
'startDate' => $startDate,
'endDate' => $endDate,
'report' => $report,
];
$report->run();
$params = array_merge($params, $report->results());
$file = dispatch(new ExportReportResults($scheduledReport->user, $config->export_format, $reportType, $params));
if ($file) {
$this->userMailer->sendScheduledReport($scheduledReport, $file);
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Jobs\ExportReportResults;
use App\Models\Account;
use App\Models\ScheduledReport;
use Auth;
@ -9,7 +10,7 @@ use Input;
use Str;
use Utils;
use View;
use Excel;
use Carbon;
/**
* Class ReportController.
@ -112,7 +113,7 @@ class ReportController extends BaseController
$params = array_merge($params, $report->results());
switch ($action) {
case 'export':
return self::export($format, $reportType, $params);
return dispatch(new ExportReportResults(auth()->user(), $format, $reportType, $params))->export($format);
break;
case 'schedule':
self::schedule($params, $options);
@ -136,11 +137,17 @@ class ReportController extends BaseController
private function schedule($params, $options)
{
$options['report_type'] = $params['reportType'];
$options['range'] = request('range');
$options['start_date'] = $options['range'] ? '' : Carbon::parse($params['startDate'])->diffInDays(null, false); // null,false to get the relative/non-absolute diff
$options['end_date'] = $options['range'] ? '' : Carbon::parse($params['endDate'])->diffInDays(null, false);
$schedule = ScheduledReport::createNew();
$schedule->config = json_encode($options);
$schedule->frequency = request('frequency');
$schedule->send_date = Utils::toSqlDate(request('send_date'));
$schedule->save();
session()->now('message', trans('texts.created_scheduled_report'));
}
private function cancelSchdule()
@ -149,109 +156,7 @@ class ReportController extends BaseController
->whereUserId(auth()->user()->id)
->wherePublicId(request('scheduled_report_id'))
->delete();
}
/**
* @param $format
* @param $reportType
* @param $params
* @todo: Add summary to export
*/
private function export($format, $reportType, $params)
{
if (! Auth::user()->hasPermission('view_all')) {
exit;
}
$format = strtolower($format);
$data = $params['displayData'];
$columns = $params['columns'];
$totals = $params['reportTotals'];
$report = $params['report'];
$filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
$formats = ['csv', 'pdf', 'xlsx', 'zip'];
if (! in_array($format, $formats)) {
throw new \Exception("Invalid format request to export report");
}
//Get labeled header
$data = array_merge(
[
array_map(function($col) {
return $col['label'];
}, $report->tableHeaderArray())
],
$data
);
$summary = [];
if (count(array_values($totals))) {
$summary[] = array_merge([
trans("texts.totals")
], array_map(function ($key) {
return trans("texts.{$key}");
}, array_keys(array_values(array_values($totals)[0])[0])));
}
foreach ($totals as $currencyId => $each) {
foreach ($each as $dimension => $val) {
$tmp = [];
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
foreach ($val as $id => $field) {
$tmp[] = Utils::formatMoney($field, $currencyId);
}
$summary[] = $tmp;
}
}
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
if ($format == 'csv') {
$sheet->rows(array_merge($data, [[]], $summary));
} else {
$sheet->rows($data);
}
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
$excel->sheet(trans("texts.totals"), function($sheet) use($report, $summary, $format) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
$sheet->rows($summary);
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($summary[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
})->export($format);
session()->now('message', trans('texts.deleted_scheduled_report'));
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Jobs;
use Utils;
use Excel;
use App\Jobs\Job;
class ExportReportResults extends Job
{
public function __construct($user, $format, $reportType, $params)
{
$this->user = $user;
$this->format = strtolower($format);
$this->reportType = $reportType;
$this->params = $params;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (! $this->user->hasPermission('view_all')) {
return false;
}
$format = $this->format;
$reportType = $this->reportType;
$params = $this->params;
$data = $params['displayData'];
$columns = $params['columns'];
$totals = $params['reportTotals'];
$report = $params['report'];
$filename = "{$params['startDate']}-{$params['endDate']}_invoiceninja-".strtolower(Utils::normalizeChars(trans("texts.$reportType")))."-report";
$formats = ['csv', 'pdf', 'xlsx', 'zip'];
if (! in_array($format, $formats)) {
throw new \Exception("Invalid format request to export report");
}
//Get labeled header
$data = array_merge(
[
array_map(function($col) {
return $col['label'];
}, $report->tableHeaderArray())
],
$data
);
$summary = [];
if (count(array_values($totals))) {
$summary[] = array_merge([
trans("texts.totals")
], array_map(function ($key) {
return trans("texts.{$key}");
}, array_keys(array_values(array_values($totals)[0])[0])));
}
foreach ($totals as $currencyId => $each) {
foreach ($each as $dimension => $val) {
$tmp = [];
$tmp[] = Utils::getFromCache($currencyId, 'currencies')->name . (($dimension) ? ' - ' . $dimension : '');
foreach ($val as $id => $field) {
$tmp[] = Utils::formatMoney($field, $currencyId);
}
$summary[] = $tmp;
}
}
return Excel::create($filename, function($excel) use($report, $data, $reportType, $format, $summary) {
$excel->sheet(trans("texts.$reportType"), function($sheet) use($report, $data, $format, $summary) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
if ($format == 'csv') {
$sheet->rows(array_merge($data, [[]], $summary));
} else {
$sheet->rows($data);
}
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($data[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
$excel->sheet(trans("texts.totals"), function($sheet) use($report, $summary, $format) {
$sheet->setOrientation('landscape');
$sheet->freezeFirstRow();
if ($format == 'pdf') {
$sheet->setAllBorders('thin');
}
$sheet->rows($summary);
// Styling header
$sheet->cells('A1:'.Utils::num2alpha(count($summary[0])-1).'1', function($cells) {
$cells->setBackground('#777777');
$cells->setFontColor('#FFFFFF');
$cells->setFontSize(13);
$cells->setFontFamily('Calibri');
$cells->setFontWeight('bold');
});
$sheet->setAutoSize(true);
});
});
}
}

View File

@ -17,6 +17,23 @@ class ScheduledReport extends EntityModel
protected $fillable = [
'frequency',
'config',
'send_date',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
{
return $this->belongsTo('App\Models\Account');
}
/**
* @return mixed
*/
public function user()
{
return $this->belongsTo('App\Models\User')->withTrashed();
}
}

View File

@ -169,4 +169,27 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendScheduledReport($scheduledReport, $file)
{
$user = $scheduledReport->user;
$config = json_decode($scheduledReport->config);
if (! $user->email) {
return;
}
$subject = sprintf('%s - %s %s', APP_NAME, trans('texts.' . $config->report_type), trans('texts.report'));
$view = 'user_message';
$data = [
'userName' => $user->getDisplayName(),
'primaryMessage' => trans('texts.scheduled_report_attached', ['type' => trans('texts.' . $config->report_type)]),
'documents' => [[
'name' => $file->filename . '.' . $config->export_format,
'data' => $file->string($config->export_format),
]]
];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
}

View File

@ -54,7 +54,8 @@ class AddSubdomainToLookups extends Migration
$table->softDeletes();
$table->text('config');
$table->enum('frequency', ['daily', 'weekly', 'monthly']);
$table->enum('frequency', ['daily', 'weekly', 'biweekly', 'monthly']);
$table->date('send_date');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');

View File

@ -1380,6 +1380,7 @@ $LANG = array(
'freq_inactive' => 'Inactive',
'freq_daily' => 'Daily',
'freq_weekly' => 'Weekly',
'freq_biweekly' => 'Biweekly',
'freq_two_weeks' => 'Two weeks',
'freq_four_weeks' => 'Four weeks',
'freq_monthly' => 'Monthly',
@ -2547,6 +2548,11 @@ $LANG = array(
'delivery_note' => 'Delivery Note',
'show_tasks_in_portal' => 'Show tasks in the client portal',
'cancel_schedule' => 'Cancel Schedule',
'scheduled_report' => 'Scheduled Report',
'scheduled_report_help' => 'Email the :report report as :format to :email',
'created_scheduled_report' => 'Successfully scheduled report',
'deleted_scheduled_report' => 'Successfully canceled scheduled report',
'scheduled_report_attached' => 'Your scheduled :type report is attached.',
);

View File

@ -14,7 +14,6 @@
color: white;
background-color: #777 !important;
}
</style>
@stop
@ -42,10 +41,25 @@
var chartEndDate = moment("{{ $endDate }}");
var dateRanges = {!! $account->present()->dateRangeOptions !!};
function resolveRange(range) {
if (range == "{{ trans('texts.this_month') }}") {
return 'this_month';
} else if (range == "{{ trans('texts.last_month') }}") {
return 'last_month';
} else if (range == "{{ trans('texts.this_year') }}") {
return 'this_year';
} else if (range == "{{ trans('texts.last_year') }}") {
return 'last_year';
} else {
return '';
}
}
$(function() {
if (isStorageSupported()) {
var lastRange = localStorage.getItem('last:report_range');
$('#range').val(resolveRange(lastRange));
lastRange = dateRanges[lastRange];
if (lastRange) {
chartStartDate = lastRange[0];
@ -58,6 +72,9 @@
$('#reportrange span').html(start.format('{{ $account->getMomentDateFormat() }}') + ' - ' + end.format('{{ $account->getMomentDateFormat() }}'));
$('#start_date').val(start.format('YYYY-MM-DD'));
$('#end_date').val(end.format('YYYY-MM-DD'));
if (label) {
$('#range').val(resolveRange(label));
}
if (isStorageSupported() && label && label != "{{ trans('texts.custom_range') }}") {
localStorage.setItem('last:report_range', label);
@ -86,7 +103,7 @@
<div style="display:none">
{!! Former::text('action') !!}
{!! Former::text('frequency') !!}
{!! Former::text('range') !!}
{!! Former::text('scheduled_report_id') !!}
</div>
@ -194,14 +211,10 @@
->withAttributes(['id' => 'cancelSchduleButton', 'onclick' => 'onCancelScheduleClick()', 'style' => 'display:none'])
->appendIcon(Icon::create('remove')) !!}
{!! Button::primary(trans('texts.schedule'))
->withAttributes(['id'=>'scheduleButton', 'onclick' => 'showScheduleModal()'])
->appendIcon(Icon::create('time')) !!}
{!! DropdownButton::primary(trans('texts.schedule'))
->withAttributes(['id'=>'scheduleDropDown'])
->withContents([
['url' => 'javascript:onScheduleClick("daily")', 'label' => trans('texts.freq_daily')],
['url' => 'javascript:onScheduleClick("weekly")', 'label' => trans('texts.freq_weekly')],
['url' => 'javascript:onScheduleClick("monthly")', 'label' => trans('texts.freq_monthly')],
]) !!}
</span> &nbsp;&nbsp;
{!! Button::success(trans('texts.run'))
->withAttributes(array('id' => 'submitButton'))
@ -210,9 +223,6 @@
->large() !!}
</center>
{!! Former::close() !!}
@if (request()->report_type)
<div class="panel panel-default">
<div class="panel-body">
@ -260,7 +270,7 @@
<table class="tablesorter tablesorter-data" style="display:none">
<thead>
<tr>
{!! $report->tableHeader() !!}
{!! $report ? $report->tableHeader() : '' !!}
</tr>
</thead>
<tbody>
@ -292,6 +302,51 @@
@endif
<div class="modal fade" id="scheduleModal" tabindex="-1" role="dialog" aria-labelledby="scheduleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="myModalLabel">{{ trans('texts.scheduled_report') }}</h4>
</div>
<div class="container" style="width: 100%; padding-bottom: 0px !important">
<div class="panel panel-default">
<div class="panel-body">
<center style="padding-bottom:40px;font-size:16px;">
<div id="scheduleHelp"></div>
</center>
{!! Former::select('frequency')
->addOption(trans('texts.freq_daily'), 'daily')
->addOption(trans('texts.freq_weekly'), 'weekly')
->addOption(trans('texts.freq_biweekly'), 'biweekly')
->addOption(trans('texts.freq_monthly'), 'monthly')
->value('weekly') !!} &nbsp;
{!! Former::text('send_date')
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))
->label('start_date')
->appendIcon('calendar')
->placeholder('')
->addGroupClass('send-date') !!}
</div>
</div>
</div>
<div class="modal-footer" id="signUpFooter" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }} </button>
<button type="button" class="btn btn-success" onclick="onScheduleClick()">{{ trans('texts.schedule') }} </button>
</div>
</div>
</div>
</div>
{!! Former::close() !!}
<script type="text/javascript">
var scheduledReports = {!! $scheduledReports !!};
@ -303,6 +358,15 @@
scheduledReportMap[config.report_type] = schedule.public_id;
}
function showScheduleModal() {
var help = "{{ trans('texts.scheduled_report_help') }}";
help = help.replace(':email', "{{ auth()->user()->email }}");
help = help.replace(':format', $("#format").val().toUpperCase());
help = help.replace(':report', $("#report_type option:selected").text());
$('#scheduleHelp').text(help);
$('#scheduleModal').modal('show');
}
function onExportClick() {
$('#action').val('export');
$('#submitButton').click();
@ -311,7 +375,6 @@
function onScheduleClick(frequency) {
$('#action').val('schedule');
$('#frequency').val(frequency);
$('#submitButton').click();
$('#action').val('');
}
@ -346,7 +409,7 @@
function setScheduleButton() {
var reportType = $('#report_type').val();
$('#scheduleDropDown').toggle(! scheduledReportMap[reportType]);
$('#scheduleButton').toggle(! scheduledReportMap[reportType]);
$('#cancelSchduleButton').toggle(!! scheduledReportMap[reportType]);
}
@ -466,4 +529,8 @@
keyboardNavigation: false
});
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + 1);
$('#send_date').datepicker('update', currentDate);
@stop