diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 8c80a19c3abf..1ed44ce989e6 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -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); + } } } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 0f0003e3fb2d..9528aae9a62a 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -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')); } } diff --git a/app/Jobs/ExportReportResults.php b/app/Jobs/ExportReportResults.php new file mode 100644 index 000000000000..17f002fa8d58 --- /dev/null +++ b/app/Jobs/ExportReportResults.php @@ -0,0 +1,124 @@ +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); + }); + + }); + } +} diff --git a/app/Models/ScheduledReport.php b/app/Models/ScheduledReport.php index 9b616fb66edc..852ac72b4f12 100644 --- a/app/Models/ScheduledReport.php +++ b/app/Models/ScheduledReport.php @@ -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(); + } + } diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php index c2f9fd37b460..6b5d130b697a 100644 --- a/app/Ninja/Mailers/UserMailer.php +++ b/app/Ninja/Mailers/UserMailer.php @@ -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); + } } diff --git a/database/migrations/2017_11_15_114422_add_subdomain_to_lookups.php b/database/migrations/2017_11_15_114422_add_subdomain_to_lookups.php index a4528c3ea700..9e13b79d44e8 100644 --- a/database/migrations/2017_11_15_114422_add_subdomain_to_lookups.php +++ b/database/migrations/2017_11_15_114422_add_subdomain_to_lookups.php @@ -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'); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 4415c9a3238d..485cc4717785 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -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.', ); diff --git a/resources/views/reports/report_builder.blade.php b/resources/views/reports/report_builder.blade.php index 9153643d9d55..93cd4350f107 100644 --- a/resources/views/reports/report_builder.blade.php +++ b/resources/views/reports/report_builder.blade.php @@ -14,7 +14,6 @@ color: white; background-color: #777 !important; } - @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 @@
@@ -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')], - ]) !!} {!! Button::success(trans('texts.run')) ->withAttributes(array('id' => 'submitButton')) @@ -210,9 +223,6 @@ ->large() !!} - {!! Former::close() !!} - - @if (request()->report_type)