From 3405b91c6458962cfb7a47c9ac8ce648b03819e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 6 Nov 2019 09:52:57 +1100 Subject: [PATCH] Bulk download PDF, Client Portal formatting (#3046) * Update OpenAPI for TemplateController * Add bulk invoice download functionality * Working on Client portal * Move selective queries to cache instead of DB * Fix formatting in Payments table, implement cache for languages, resolve route model for clientcontacts and users --- .../ClientPortal/InvoiceController.php | 40 +++-- .../ClientPortal/PaymentController.php | 9 +- .../RecurringInvoiceController.php | 14 +- app/Http/Controllers/TemplateController.php | 2 +- app/Http/Middleware/QueryLogging.php | 2 +- app/Jobs/Invoice/CreateInvoicePdf.php | 155 ++++++++++++++++++ app/Listeners/Invoice/CreateInvoicePdf.php | 114 +------------ app/Models/Client.php | 20 ++- app/Models/ClientContact.php | 24 ++- app/Models/Invoice.php | 12 ++ app/Models/RecurringInvoice.php | 3 +- app/Models/User.php | 13 +- .../ClientContactRequestCancellation.php | 2 +- composer.json | 1 + .../portal/default/invoices/index.blade.php | 5 +- .../default/recurring_invoices/show.blade.php | 6 +- tests/Integration/CheckCacheTest.php | 39 +++++ 17 files changed, 319 insertions(+), 142 deletions(-) create mode 100644 app/Jobs/Invoice/CreateInvoicePdf.php create mode 100644 tests/Integration/CheckCacheTest.php diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index c656b116f314..d3580d4d2694 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -20,11 +20,14 @@ use App\Repositories\BaseRepository; use App\Utils\Number; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; -use Barracuda\ArchiveStream\Archive; use Illuminate\Http\Request; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; use Yajra\DataTables\Facades\DataTables; use Yajra\DataTables\Html\Builder; +use ZipStream\Option\Archive; +use ZipStream\ZipStream; /** * Class InvoiceController @@ -168,25 +171,32 @@ class InvoiceController extends Controller { $invoices = Invoice::whereIn('id', $ids) ->whereClientId(auth()->user()->client->id) - ->get() - ->filter(function ($invoice){ - return $invoice->isPayable(); - }); + ->get(); //generate pdf's of invoices locally - + if(!$invoices || $invoices->count() == 0) + return; //if only 1 pdf, output to buffer for download - - - //if multiple pdf's, output to zip stream using Barracuda lib - + if($invoices->count() == 1) + return response()->download(public_path($invoices->first()->pdf_file_path())); + + + # enable output of HTTP headers + $options = new Archive(); + $options->setSendHttpHeaders(true); + + # create a new zipstream object + $zip = new ZipStream(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoices')).".zip", $options); + + foreach($invoices as $invoice){ + + $zip->addFileFromPath(basename($invoice->pdf_file_path()), public_path($invoice->pdf_file_path())); + } + + # finish the zip stream + $zip->finish(); -/* - $zip = Archive::instance_by_useragent(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoices'))); - $zip->add_file($name, $document->getRaw()); - $zip->finish(); -*/ } diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 0dbb2d98c34c..9b049f05ff73 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -46,7 +46,7 @@ class PaymentController extends Controller public function index(PaymentFilters $filters, Builder $builder) { //$payments = Payment::filter($filters); - $payments = Payment::with('type')->get(); + $payments = Payment::with('type','client')->get(); if (request()->ajax()) { @@ -58,6 +58,13 @@ class PaymentController extends Controller ->editColumn('status_id', function ($payment){ return Payment::badgeForStatus($payment->status_id); }) + ->editColumn('payment_date', function ($payment){ + //return $payment->payment_date; + return $payment->formatDate($payment->payment_date, $payment->client->date_format()); + }) + ->editColumn('amount', function ($payment) { + return Number::formatMoney($payment->amount, $payment->client); + }) ->rawColumns(['action', 'status_id','payment_type_id']) ->make(true); diff --git a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php index 73b4c6df7204..4415374b0dc2 100644 --- a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php +++ b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php @@ -16,6 +16,8 @@ use App\Http\Requests\ClientPortal\ShowRecurringInvoiceRequest; use App\Models\RecurringInvoice; use App\Notifications\ClientContactRequestCancellation; use App\Notifications\ClientContactResetPassword; +use App\Utils\Number; +use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -33,7 +35,7 @@ class RecurringInvoiceController extends Controller { use MakesHash; - + use MakesDates; /** * Show the list of Invoices * @@ -46,6 +48,7 @@ class RecurringInvoiceController extends Controller $invoices = RecurringInvoice::whereClientId(auth()->user()->client->id) ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED]) ->orderBy('status_id', 'asc') + ->with('client') ->get(); if (request()->ajax()) { @@ -58,6 +61,15 @@ class RecurringInvoiceController extends Controller ->editColumn('status_id', function ($invoice){ return RecurringInvoice::badgeForStatus($invoice->status); }) + ->editColumn('start_date', function ($invoice){ + return $this->formatDate($invoice->invoice_date, $invoice->client->date_format()); + }) + ->editColumn('next_send_date', function ($invoice){ + return $this->formatDate($invoice->next_send_date, $invoice->client->date_format()); + }) + ->editColumn('amount', function ($invoice){ + return Number::formatMoney($invoice->amount, $invoice->client); + }) ->rawColumns(['action', 'status_id']) ->make(true); diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index 90f1329d0c7c..596bb61aa90a 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -77,7 +77,7 @@ class TemplateController extends BaseController * * @return \Illuminate\Http\Response * - * @OA\Get( + * @OA\Post( * path="/api/v1/templates/{entity}/{entity_id}", * operationId="getShowTemplate", * tags={"templates"}, diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index eae1dad7ab35..128144656e87 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -50,7 +50,7 @@ class QueryLogging Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time); // if($count > 50) - // Log::info($queries); + // Log::info($queries); } } diff --git a/app/Jobs/Invoice/CreateInvoicePdf.php b/app/Jobs/Invoice/CreateInvoicePdf.php new file mode 100644 index 000000000000..7e5aa960e6f0 --- /dev/null +++ b/app/Jobs/Invoice/CreateInvoicePdf.php @@ -0,0 +1,155 @@ +invoice = $invoice; + + } + + public function handle() + { + + + $this->invoice->load('client'); + $path = 'public/' . $this->invoice->client->client_hash . '/invoices/'; + $file_path = $path . $this->invoice->invoice_number . '.pdf'; + + //get invoice design + $html = $this->generateInvoiceHtml($this->invoice->design(), $this->invoice); + + //todo - move this to the client creation stage so we don't keep hitting this unnecessarily + Storage::makeDirectory($path, 0755); + + //create pdf + $pdf = $this->makePdf(null,null,$html); + + $path = Storage::put($file_path, $pdf); + + + } + + /** + * Returns a PDF stream + * + * @param string $header Header to be included in PDF + * @param string $footer Footer to be included in PDF + * @param string $html The HTML object to be converted into PDF + * + * @return string The PDF string + */ + private function makePdf($header, $footer, $html) + { + return Browsershot::html($html) + //->showBrowserHeaderAndFooter() + //->headerHtml($header) + //->footerHtml($footer) + ->waitUntilNetworkIdle(false)->pdf(); + //->margins(10,10,10,10) + //->savePdf('test.pdf'); + } + + /** + * Generate the HTML invoice parsing variables + * and generating the final invoice HTML + * + * @param string $design either the path to the design template, OR the full design template string + * @param Collection $invoice The invoice object + * + * @return string The invoice string in HTML format + */ + private function generateInvoiceHtml($design, $invoice) :string + { + + $variables = array_merge($invoice->makeLabels(), $invoice->makeValues()); + $design = str_replace(array_keys($variables), array_values($variables), $design); + + $data['invoice'] = $invoice; + + return $this->renderView($design, $data); + + //return view($design, $data)->render(); + + } + + /** + * Parses the blade file string and processes the template variables + * + * @param string $string The Blade file string + * @param array $data The array of template variables + * @return string The return HTML string + * + */ + private function renderView($string, $data) :string + { + if (!$data) { + $data = []; + } + + $data['__env'] = app(\Illuminate\View\Factory::class); + + $php = Blade::compileString($string); + + $obLevel = ob_get_level(); + ob_start(); + extract($data, EXTR_SKIP); + + try { + eval('?' . '>' . $php); + } catch (\Exception $e) { + while (ob_get_level() > $obLevel) { + ob_end_clean(); + } + + throw $e; + } catch (\Throwable $e) { + while (ob_get_level() > $obLevel) { + ob_end_clean(); + } + + throw new FatalThrowableError($e); + } + + return ob_get_clean(); + } +} diff --git a/app/Listeners/Invoice/CreateInvoicePdf.php b/app/Listeners/Invoice/CreateInvoicePdf.php index 04f1f1c37767..f136bfee8574 100644 --- a/app/Listeners/Invoice/CreateInvoicePdf.php +++ b/app/Listeners/Invoice/CreateInvoicePdf.php @@ -11,18 +11,13 @@ namespace App\Listeners\Invoice; -use App\Utils\Traits\MakesHash; +use App\Jobs\Invoice\CreateInvoicePdf as PdfCreator; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Support\Facades\Blade; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Storage; -use Spatie\Browsershot\Browsershot; -use Symfony\Component\Debug\Exception\FatalThrowableError; class CreateInvoicePdf implements ShouldQueue { - protected $activity_repo; + /** * Create the event listener. * @@ -40,109 +35,6 @@ class CreateInvoicePdf implements ShouldQueue */ public function handle($event) { - - $invoice = $event->invoice; - - $invoice->load('client'); - $path = 'public/' . $invoice->client->client_hash . '/invoices/'; - $file_path = $path . $invoice->invoice_number . '.pdf'; - - //get invoice design - $html = $this->generateInvoiceHtml($invoice->design(), $invoice); - - //todo - move this to the client creation stage so we don't keep hitting this unnecessarily - Storage::makeDirectory($path, 0755); - - //create pdf - $pdf = $this->makePdf(null,null,$html); - - $path = Storage::put($file_path, $pdf); - - + PdfCreator::dispatch($event->invoice); } - - /** - * Returns a PDF stream - * - * @param string $header Header to be included in PDF - * @param string $footer Footer to be included in PDF - * @param string $html The HTML object to be converted into PDF - * - * @return string The PDF string - */ - private function makePdf($header, $footer, $html) - { - return Browsershot::html($html) - //->showBrowserHeaderAndFooter() - //->headerHtml($header) - //->footerHtml($footer) - ->waitUntilNetworkIdle(false)->pdf(); - //->margins(10,10,10,10) - //->savePdf('test.pdf'); - } - - /** - * Generate the HTML invoice parsing variables - * and generating the final invoice HTML - * - * @param string $design either the path to the design template, OR the full design template string - * @param Collection $invoice The invoice object - * - * @return string The invoice string in HTML format - */ - private function generateInvoiceHtml($design, $invoice) :string - { - - $variables = array_merge($invoice->makeLabels(), $invoice->makeValues()); - $design = str_replace(array_keys($variables), array_values($variables), $design); - - $data['invoice'] = $invoice; - - return $this->renderView($design, $data); - - //return view($design, $data)->render(); - - } - - /** - * Parses the blade file string and processes the template variables - * - * @param string $string The Blade file string - * @param array $data The array of template variables - * @return string The return HTML string - * - */ - private function renderView($string, $data) :string - { - if (!$data) { - $data = []; - } - - $data['__env'] = app(\Illuminate\View\Factory::class); - - $php = Blade::compileString($string); - - $obLevel = ob_get_level(); - ob_start(); - extract($data, EXTR_SKIP); - - try { - eval('?' . '>' . $php); - } catch (\Exception $e) { - while (ob_get_level() > $obLevel) { - ob_end_clean(); - } - - throw $e; - } catch (\Throwable $e) { - while (ob_get_level() > $obLevel) { - ob_end_clean(); - } - - throw new FatalThrowableError($e); - } - - return ob_get_clean(); - } - } diff --git a/app/Models/Client.php b/app/Models/Client.php index d7d5cdef817a..38d36ec323e7 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -31,6 +31,7 @@ use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use Hashids\Hashids; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\URL; use Laracasts\Presenter\PresentableTrait; @@ -168,13 +169,26 @@ class Client extends BaseModel } public function date_format() - { - return DateFormat::find($this->getSetting('date_format_id'))->format; + { + $date_formats = Cache::get('date_formats'); + + return $date_formats->filter(function($item) { + return $item->id == $this->getSetting('date_format_id'); + })->first()->format; + + //return DateFormat::find($this->getSetting('date_format_id'))->format; } public function currency() { - return Currency::find($this->getSetting('currency_id')); + + $currencies = Cache::get('currencies'); + + return $currencies->filter(function($item) { + return $item->id == $this->getSetting('currency_id'); + })->first(); + + //return Currency::find($this->getSetting('currency_id')); //return $this->belongsTo(Currency::class); } diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 8e6c337c2358..629523f0cce6 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -138,8 +138,28 @@ class ClientContact extends Authenticatable implements HasLocalePreference public function preferredLocale() { - $lang = Language::find($this->client->getSetting('language_id')); + $languages = Cache::get('languages'); + + return $languages->filter(function($item) { + return $item->id == $this->client->getSetting('language_id'); + })->first()->locale; - return $lang->locale; + //$lang = Language::find($this->client->getSetting('language_id')); + + //return $lang->locale; + } + + + /** + * Retrieve the model for a bound value. + * + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Model|null + */ + public function resolveRouteBinding($value) + { + return $this + ->withTrashed() + ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 79237e8561a4..42eaff22b679 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -14,6 +14,7 @@ namespace App\Models; use App\Events\Invoice\InvoiceWasUpdated; use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSumInclusive; +use App\Jobs\Invoice\CreateInvoicePdf; use App\Models\Currency; use App\Models\Filterable; use App\Models\PaymentTerm; @@ -298,6 +299,17 @@ class Invoice extends BaseModel return $public_path; } + public function pdf_file_path() + { + $storage_path = 'storage/' . $this->client->client_hash . '/invoices/'. $this->invoice_number . '.pdf'; + + if(!Storage::exists($storage_path)) { + CreateInvoicePdf::dispatchNow($this); + } + + return $storage_path; + } + /** * @param bool $save */ diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index d4eaa2232517..f0c49f6434ac 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -12,6 +12,7 @@ namespace App\Models; use App\Models\Filterable; +use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -25,7 +26,7 @@ class RecurringInvoice extends BaseModel use MakesHash; use SoftDeletes; use Filterable; - + use MakesDates; /** * Invoice Statuses */ diff --git a/app/Models/User.php b/app/Models/User.php index 7c59ceaf1754..73ab78d7f0fe 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -361,6 +361,17 @@ class User extends Authenticatable implements MustVerifyEmail return $this->email; } - + /** + * Retrieve the model for a bound value. + * + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Model|null + */ + public function resolveRouteBinding($value) + { + return $this + ->withTrashed() + ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); + } } diff --git a/app/Notifications/ClientContactRequestCancellation.php b/app/Notifications/ClientContactRequestCancellation.php index 6bf6549efc76..3425c0160005 100644 --- a/app/Notifications/ClientContactRequestCancellation.php +++ b/app/Notifications/ClientContactRequestCancellation.php @@ -46,7 +46,7 @@ class ClientContactRequestCancellation extends Notification implements ShouldQue */ public function via($notifiable) { - return ['mail']; + return ['mail','slack']; } /** diff --git a/composer.json b/composer.json index 4b6a128dc561..e01252b86638 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "laravelcollective/html": "6.0.*", "league/fractal": "^0.17.0", "league/omnipay": "^3.0", + "maennchen/zipstream-php": "^1.2", "nwidart/laravel-modules": "^6.0", "omnipay/paypal": "^3.0", "omnipay/stripe": "^3.0", diff --git a/resources/views/portal/default/invoices/index.blade.php b/resources/views/portal/default/invoices/index.blade.php index 33d42065bc0e..ff7c4693d927 100644 --- a/resources/views/portal/default/invoices/index.blade.php +++ b/resources/views/portal/default/invoices/index.blade.php @@ -178,7 +178,10 @@ $(document).ready(function() { }); $('#download_invoices').click(function() { - alert('download'); + $('#hashed_ids').val(selected); + $('#action').val('download'); + + $('#payment_form').submit(); }); diff --git a/resources/views/portal/default/recurring_invoices/show.blade.php b/resources/views/portal/default/recurring_invoices/show.blade.php index b6175792ca73..df82793c0b6b 100644 --- a/resources/views/portal/default/recurring_invoices/show.blade.php +++ b/resources/views/portal/default/recurring_invoices/show.blade.php @@ -12,11 +12,11 @@
- - + + - +
{{ctrans('texts.start_date')}}{!! $invoice->start_date !!}
{{ctrans('texts.next_send_date')}}{!! $invoice->next_send_date !!}
{{ctrans('texts.start_date')}}{!! $invoice->formatDate($invoice->start_date,$invoice->client->date_format()) !!}
{{ctrans('texts.next_send_date')}}{!! $invoice->formatDate($invoice->next_send_date,$invoice->client->date_format()) !!}
{{ctrans('texts.frequency')}}{!! App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) !!}
{{ctrans('texts.cycles_remaining')}}{!! $invoice->remaining_cycles !!}
{{ctrans('texts.amount')}}{!! $invoice->amount !!}
{{ctrans('texts.amount')}}{!! App\Utils\Number::formatMoney($invoice->amount, $invoice->client) !!}
diff --git a/tests/Integration/CheckCacheTest.php b/tests/Integration/CheckCacheTest.php new file mode 100644 index 000000000000..a4e771185e5c --- /dev/null +++ b/tests/Integration/CheckCacheTest.php @@ -0,0 +1,39 @@ +makeTestData(); + } + + public function testWarmedUpCache() + { + $date_formats = Cache::get('date_formats'); + + $this->assertNotNull($date_formats); + } + + public function testCacheCount() + { + $date_formats = Cache::get('date_formats'); + + $this->assertEquals(13, count($date_formats)); + } +} \ No newline at end of file