diff --git a/app/Http/Controllers/ClientPortal/StatementController.php b/app/Http/Controllers/ClientPortal/StatementController.php new file mode 100644 index 000000000000..2368ca395038 --- /dev/null +++ b/app/Http/Controllers/ClientPortal/StatementController.php @@ -0,0 +1,59 @@ +client()->service()->statement( + $request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table']) + ); + + if ($pdf && $request->query('download')) { + return response()->streamDownload(function () use ($pdf) { + echo $pdf; + }, 'statement.pdf', ['Content-Type' => 'application/pdf']); + } + + if ($pdf) { + return response($pdf, 200)->withHeaders([ + 'Content-Type' => 'application/pdf', + ]); + } + + return response()->json(['message' => 'Something went wrong. Please check logs.']); + } +} diff --git a/app/Http/Controllers/ClientStatementController.php b/app/Http/Controllers/ClientStatementController.php index 6acf4d6d89bc..002aadd2b882 100644 --- a/app/Http/Controllers/ClientStatementController.php +++ b/app/Http/Controllers/ClientStatementController.php @@ -110,8 +110,11 @@ class ClientStatementController extends BaseController public function statement(CreateStatementRequest $request) { - $pdf = $this->createStatement($request); - + $pdf = $request->client()->service()->statement([ + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + ]); + if ($pdf) { return response()->streamDownload(function () use ($pdf) { echo $pdf; @@ -120,88 +123,4 @@ class ClientStatementController extends BaseController return response()->json(['message' => 'Something went wrong. Please check logs.']); } - - protected function createStatement(CreateStatementRequest $request): ?string - { - $invitation = false; - - if ($request->getInvoices()->count() >= 1) { - $this->entity = $request->getInvoices()->first(); - $invitation = $this->entity->invitations->first(); - } - else if ($request->getPayments()->count() >= 1) { - $this->entity = $request->getPayments()->first()->invoices->first()->invitations->first(); - $invitation = $this->entity->invitations->first(); - } - - $entity_design_id = 1; - - $entity_design_id = $this->entity->design_id - ? $this->entity->design_id - : $this->decodePrimaryKey($this->entity->client->getSetting('invoice_design_id')); - - - $design = Design::find($entity_design_id); - - if (!$design) { - $design = Design::find($entity_design_id); - } - - $html = new HtmlEngine($invitation); - - $options = [ - 'start_date' => $request->start_date, - 'end_date' => $request->end_date, - 'show_payments_table' => $request->show_payments_table, - 'show_aging_table' => $request->show_aging_table, - ]; - - if ($design->is_custom) { - $options['custom_partials'] = \json_decode(\json_encode($design->design), true); - $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); - } else { - $template = new PdfMakerDesign(strtolower($design->name), $options); - } - - $variables = $html->generateLabelsAndValues(); - - $state = [ - 'template' => $template->elements([ - 'client' => $this->entity->client, - 'entity' => $this->entity, - 'pdf_variables' => (array)$this->entity->company->settings->pdf_variables, - '$product' => $design->design->product, - 'variables' => $variables, - 'invoices' => $request->getInvoices(), - 'payments' => $request->getPayments(), - 'aging' => $request->getAging(), - ], \App\Services\PdfMaker\Design::STATEMENT), - 'variables' => $variables, - 'options' => [], - 'process_markdown' => $this->entity->client->company->markdown_enabled, - ]; - - $maker = new PdfMakerService($state); - - $maker - ->design($template) - ->build(); - - $pdf = null; - - try { - if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { - $pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); - } - else if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { - $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); - } else { - $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); - } - } catch (\Exception $e) { - nlog(print_r($e->getMessage(), 1)); - } - - return $pdf; - } } diff --git a/app/Http/Livewire/Statement.php b/app/Http/Livewire/Statement.php new file mode 100644 index 000000000000..42f5a7527d19 --- /dev/null +++ b/app/Http/Livewire/Statement.php @@ -0,0 +1,49 @@ + 0, + 'show_aging_table' => 0, + ]; + + public function mount(): void + { + $this->options['start_date'] = now()->startOfYear()->format('Y-m-d'); + $this->options['end_date'] = now()->format('Y-m-d'); + } + + protected function getCurrentUrl(): string + { + return route('client.statement.raw', $this->options); + } + + public function download() + { + return redirect()->route('client.statement.raw', \array_merge($this->options, ['download' => 1])); + } + + public function render(): View + { + $this->url = route('client.statement.raw', $this->options); + + return render('components.statement'); + } +} diff --git a/app/Http/Requests/ClientPortal/Statements/ShowStatementRequest.php b/app/Http/Requests/ClientPortal/Statements/ShowStatementRequest.php new file mode 100644 index 000000000000..c205aac9095c --- /dev/null +++ b/app/Http/Requests/ClientPortal/Statements/ShowStatementRequest.php @@ -0,0 +1,49 @@ +merge([ + 'show_payments_table' => $this->has('show_payments_table') ? \boolval($this->show_payments_table) : false, + 'show_aging_table' => $this->has('show_aging_table') ? \boolval($this->show_aging_table) : false, + ]); + } + + public function client(): Client + { + return auth('contact')->user()->client; + } +} diff --git a/app/Http/Requests/Statements/CreateStatementRequest.php b/app/Http/Requests/Statements/CreateStatementRequest.php index 9a75794a550d..c4a2c31506ba 100644 --- a/app/Http/Requests/Statements/CreateStatementRequest.php +++ b/app/Http/Requests/Statements/CreateStatementRequest.php @@ -4,11 +4,7 @@ namespace App\Http\Requests\Statements; use App\Http\Requests\Request; use App\Models\Client; -use App\Models\Invoice; -use App\Models\Payment; -use App\Utils\Number; use App\Utils\Traits\MakesHash; -use Carbon\Carbon; class CreateStatementRequest extends Request { @@ -33,137 +29,23 @@ class CreateStatementRequest extends Request return [ 'start_date' => 'required|date_format:Y-m-d', 'end_date' => 'required|date_format:Y-m-d', - 'client_id' => 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id, + 'client_id' => 'bail|required|exists:clients,id,company_id,' . auth()->user()->company()->id, 'show_payments_table' => 'boolean', 'show_aging_table' => 'boolean', ]; } + protected function prepareForValidation() { $input = $this->all(); $input = $this->decodePrimaryKeys($input); - + $this->replace($input); } - /** - * The collection of invoices for the statement. - * - * @return Invoice[]|\Illuminate\Database\Eloquent\Collection - */ - public function getInvoices() + + public function client(): ?Client { - $input = $this->all(); - - // $input['start_date & $input['end_date are available. - $client = Client::where('id', $input['client_id'])->first(); - - $from = Carbon::parse($input['start_date']); - $to = Carbon::parse($input['end_date']); - - return Invoice::where('company_id', auth()->user()->company()->id) - ->where('client_id', $client->id) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]) - ->whereBetween('date',[$from, $to]) - ->get(); - } - - /** - * The collection of payments for the statement. - * - * @return Payment[]|\Illuminate\Database\Eloquent\Collection - */ - public function getPayments() - { - // $input['start_date & $input['end_date are available. - $input = $this->all(); - - $client = Client::where('id', $input['client_id'])->first(); - - $from = Carbon::parse($input['start_date']); - $to = Carbon::parse($input['end_date']); - - return Payment::where('company_id', auth()->user()->company()->id) - ->where('client_id', $client->id) - ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) - ->whereBetween('date',[$from, $to]) - ->get(); - - } - - - /** - * The array of aging data. - */ - public function getAging(): array - { - return [ - '0-30' => $this->getAgingAmount('30'), - '30-60' => $this->getAgingAmount('60'), - '60-90' => $this->getAgingAmount('90'), - '90-120' => $this->getAgingAmount('120'), - '120+' => $this->getAgingAmount('120+'), - ]; - } - - private function getAgingAmount($range) - { - $input = $this->all(); - - $ranges = $this->calculateDateRanges($range); - - $from = $ranges[0]; - $to = $ranges[1]; - - $client = Client::where('id', $input['client_id'])->first(); - - $amount = Invoice::where('company_id', auth()->user()->company()->id) - ->where('client_id', $client->id) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0) - ->whereBetween('date',[$from, $to]) - ->sum('balance'); - - return Number::formatMoney($amount, $client); - } - - private function calculateDateRanges($range) - { - - $ranges = []; - - switch ($range) { - case '30': - $ranges[0] = now(); - $ranges[1] = now()->subDays(30); - return $ranges; - break; - case '60': - $ranges[0] = now()->subDays(30); - $ranges[1] = now()->subDays(60); - return $ranges; - break; - case '90': - $ranges[0] = now()->subDays(60); - $ranges[1] = now()->subDays(90); - return $ranges; - break; - case '120': - $ranges[0] = now()->subDays(90); - $ranges[1] = now()->subDays(120); - return $ranges; - break; - case '120+': - $ranges[0] = now()->subDays(120); - $ranges[1] = now()->subYears(40); - return $ranges; - break; - default: - $ranges[0] = now()->subDays(0); - $ranges[1] = now()->subDays(30); - return $ranges; - break; - } - + return Client::where('id', $this->client_id)->first(); } } diff --git a/app/Http/ViewComposers/PortalComposer.php b/app/Http/ViewComposers/PortalComposer.php index e5be85883ea4..3f589c759ef1 100644 --- a/app/Http/ViewComposers/PortalComposer.php +++ b/app/Http/ViewComposers/PortalComposer.php @@ -119,6 +119,8 @@ class PortalComposer $data[] = ['title' => ctrans('texts.tasks'), 'url' => 'client.tasks.index', 'icon' => 'clock']; } + $data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity']; + return $data; } } diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 840a3b7a9754..1fa8ee4b7324 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -85,6 +85,16 @@ class ClientService return $this; } + /** + * Generate the client statement. + * + * @param array $options + */ + public function statement(array $options = []) + { + return (new Statement($this->client, $options))->run(); + } + public function save() :Client { $this->client->save(); diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php new file mode 100644 index 000000000000..5d6d5a0e4376 --- /dev/null +++ b/app/Services/Client/Statement.php @@ -0,0 +1,355 @@ +client = $client; + + $this->options = $options; + } + + public function run(): ?string + { + $this + ->setupOptions() + ->setupEntity(); + + $html = new HtmlEngine($this->getInvitation()); + + if ($this->getDesign()->is_custom) { + $this->options['custom_partials'] = \json_decode(\json_encode($this->getDesign()->design), true); + + $template = new PdfMakerDesign(\App\Services\PdfMaker\Design::CUSTOM, $this->options); + } else { + $template = new PdfMakerDesign(strtolower($this->getDesign()->name), $this->options); + } + + $variables = $html->generateLabelsAndValues(); + + $state = [ + 'template' => $template->elements([ + 'client' => $this->entity->client, + 'entity' => $this->entity, + 'pdf_variables' => (array)$this->entity->company->settings->pdf_variables, + '$product' => $this->getDesign()->design->product, + 'variables' => $variables, + 'invoices' => $this->getInvoices(), + 'payments' => $this->getPayments(), + 'aging' => $this->getAging(), + ], \App\Services\PdfMaker\Design::STATEMENT), + 'variables' => $variables, + 'options' => [], + 'process_markdown' => $this->entity->client->company->markdown_enabled, + ]; + + $maker = new PdfMaker($state); + + $maker + ->design($template) + ->build(); + + $pdf = null; + + try { + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + $pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); + } elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { + $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); + } else { + $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); + } + } catch (\Exception $e) { + nlog(print_r($e->getMessage(), 1)); + } + + if ($this->rollback) { + DB::rollBack(); + } + + return $pdf; + } + + /** + * Setup correct entity instance. + * + * @return Statement + */ + protected function setupEntity(): self + { + if (count($this->getInvoices()) >= 1) { + $this->entity = $this->getInvoices()->first(); + } + + if (\is_null($this->entity)) { + DB::beginTransaction(); + $this->rollback = true; + + $invoice = InvoiceFactory::create($this->client->company->id, $this->client->user->id); + $invoice->client_id = $this->client->id; + $invoice->line_items = $this->buildLineItems(); + $invoice->save(); + + $invitation = InvoiceInvitationFactory::create($invoice->company_id, $invoice->user_id); + $invitation->invoice_id = $invoice->id; + $invitation->client_contact_id = $this->client->contacts->first()->id; + $invitation->save(); + + $this->entity = $invoice; + } + + return $this; + } + + protected function buildLineItems($count = 1) + { + $line_items = []; + + for ($x = 0; $x < $count; $x++) { + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + //$item->cost = 10; + + if (rand(0, 1)) { + $item->tax_name1 = 'GST'; + $item->tax_rate1 = 10.00; + } + + if (rand(0, 1)) { + $item->tax_name1 = 'VAT'; + $item->tax_rate1 = 17.50; + } + + if (rand(0, 1)) { + $item->tax_name1 = 'Sales Tax'; + $item->tax_rate1 = 5; + } + + $product = Product::all()->random(); + + $item->cost = (float) $product->cost; + $item->product_key = $product->product_key; + $item->notes = $product->notes; + $item->custom_value1 = $product->custom_value1; + $item->custom_value2 = $product->custom_value2; + $item->custom_value3 = $product->custom_value3; + $item->custom_value4 = $product->custom_value4; + + $line_items[] = $item; + } + + return $line_items; + } + + /** + * Setup & prepare options. + * + * @return Statement + */ + protected function setupOptions(): self + { + if (! \array_key_exists('start_date', $this->options)) { + $this->options['start_date'] = now()->startOfYear()->format('Y-m-d'); + } + + if (! \array_key_exists('end_date', $this->options)) { + $this->options['end_date'] = now()->format('Y-m-d'); + } + + if (! \array_key_exists('show_payments_table', $this->options)) { + $this->options['show_payments_table'] = false; + } + + if (! \array_key_exists('show_aging_table', $this->options)) { + $this->options['show_aging_table'] = false; + } + + return $this; + } + + /** + * The collection of invoices for the statement. + * + * @return Invoice[]|\Illuminate\Database\Eloquent\Collection + */ + protected function getInvoices(): Collection + { + return Invoice::where('company_id', $this->client->company->id) + ->where('client_id', $this->client->id) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]) + ->whereBetween('date', [$this->options['start_date'], $this->options['end_date']]) + ->get(); + } + + /** + * The collection of payments for the statement. + * + * @return Payment[]|\Illuminate\Database\Eloquent\Collection + */ + protected function getPayments(): Collection + { + return Payment::where('company_id', $this->client->company->id) + ->where('client_id', $this->client->id) + ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) + ->whereBetween('date', [$this->options['start_date'], $this->options['end_date']]) + ->get(); + } + + /** + * Get correct invitation ID. + * + * @return int|bool + */ + protected function getInvitation() + { + if ($this->entity instanceof Invoice || $this->entity instanceof Payment) { + return $this->entity->invitations->first(); + } + + return false; + } + + /** + * Get the array of aging data. + * + * @return array + */ + protected function getAging(): array + { + return [ + '0-30' => $this->getAgingAmount('30'), + '30-60' => $this->getAgingAmount('60'), + '60-90' => $this->getAgingAmount('90'), + '90-120' => $this->getAgingAmount('120'), + '120+' => $this->getAgingAmount('120+'), + ]; + } + + /** + * Generate aging amount. + * + * @param mixed $range + * @return string + */ + private function getAgingAmount($range) + { + $ranges = $this->calculateDateRanges($range); + + $from = $ranges[0]; + $to = $ranges[1]; + + $client = Client::where('id', $this->client->id)->first(); + + $amount = Invoice::where('company_id', $this->client->company->id) + ->where('client_id', $client->id) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('balance', '>', 0) + ->whereBetween('date', [$from, $to]) + ->sum('balance'); + + return Number::formatMoney($amount, $client); + } + + /** + * Calculate date ranges for aging. + * + * @param mixed $range + * @return array + */ + private function calculateDateRanges($range) + { + $ranges = []; + + switch ($range) { + case '30': + $ranges[0] = now(); + $ranges[1] = now()->subDays(30); + return $ranges; + break; + case '60': + $ranges[0] = now()->subDays(30); + $ranges[1] = now()->subDays(60); + return $ranges; + break; + case '90': + $ranges[0] = now()->subDays(60); + $ranges[1] = now()->subDays(90); + return $ranges; + break; + case '120': + $ranges[0] = now()->subDays(90); + $ranges[1] = now()->subDays(120); + return $ranges; + break; + case '120+': + $ranges[0] = now()->subDays(120); + $ranges[1] = now()->subYears(40); + return $ranges; + break; + default: + $ranges[0] = now()->subDays(0); + $ranges[1] = now()->subDays(30); + return $ranges; + break; + } + } + + /** + * Get correct design for statement. + * + * @return \App\Models\Design + */ + protected function getDesign(): Design + { + $id = 1; + + if (!empty($this->client->getSetting('entity_design_id'))) { + $id = (int) $this->client->getSetting('entity_design_id'); + } + + return Design::find($id); + } +} diff --git a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php index b9aea8184d42..02fb145f1280 100644 --- a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php +++ b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php @@ -40,7 +40,10 @@ trait DesignHelpers if (isset($this->context['invoices'])) { $this->invoices = $this->context['invoices']; - $this->entity = $this->invoices->first(); + + if (\count($this->invoices) >= 1) { + $this->entity = $this->invoices->first(); + } } if (isset($this->context['payments'])) { diff --git a/app/Services/PdfMaker/PdfMaker.php b/app/Services/PdfMaker/PdfMaker.php index aded4c4a54fe..aad88b9e37fb 100644 --- a/app/Services/PdfMaker/PdfMaker.php +++ b/app/Services/PdfMaker/PdfMaker.php @@ -82,6 +82,12 @@ class PdfMaker return $this; } + /** + * Return compiled HTML. + * + * @param bool $final deprecated + * @return mixed + */ public function getCompiledHTML($final = false) { $html = $this->document->saveHTML(); diff --git a/resources/views/portal/ninja2020/components/pdf-viewer.blade.php b/resources/views/portal/ninja2020/components/pdf-viewer.blade.php index ec97ae97e34b..7272095362a5 100644 --- a/resources/views/portal/ninja2020/components/pdf-viewer.blade.php +++ b/resources/views/portal/ninja2020/components/pdf-viewer.blade.php @@ -3,7 +3,7 @@ @endphp @push('head') - + @endpush @@ -72,7 +72,7 @@ class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
@@ -86,7 +86,7 @@ @else - + @endif diff --git a/resources/views/portal/ninja2020/components/statement.blade.php b/resources/views/portal/ninja2020/components/statement.blade.php new file mode 100644 index 000000000000..be4713a57c72 --- /dev/null +++ b/resources/views/portal/ninja2020/components/statement.blade.php @@ -0,0 +1,39 @@ +