Performance improvements (#3634)

* Adjustments for tests

* Implement handling of temp downloading resources

* Refactor paths

* Refactors for file paths

* Refactor paths

* Add in S3 adapter

* Refactor company Documment URL

* Refactor for entity pdf performance

* Refactors for invoice generation

* Enhancements for emails invoices

* Emails

* Fixes for client portal queries
This commit is contained in:
David Bomba 2020-04-16 18:41:25 +10:00 committed by GitHub
parent 74a6c4f2ee
commit e5a230e0c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 877 additions and 296 deletions

View File

@ -5,7 +5,7 @@ APP_URL=http://127.0.0.1:8000
APP_KEY=s7epnjtomsdond5zgfqgaqmwhhcjct02
APP_CIPHER=AES-256-CBC
REQUIRE_HTTPS=false
NINJA_ENVIRONMENT=hosted
NINJA_ENVIRONMENT=development
DB_TYPE=mysql
DB_STRICT=false

View File

@ -21,3 +21,4 @@ TEST_USERNAME=user@example.com
TEST_PERMISSIONS_USERNAME=permissions@example.com
MULTI_DB_ENABLED=true
NINJA_ENVIRONMENT=development

View File

@ -143,7 +143,7 @@ class SendTestEmails extends Command
$invoice->setRelation('invitations', $ii);
$invoice->service()->markSent()->save();
CreateInvoicePdf::dispatch($invoice, $company, $client->primary_contact()->first());
CreateInvoicePdf::dispatch($invoice->invitations()->first());
$cc_emails = [config('ninja.testvars.test_email')];
$bcc_emails = [config('ninja.testvars.test_email')];

View File

@ -173,7 +173,7 @@ class InvoiceFilters extends QueryFilters
if (auth('contact')->user()) {
return $this->contactViewFilter();
} else {
return $this->builder->company();
return $this->builder->company()->with(['invitations.company'],['documents.company']);
}
// return $this->builder->whereCompanyId(auth()->user()->company()->id);

View File

@ -77,7 +77,7 @@ class InvoiceEmail extends EmailBuilder
->setViewText(ctrans('texts.view_invoice'));
if ($client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments($invoice->pdf_file_path());
$this->setAttachments($invitation->pdf_file_path());
}
return $this;
}

View File

@ -61,7 +61,7 @@ class QuoteEmail extends EmailBuilder
->setBody($body_template);
if ($client->getSetting('pdf_email_attachment') !== false) {
$this->attachments = $quote->pdf_file_path();
$this->attachments = $invitation->pdf_file_path();
}
return $this;

View File

@ -19,6 +19,7 @@ use App\Http\Requests\Request;
use App\Jobs\Entity\ActionEntity;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\TempFile;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use ZipStream\Option\Archive;
@ -45,8 +46,11 @@ class InvoiceController extends Controller
*/
public function index(InvoiceFilters $filters)
{
$invoices = auth()->user()->client->company->invoices()->paginate(10);
$client_id = auth('contact')->user()->client->id;
$invoices = Invoice::where('client_id', $client_id)->paginate(10);
// $invoices = Invoice::filter($filters);
return $this->render('invoices.index', ['invoices' => $invoices]);
}
@ -154,10 +158,9 @@ class InvoiceController extends Controller
//if only 1 pdf, output to buffer for download
if ($invoices->count() == 1) {
return response()->download(public_path($invoices->first()->pdf_file_path()));
return response()->download(TempFile::path($invoices->first()->pdf_file_path()), basename($invoices->first()->pdf_file_path()));
}
# enable output of HTTP headers
$options = new Archive();
$options->setSendHttpHeaders(true);
@ -166,7 +169,7 @@ class InvoiceController extends Controller
$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()));
$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
}
# finish the zip stream

View File

@ -7,6 +7,7 @@ use App\Http\Requests\ClientPortal\ProcessQuotesInBulkRequest;
use App\Http\Requests\ClientPortal\ShowQuoteRequest;
use App\Models\Company;
use App\Models\Quote;
use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
@ -70,7 +71,7 @@ class QuoteController extends Controller
}
if ($quotes->count() == 1) {
return response()->download(public_path($quotes->first()->pdf_file_path()));
return response()->download(TempFile::path($invoices->first()->pdf_file_path()), basename($quotes->first()->pdf_file_path()));
}
# enable output of HTTP headers
@ -81,7 +82,7 @@ class QuoteController extends Controller
$zip = new ZipStream(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoices')) . ".zip", $options);
foreach ($quotes as $quote) {
$zip->addFileFromPath(basename($quote->pdf_file_path()), public_path($quote->pdf_file_path()));
$zip->addFileFromPath(basename($quote->pdf_file_path()), TempFile::path($quote->pdf_file_path()));
}
# finish the zip stream

View File

@ -22,6 +22,7 @@ use App\Models\Client;
use App\Models\Credit;
use App\Repositories\CreditRepository;
use App\Transformers\CreditTransformer;
use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
/**
@ -526,8 +527,8 @@ class CreditController extends BaseController
}
break;
case 'download':
return response()->download(public_path($credit->pdf_file_path()));
break;
return response()->download(TempFile::path($credit->pdf_file_path()), basename($credit->pdf_file_path()));
break;
case 'archive':
$this->credit_repository->archive($credit);

View File

@ -36,6 +36,7 @@ use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Repositories\InvoiceRepository;
use App\Transformers\InvoiceTransformer;
use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
@ -659,7 +660,7 @@ class InvoiceController extends BaseController
}
break;
case 'download':
return response()->download(public_path($invoice->pdf_file_path()));
return response()->download(TempFile::path($invoice->pdf_file_path()), basename($invoice->pdf_file_path()));
break;
case 'archive':
$this->invoice_repo->archive($invoice);

View File

@ -31,6 +31,7 @@ use App\Models\Quote;
use App\Repositories\QuoteRepository;
use App\Transformers\InvoiceTransformer;
use App\Transformers\QuoteTransformer;
use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
@ -657,7 +658,7 @@ class QuoteController extends BaseController
# code...
break;
case 'download':
return response()->download(public_path($quote->pdf_file_path()));
return response()->download(TempFile::path($quote->pdf_file_path()), basename($quote->pdf_file_path()));
break;
case 'archive':
$this->invoice_repo->archive($quote);

View File

@ -47,10 +47,11 @@ class QueryLogging
$count = count($queries);
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
// Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time);
Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time);
// if($count > 50)
// Log::info($queries);
if($count > 50)
Log::info($queries);
}
}

View File

@ -25,7 +25,6 @@ class CreateAccountRequest extends Request
public function authorize()
{
return true;
//return ! auth()->user();
}
/**

View File

@ -23,7 +23,8 @@ class ShowInvoiceRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->client->id === $this->invoice->client_id;
{info(auth('contact')->user()->client->id);
info($this->invoice->client_id);
return auth('contact')->user()->client->id === $this->invoice->client_id;
}
}

View File

@ -19,6 +19,7 @@ use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Design;
use App\Models\Invoice;
use App\Utils\HtmlEngine;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
@ -44,32 +45,30 @@ class CreateCreditPdf implements ShouldQueue
private $disk;
public $invitation;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($credit, Company $company, ClientContact $contact = null)
public function __construct($invitation)
{
$this->credit = $credit;
$this->invitation = $invitation;
$this->company = $company;
$this->credit = $invitation->credit;
$this->contact = $contact;
$this->company = $invitation->company;
$this->contact = $invitation->contact;
$this->disk = $disk ?? config('filesystems.default');
}
public function handle()
{
MultiDB::setDB($this->company->db);
$this->credit->load('client');
if (!$this->contact) {
$this->contact = $this->credit->client->primary_contact()->first();
}
App::setLocale($this->contact->preferredLocale());
$path = $this->credit->client->credit_filepath();
@ -80,11 +79,8 @@ class CreateCreditPdf implements ShouldQueue
$designer = new Designer($this->credit, $design, $this->credit->client->getSetting('pdf_variables'), 'credit');
//get invoice design
// $html = $this->generateInvoiceHtml($designer->build()->getHtml(), $this->credit, $this->contact);
$html = $this->generateEntityHtml($designer, $this->credit, $this->contact);
$html = (new HtmlEngine($designer, $invitation, 'credit'))->build();
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755);
$pdf = $this->makePdf(null, null, $html);

View File

@ -19,6 +19,7 @@ use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Design;
use App\Models\Invoice;
use App\Utils\HtmlEngine;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
@ -44,30 +45,28 @@ class CreateInvoicePdf implements ShouldQueue
private $disk;
public $invitation;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($invoice, Company $company, ClientContact $contact = null)
public function __construct($invitation)
{
$this->invoice = $invoice;
$this->invitation = $invitation;
$this->company = $company;
$this->invoice = $invitation->invoice;
$this->contact = $contact;
$this->company = $invitation->company;
$this->contact = $invitation->contact;
$this->disk = $disk ?? config('filesystems.default');
}
public function handle()
{
$this->invoice->load('client');
if (!$this->contact) {
$this->contact = $this->invoice->client->primary_contact()->first();
}
{info(print_r($this->invitation->contact,1));
App::setLocale($this->contact->preferredLocale());
$path = $this->invoice->client->invoice_filepath();
@ -78,8 +77,7 @@ class CreateInvoicePdf implements ShouldQueue
$designer = new Designer($this->invoice, $design, $this->invoice->client->getSetting('pdf_variables'), 'invoice');
//get invoice design
$html = $this->generateEntityHtml($designer, $this->invoice, $this->contact);
$html = (new HtmlEngine($designer, $this->invitation, 'invoice'))->build();
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755);

View File

@ -16,6 +16,7 @@ use App\Libraries\MultiDB;
use App\Mail\DownloadInvoices;
use App\Models\Company;
use App\Models\Invoice;
use App\Utils\TempFile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -72,7 +73,7 @@ class ZipInvoices implements ShouldQueue
$zip = new ZipStream($file_name, $options);
foreach ($this->invoices as $invoice) {
$zip->addFileFromPath(basename($invoice->pdf_file_path()), public_path($invoice->pdf_file_path()));
$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
}
$zip->finish();

View File

@ -19,6 +19,7 @@ use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Design;
use App\Models\Invoice;
use App\Utils\HtmlEngine;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
@ -44,34 +45,30 @@ class CreateQuotePdf implements ShouldQueue
private $disk;
public $invitation;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($quote, Company $company, ClientContact $contact = null)
public function __construct($invitation)
{
$this->quote = $quote;
$this->invitation = $invitation;
$this->company = $company;
$this->quote = $invitation->quote;
$this->contact = $contact;
$this->company = $invitation->company;
$this->contact = $invitation->contact;
$this->disk = $disk ?? config('filesystems.default');
}
public function handle()
{
MultiDB::setDB($this->company->db);
$settings = $this->quote->client->getMergedSettings();
$this->quote->load('client');
if (!$this->contact) {
$this->contact = $this->quote->client->primary_contact()->first();
}
App::setLocale($this->contact->preferredLocale());
$path = $this->quote->client->quote_filepath();
@ -82,20 +79,12 @@ class CreateQuotePdf implements ShouldQueue
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755);
$quote_number = $this->quote->number;
//$start = microtime(true);
$design_body = $designer->build()->getHtml();
$html = $this->generateEntityHtml($designer, $this->quote, $this->contact);
$html = (new HtmlEngine($designer, $this->invitation, 'quote'))->build();
$pdf = $this->makePdf(null, null, $html);
//\Log::error("PDF Build time = ". (microtime(true) - $start));
$file_path = $path . $quote_number . '.pdf';
$file_path = $path . $this->quote->number . '.pdf';
$instance = Storage::disk($this->disk)->put($file_path, $pdf);

View File

@ -47,7 +47,7 @@ class UploadFile implements ShouldQueue
public $entity;
public function __construct($file, $type, $user, $company, $entity, $disk = 'public')
public function __construct($file, $type, $user, $company, $entity, $disk = null)
{
$this->file = $file;
$this->type = $type;

View File

@ -34,8 +34,8 @@ class DeleteCompanyDocuments
// Remove all files & folders, under company's path.
// This will delete directory itself, as well.
// In case we want to remove the content of folder, we should use $fs->cleanDirectory();
$filesystem = new Filesystem();
$filesystem->deleteDirectory($path);
//$filesystem = new Filesystem();
Storage::deleteDirectory($event->company->company_key);
Document::whereCompanyId($event->company->id)->delete();
}

View File

@ -57,6 +57,7 @@ class TemplateEmail extends Mailable
'view_link' => $this->build_email->getViewLink(),
'view_text' => $this->build_email->getViewText(),
'title' => $this->build_email->getSubject(),
'signature' => $settings->email_signature,
'settings' => $settings,
'company' => $company
]);

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Events\Credit\CreditWasUpdated;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Credit\CreateCreditPdf;
@ -218,15 +219,18 @@ class Credit extends BaseModel
public function pdf_file_path($invitation = null)
{
$storage_path = 'storage/' . $this->client->credit_filepath() . $this->number . '.pdf';
if (Storage::exists($storage_path)) {
$storage_path = Storage::url($this->client->credit_filepath() . $this->number . '.pdf');
if (Storage::exists($this->client->credit_filepath() . $this->number . '.pdf')) {
return $storage_path;
}
if (!$invitation) {
event(new CreditWasUpdated($this, $this->company));
CreateCreditPdf::dispatchNow($this, $this->company, $this->client->primary_contact()->first());
} else {
event(new CreditWasUpdated($this, $this->company));
CreateCreditPdf::dispatchNow($invitation->credit, $invitation->company, $invitation->contact);
}

View File

@ -11,12 +11,15 @@
namespace App\Models;
use App\Events\Credit\CreditWasUpdated;
use App\Jobs\Credit\CreateCreditPdf;
use App\Models\Invoice;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
class CreditInvitation extends BaseModel
{
@ -112,4 +115,16 @@ class CreditInvitation extends BaseModel
$this->viewed_date = Carbon::now();
$this->save();
}
public function pdf_file_path()
{
$storage_path = Storage::url($this->credit->client->quote_filepath() . $this->credit->number . '.pdf');
if (!Storage::exists($this->credit->client->credit_filepath() . $this->credit->number . '.pdf')) {
event(new CreditWasUpdated($this, $this->company));
CreateCreditPdf::dispatchNow($this);
}
return $storage_path;
}
}

View File

@ -195,6 +195,16 @@ class Invoice extends BaseModel
return $this->morphMany(CompanyLedger::class, 'company_ledgerable');
}
public function activities()
{
return $this->hasMany(Activity::class);
}
public function history()
{
$this->activities->with('backup');
}
// public function credits()
// {
// return $this->belongsToMany(Credit::class)->using(Paymentable::class)->withPivot(
@ -356,21 +366,6 @@ class Invoice extends BaseModel
}
}
/**
* Returns the template for the invoice
*
* @return string Either the template view, OR the template HTML string
* @todo this needs attention, invoice->settings needs clarification
*/
public function design(): string
{
if ($this->client->getSetting('design')) {
return File::exists(resource_path($this->client->getSetting('design'))) ? File::get(resource_path($this->client->getSetting('design'))) : File::get(resource_path('views/pdf/design1.blade.php'));
} else {
return File::get(resource_path('views/pdf/design1.blade.php'));
}
}
/**
* Access the invoice calculator object
*
@ -389,34 +384,16 @@ class Invoice extends BaseModel
return $invoice_calc->build();
}
/** TODO// DOCUMENT THIS FUNCTIONALITY */
public function pdf_url()
public function pdf_file_path($invitation = null)
{
// $public_path = 'storage/' . $this->client->invoice_filepath() . $this->number . '.pdf';
if(!$invitation)
$invitation = $this->invitations->first();
// $storage_path = 'public/' . $this->client->invoice_filepath() . $this->number . '.pdf';
$storage_path = Storage::url($this->client->invoice_filepath() . $this->number . '.pdf');
$public_path = $this->client->invoice_filepath() . $this->number . '.pdf';
$storage_path = 'storage/' . $this->client->invoice_filepath() . $this->number . '.pdf';
$disk = config('filesystems.default');
if (!Storage::disk($disk)->exists($public_path)) {
if (!Storage::exists($this->client->invoice_filepath() . $this->number . '.pdf')) {
event(new InvoiceWasUpdated($this, $this->company));
CreateInvoicePdf::dispatch($this, $this->company, $this->client->primary_contact()->first());
}
return $storage_path;
}
public function pdf_file_path()
{
$storage_path = 'storage/' . $this->client->invoice_filepath() . $this->number . '.pdf';
if (!Storage::exists($storage_path)) {
CreateInvoicePdf::dispatchNow($this, $this->company, $this->client->primary_contact()->first());
CreateInvoicePdf::dispatchNow($invitation);
}
return $storage_path;

View File

@ -11,12 +11,14 @@
namespace App\Models;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Jobs\Invoice\CreateInvoicePdf;
use App\Models\Invoice;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
class InvoiceInvitation extends BaseModel
{
@ -122,4 +124,17 @@ class InvoiceInvitation extends BaseModel
$this->viewed_date = Carbon::now();
$this->save();
}
public function pdf_file_path()
{
$storage_path = Storage::url($this->invoice->client->invoice_filepath() . $this->invoice->number . '.pdf');
if (!Storage::exists($this->invoice->client->invoice_filepath() . $this->invoice->number . '.pdf')) {
event(new InvoiceWasUpdated($this->invoice, $this->company));
CreateInvoicePdf::dispatchNow($this);
}
return $storage_path;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Events\Quote\QuoteWasUpdated;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Invoice\CreateInvoicePdf;
@ -177,17 +178,18 @@ class Quote extends BaseModel
public function pdf_file_path($invitation = null)
{
$storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf';
if(!$invitation)
$invitation = $this->invitations->where('client_contact_id', $this->client->primary_contact()->first()->id)->first();
if (Storage::exists($storage_path)) {
$storage_path = Storage::url($this->client->quote_filepath() . $this->number . '.pdf');
if (Storage::exists($this->client->quote_filepath() . $this->number . '.pdf')) {
return $storage_path;
}
if (!$invitation) {
CreateQuotePdf::dispatchNow($this, $this->company, $this->client->primary_contact()->first());
} else {
CreateQuotePdf::dispatchNow($invitation->quote, $invitation->company, $invitation->contact);
}
event(new QuoteWasUpdated($this, $this->company));
CreateQuotePdf::dispatchNow($invitation);
return $storage_path;
}

View File

@ -11,11 +11,14 @@
namespace App\Models;
use App\Events\Quote\QuoteWasUpdated;
use App\Jobs\Quote\CreateQuotePdf;
use App\Models\Quote;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
class QuoteInvitation extends BaseModel
{
@ -110,4 +113,16 @@ class QuoteInvitation extends BaseModel
$this->viewed_date = Carbon::now();
$this->save();
}
public function pdf_file_path()
{
$storage_path = Storage::url($this->quote->client->quote_filepath() . $this->quote->number . '.pdf');
if (!Storage::exists($this->quote->client->quote_filepath() . $this->quote->number . '.pdf')) {
event(new QuoteWasUpdated($this, $this->company));
CreateQuotePdf::dispatchNow($this);
}
return $storage_path;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Notifications;
use App\Utils\TempFile;
use App\Utils\Traits\MakesInvoiceHtml;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -74,11 +75,11 @@ class BaseNotification extends Notification implements ShouldQueue
}
if ($this->settings->pdf_email_attachment) {
$mail_message->attach(public_path($this->entity->pdf_file_path()));
$mail_message->attach(TempFile::path($this->invitation->pdf_file_path()), ['as' => basename($this->invitation->pdf_file_path())]);
}
foreach ($this->entity->documents as $document) {
$mail_message->attach($document->generateUrl(), ['as' => $document->name]);
$mail_message->attach(TempFile::path($document->generateUrl()), ['as' => $document->name]);
}
if ($this->entity instanceof Invoice && $this->settings->ubl_email_attachment) {
@ -98,7 +99,6 @@ class BaseNotification extends Notification implements ShouldQueue
if ($design_style == 'custom') {
$email_style_custom = $this->settings->email_style_custom;
//$body = str_replace("$body", $body, $email_style_custom);
$body = strtr($email_style_custom, "$body", $body);
}
@ -109,11 +109,13 @@ class BaseNotification extends Notification implements ShouldQueue
'title' => '',
'settings' => '',
'company' => '',
'view_link' => $this->invitation->getLink(),
'view_text' => ctrans('texts.view_'.$this->entity_string),
'logo' => $this->entity->company->present()->logo(),
'signature' => '',
'signature' => $this->settings->email_signature,
];
return $data;
}
}
}

View File

@ -50,6 +50,8 @@ class SendGenericNotification extends BaseNotification implements ShouldQueue
$this->settings = $this->entity->client->getMergedSettings();
$this->subject = $subject;
$this->body = $body;
$this->invitation = $invitation;
$this->entity_string = $entity_string;
}
/**

View File

@ -32,6 +32,8 @@ class GetInvoicePdf extends AbstractService
$this->contact = $this->invoice->client->primary_contact()->first();
}
$invitation = $this->invoice->invitations->where('client_contact_id', $this->contact->id)->first();
$path = $this->invoice->client->invoice_filepath();
$file_path = $path . $this->invoice->number . '.pdf';
@ -41,11 +43,9 @@ class GetInvoicePdf extends AbstractService
$file = Storage::disk($disk)->exists($file_path);
if (!$file) {
$file_path = CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->contact);
$file_path = CreateInvoicePdf::dispatchNow($invitation);
}
//return $file_path;
return Storage::disk($disk)->path($file_path);
}
}

View File

@ -105,9 +105,7 @@ class InvoiceService
public function getInvoicePdf($contact)
{
$get_invoice_pdf = new GetInvoicePdf($this->invoice, $contact);
return $get_invoice_pdf->run();
return (new GetInvoicePdf($this->invoice, $contact))->run();
}
public function sendEmail($contact)

View File

@ -22,7 +22,7 @@ class MarkSent
/* Return immediately if status is not draft */
if ($this->quote->status_id != Quote::STATUS_DRAFT) {
return $quote;
return $this->quote;
}
$this->quote->markInvitationsSent();

603
app/Utils/HtmlEngine.php Normal file
View File

@ -0,0 +1,603 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils;
use App\Designs\Designer;
use App\Models\Country;
use App\Utils\Number;
use Illuminate\Support\Facades\App;
class HtmlEngine
{
public $entity;
public $invitation;
public $client;
public $contact;
public $company;
public $designer;
public $settings;
public $entity_calc;
public $entity_string;
public function __construct(Designer $designer, $invitation, $entity_string)
{
$this->designer = $designer;
$this->invitation = $invitation;
$this->entity = $invitation->{$entity_string};
$this->company = $invitation->company;
$this->contact = $invitation->contact;
$this->client = $this->entity->client;
$this->settings = $this->client->getMergedSettings();
$this->entity_calc = $this->entity->calc();
$this->entity_string = $entity_string;
}
public function build() :string
{
App::setLocale($this->client->preferredLocale());
$values_and_labels = $this->generateLabelsAndValues();
$this->designer->build();
$data = [];
$data['entity'] = $this->entity;
$data['lang'] = $this->client->preferredLocale();
$data['includes'] = $this->designer->getIncludes();
$data['header'] = $this->designer->getHeader();
$data['body'] = $this->designer->getBody();
$data['footer'] = $this->designer->getFooter();
$html = view('pdf.stub', $data)->render();
$html = $this->parseLabelsAndValues($values_and_labels['labels'], $values_and_labels['values'], $html);
return $html;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function buildEntityDataArray() :array
{
if (!$this->client->currency()) {
throw new \Exception(debug_backtrace()[1]['function'], 1);
exit;
}
$data = [];
$data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$app_url'] = ['value' => $this->generateAppUrl(), 'label' => ''];
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
$data['$total_tax_labels'] = ['value' => $this->totalTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->entity->date ?: '&nbsp;', 'label' => ctrans('texts.date')];
//$data['$invoice_date'] = ['value' => $this->date ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
$data['$invoice.date'] = &$data['$date'];
$data['$invoice.due_date'] = ['value' => $this->entity->due_date ?: '&nbsp;', 'label' => ctrans('texts.due_date')];
$data['$due_date'] = &$data['$invoice.due_date'];
$data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')];
$data['$line_taxes'] = ['value' => $this->makeLineTaxes() ?: '&nbsp;', 'label' => ctrans('texts.taxes')];
$data['$invoice.line_taxes'] = &$data['$line_taxes'];
$data['$total_taxes'] = ['value' => $this->makeTotalTaxes() ?: '&nbsp;', 'label' => ctrans('texts.taxes')];
$data['$invoice.total_taxes'] = &$data['$total_taxes'];
if ($this->entity_string == 'invoice') {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.invoice')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
if ($this->entity_string == 'quote') {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.quote')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.quote_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
if ($this->entity_string == 'credit') {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.credit_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
$data['$entity_number'] = &$data['$number'];
//$data['$paid_to_date'] = ;
$data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.discount')];
$data['$discount'] = &$data['$invoice.discount'];
$data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
$data['$invoice.subtotal'] = &$data['$subtotal'];
$data['$invoice.balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$quote.balance_due'] = &$data['$invoice.balance_due'];
$data['$balance_due'] = &$data['$invoice.balance_due'];
$data['$invoice.partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.total')];
$data['$amount'] = &$data['$total'];
$data['$quote.total'] = &$data['$total'];
$data['$invoice.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.invoice_total')];
$data['$invoice.amount'] = &$data['$total'];
$data['$quote.amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.quote_total')];
$data['$credit.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_total')];
$data['$credit.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$credit.amount'] = &$data['$credit.total'];
$data['$credit.po_number'] = &$data['$invoice.po_number'];
$data['$credit.date'] = ['value' => $this->entity->date, 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = &$data['$balance'];
$data['$invoice.balance'] = &$data['$balance'];
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.taxes')];
$data['$invoice.taxes'] = &$data['$taxes'];
$data['$invoice.custom1'] = ['value' => $this->entity->custom_value1 ?: '&nbsp;', 'label' => $this->makeCustomField('invoice1')];
$data['$invoice.custom2'] = ['value' => $this->entity->custom_value2 ?: '&nbsp;', 'label' => $this->makeCustomField('invoice2')];
$data['$invoice.custom3'] = ['value' => $this->entity->custom_value3 ?: '&nbsp;', 'label' => $this->makeCustomField('invoice3')];
$data['$invoice.custom4'] = ['value' => $this->entity->custom_value4 ?: '&nbsp;', 'label' => $this->makeCustomField('invoice4')];
$data['$invoice.public_notes'] = ['value' => $this->entity->public_notes ?: '&nbsp;', 'label' => ctrans('texts.public_notes')];
$data['$entity.public_notes'] = &$data['$invoice.public_notes'];
// $data['$your_invoice'] = ;
// $data['$quote'] = ;
// $data['$your_quote'] = ;
//
$data['$quote.date'] = ['value' => $this->entity->date ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$quote.po_number'] = &$data['$invoice.po_number'];
$data['$quote.quote_number'] = &$data['$quote.number'];
$data['$quote_no'] = &$data['$quote.number'];
$data['$quote.quote_no'] = &$data['$quote.number'];
$data['$quote.valid_until'] = ['value' => $this->entity->due_date, 'label' => ctrans('texts.valid_until')];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
;
$data['$credit_number'] = &$data['$number'];
$data['$credit_no'] = &$data['$number'];
$data['$credit.credit_no'] = &$data['$number'];
// $data['$invoice_issued_to'] = ;
// $data['$quote_issued_to'] = ;
// $data['$rate'] = ;
// $data['$hours'] = ;
// $data['$from'] = ;
// $data['$to'] = ;
// $data['$invoice_to'] = ;
// $data['$quote_to'] = ;
// $data['$details'] = ;
$data['$invoice_no'] = &$data['$number'];
$data['$invoice.invoice_no'] = &$data['$number'];
$data['$client1'] = ['value' => $this->client->custom_value1 ?: '&nbsp;', 'label' => $this->makeCustomField('client1')];
$data['$client2'] = ['value' => $this->client->custom_value2 ?: '&nbsp;', 'label' => $this->makeCustomField('client2')];
$data['$client3'] = ['value' => $this->client->custom_value3 ?: '&nbsp;', 'label' => $this->makeCustomField('client3')];
$data['$client4'] = ['value' => $this->client->custom_value4 ?: '&nbsp;', 'label' => $this->makeCustomField('client4')];
$data['$address1'] = ['value' => $this->client->address1 ?: '&nbsp;', 'label' => ctrans('texts.address1')];
$data['$address2'] = ['value' => $this->client->address2 ?: '&nbsp;', 'label' => ctrans('texts.address2')];
$data['$id_number'] = ['value' => $this->client->id_number ?: '&nbsp;', 'label' => ctrans('texts.id_number')];
$data['$vat_number'] = ['value' => $this->client->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$website'] = ['value' => $this->client->present()->website() ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$phone'] = ['value' => $this->client->present()->phone() ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$country'] = ['value' => isset($this->client->country->name) ? $this->client->country->name : 'No Country Set', 'label' => ctrans('texts.country')];
$data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')];
$data['$client_name'] = ['value' => $this->entity->present()->clientName() ?: '&nbsp;', 'label' => ctrans('texts.client_name')];
$data['$client.name'] = &$data['$client_name'];
$data['$client.address1'] = &$data['$address1'];
$data['$client.address2'] = &$data['$address2'];
$data['$client_address'] = ['value' => $this->entity->present()->address() ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$client.address'] = &$data['$client_address'];
$data['$client.id_number'] = &$data['$id_number'];
$data['$client.vat_number'] = &$data['$vat_number'];
$data['$client.website'] = &$data['$website'];
$data['$client.phone'] = &$data['$phone'];
$data['$city_state_postal'] = ['value' => $this->entity->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$client.city_state_postal'] = &$data['$city_state_postal'];
$data['$postal_city_state'] = ['value' => $this->entity->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
$data['$client.postal_city_state'] = &$data['$postal_city_state'];
$data['$client.country'] = &$data['$country'];
$data['$client.email'] = &$data['$email'];
$data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')];
$data['$contact.email'] = ['value' => $this->contact->email, 'label' => ctrans('texts.email')];
$data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')];
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : 'no contact name on record', 'label' => ctrans('texts.contact_name')];
$data['$contact.custom1'] = ['value' => isset($this->contact) ? $this->contact->custom_value1 : '&nbsp;', 'label' => $this->makeCustomField('contact1')];
$data['$contact.custom2'] = ['value' => isset($this->contact) ? $this->contact->custom_value2 : '&nbsp;', 'label' => $this->makeCustomField('contact1')];
$data['$contact.custom3'] = ['value' => isset($this->contact) ? $this->contact->custom_value3 : '&nbsp;', 'label' => $this->makeCustomField('contact1')];
$data['$contact.custom4'] = ['value' => isset($this->contact) ? $this->contact->custom_value4 : '&nbsp;', 'label' => $this->makeCustomField('contact1')];
$data['$company.city_state_postal'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$company.postal_city_state'] = ['value' => $this->company->present()->cityStateZip($this->settings->city, $this->settings->state, $this->settings->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
$data['$company.name'] = ['value' => $this->company->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.company_name')];
$data['$company.address1'] = ['value' => $this->settings->address1 ?: '&nbsp;', 'label' => ctrans('texts.address1')];
$data['$company.address2'] = ['value' => $this->settings->address2 ?: '&nbsp;', 'label' => ctrans('texts.address2')];
$data['$company.city'] = ['value' => $this->settings->city ?: '&nbsp;', 'label' => ctrans('texts.city')];
$data['$company.state'] = ['value' => $this->settings->state ?: '&nbsp;', 'label' => ctrans('texts.state')];
$data['$company.postal_code'] = ['value' => $this->settings->postal_code ?: '&nbsp;', 'label' => ctrans('texts.postal_code')];
$data['$company.country'] = ['value' => $this->getCountryName(), 'label' => ctrans('texts.country')];
$data['$company.phone'] = ['value' => $this->settings->phone ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$company.email'] = ['value' => $this->settings->email ?: '&nbsp;', 'label' => ctrans('texts.email')];
$data['$company.vat_number'] = ['value' => $this->settings->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$company.id_number'] = ['value' => $this->settings->id_number ?: '&nbsp;', 'label' => ctrans('texts.id_number')];
$data['$company.website'] = ['value' => $this->settings->website ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$company.address'] = ['value' => $this->company->present()->address($this->settings) ?: '&nbsp;', 'label' => ctrans('texts.address')];
$logo = $this->company->present()->logo($this->settings);
$data['$company.logo'] = ['value' => "<img src='{$logo}' class='h-32' alt='logo'>" ?: '&nbsp;', 'label' => ctrans('texts.logo')];
$data['$company_logo'] = &$data['$company.logo'];
$data['$company1'] = ['value' => $this->settings->custom_value1 ?: '&nbsp;', 'label' => $this->makeCustomField('company1')];
$data['$company2'] = ['value' => $this->settings->custom_value2 ?: '&nbsp;', 'label' => $this->makeCustomField('company2')];
$data['$company3'] = ['value' => $this->settings->custom_value3 ?: '&nbsp;', 'label' => $this->makeCustomField('company3')];
$data['$company4'] = ['value' => $this->settings->custom_value4 ?: '&nbsp;', 'label' => $this->makeCustomField('company4')];
$data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')];
$data['$product.discount'] = ['value' => '', 'label' => ctrans('texts.discount')];
$data['$product.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')];
$data['$product.notes'] = ['value' => '', 'label' => ctrans('texts.notes')];
$data['$product.cost'] = ['value' => '', 'label' => ctrans('texts.cost')];
$data['$product.quantity'] = ['value' => '', 'label' => ctrans('texts.quantity')];
$data['$product.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$product.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')];
$data['$task.date'] = ['value' => '', 'label' => ctrans('texts.date')];
$data['$task.discount'] = ['value' => '', 'label' => ctrans('texts.discount')];
$data['$task.product_key'] = ['value' => '', 'label' => ctrans('texts.product_key')];
$data['$task.notes'] = ['value' => '', 'label' => ctrans('texts.notes')];
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.cost')];
$data['$task.quantity'] = ['value' => '', 'label' => ctrans('texts.quantity')];
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name2'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$task.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')];
//$data['$contact.signature']
// $data['custom_label1'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label2'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label3'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label4'] = ['value' => '', 'label' => ctrans('texts.')];
//$data['$blank'] = ;
//$data['$surcharge'] = ;
/*
$data['$tax_invoice'] =
$data['$tax_quote'] =
$data['$statement'] = ;
$data['$statement_date'] = ;
$data['$your_statement'] = ;
$data['$statement_issued_to'] = ;
$data['$statement_to'] = ;
$data['$credit_note'] = ;
$data['$credit_date'] = ;
$data['$credit_issued_to'] = ;
$data['$credit_to'] = ;
$data['$your_credit'] = ;
$data['$phone'] = ;
$data['$outstanding'] = ;
$data['$invoice_due_date'] = ;
$data['$quote_due_date'] = ;
$data['$service'] = ;
$data['$product_key'] = ;
$data['$unit_cost'] = ;
$data['$custom_value1'] = ;
$data['$custom_value2'] = ;
$data['$delivery_note'] = ;
$data['$date'] = ;
$data['$method'] = ;
$data['$payment_date'] = ;
$data['$reference'] = ;
$data['$amount'] = ;
$data['$amount_paid'] =;
*/
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);
return $data;
}
private function generateLabelsAndValues()
{
$data = [];
$values = $this->buildEntityDataArray();
foreach ($values as $key => $value) {
$data['values'][$key] = $value['value'];
$data['labels'][$key.'_label'] = $value['label'];
}
return $data;
}
private function totalTaxLabels() :string
{
$data = '';
if (!$this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<span>'. $tax['name'] .'</span>';
}
return $data;
}
private function totalTaxValues() :string
{
$data = '';
if (!$this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<span>'. Number::formatMoney($tax['total'], $this->client) .'</span>';
}
return $data;
}
private function lineTaxLabels() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<span>'. $tax['name'] .'</span>';
}
return $data;
}
private function getCountryName() :string
{
$country = Country::find($this->settings->country_id)->first();
if($country)
return $country->name;
return '&nbsp;';
}
/**
* Due to the way we are compiling the blade template we
* have no ability to iterate, so in the case
* of line taxes where there are multiple rows,
* we use this function to format a section of rows
*
* @return string a collection of <tr> rows with line item
* aggregate data
*/
private function makeLineTaxes() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<tr class="line_taxes">';
$data .= '<td>'. $tax['name'] .'</td>';
$data .= '<td>'. Number::formatMoney($tax['total'], $this->client) .'</td></tr>';
}
return $data;
}
private function lineTaxValues() :string
{
$tax_map = $this->entity_calc->getTaxMap();
$data = '';
foreach ($tax_map as $tax) {
$data .= '<span>'. Number::formatMoney($tax['total'], $this->client) .'</span>';
}
return $data;
}
private function makeCustomField($field) :string
{
$custom_fields = $this->company->custom_fields;
if ($custom_fields && property_exists($custom_fields, $field)) {
$custom_field = $custom_fields->{$field};
$custom_field_parts = explode("|", $custom_field);
return $custom_field_parts[0];
}
return '';
}
private function makeTotalTaxes() :string
{
$data = '';
if (!$this->entity_calc->getTotalTaxMap()) {
return $data;
}
foreach ($this->entity_calc->getTotalTaxMap() as $tax) {
$data .= '<tr class="total_taxes">';
$data .= '<td>'. $tax['name'] .'</td>';
$data .= '<td>'. Number::formatMoney($tax['total'], $this->client) .'</td></tr>';
}
return $data;
}
private function parseLabelsAndValues($labels, $values, $section) :string
{
$section = strtr($section, $labels);
return strtr($section, $values);
}
/*
| Ensures the URL doesn't have duplicated trailing slash
*/
public function generateAppUrl()
{
return rtrim(config('ninja.app_url'), "/");
}
/**
* Builds CSS to assist with the generation
* of Repeating headers and footers on the PDF
* @return string The css string
*/
private function generateCustomCSS() :string
{
$header_and_footer = '
.header, .header-space {
height: 160px;
}
.footer, .footer-space {
height: 160px;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
}
.header {
position: fixed;
top: 0mm;
width: 100%;
}
@media print {
thead {display: table-header-group;}
tfoot {display: table-footer-group;}
button {display: none;}
body {margin: 0;}
}';
$header = '
.header, .header-space {
height: 160px;
}
.header {
position: fixed;
top: 0mm;
width: 100%;
}
@media print {
thead {display: table-header-group;}
button {display: none;}
body {margin: 0;}
}';
$footer = '
.footer, .footer-space {
height: 160px;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
}
@media print {
tfoot {display: table-footer-group;}
button {display: none;}
body {margin: 0;}
}';
$css = '';
if ($this->settings->all_pages_header && $this->settings->all_pages_footer) {
$css .= $header_and_footer;
} elseif ($this->settings->all_pages_header && !$this->settings->all_pages_footer) {
$css .= $header;
} elseif (!$this->settings->all_pages_header && $this->settings->all_pages_footer) {
$css .= $footer;
}
$css .= '
.page {
page-break-after: always;
}
@page {
margin: 0mm
}
html {
';
$css .= 'font-size:' . $this->settings->font_size . 'px;';
// $css .= 'font-size:14px;';
$css .= '}';
return $css;
}
}

26
app/Utils/TempFile.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils;
class TempFile
{
public static function path($url) :string
{
$temp_path = tempnam(sys_get_temp_dir(), basename($url));
copy($url, $temp_path);
return $temp_path;
}
}

View File

@ -102,7 +102,7 @@ class TemplateEngine
private function replaceValues()
{
if ($this->entity_obj) {
$this->entityValues();
$this->entityValues($this->entity_obj->client->primary_contact()->first());
} else {
$this->fakerValues();
}
@ -115,11 +115,6 @@ class TemplateEngine
$labels = $this->makeFakerLabels();
$values = $this->makeFakerValues();
// $this->body = str_replace(array_keys($labels), array_values($labels), $this->body);
// $this->body = str_replace(array_keys($values), array_values($values), $this->body);
// $this->subject = str_replace(array_keys($labels), array_values($labels), $this->subject);
// $this->subject = str_replace(array_keys($values), array_values($values), $this->subject);
$this->body = strtr($this->body, $labels);
$this->body = strtr($this->body, $values);
@ -133,16 +128,16 @@ class TemplateEngine
$this->body = $converter->convertToHtml($this->body);
}
private function entityValues()
private function entityValues($contact)
{
$labels = $this->entity_obj->makeLabels();
$values = $this->entity_obj->makeValues();
$this->body = strtr($this->body, $labels);
$this->body = strtr($this->body, $values);
$data = $this->entity_obj->buildLabelsAndValues($contact);
$this->subject = strtr($this->subject, $labels);
$this->subject = strtr($this->subject, $values);
$this->body = strtr($this->body, $data['labels']);
$this->body = strtr($this->body, $data['values']);
$this->subject = strtr($this->subject, $data['labels']);
$this->subject = strtr($this->subject, $data['values']);
$converter = new CommonMarkConverter([
'allow_unsafe_links' => false,

View File

@ -42,8 +42,7 @@ trait MakesInvoiceHtml
App::setLocale($client->preferredLocale());
$labels = $entity->makeLabels();
$values = $entity->makeValues($contact);
$values_and_labels = $entity->buildLabelsAndValues($contact);
$designer->build();
@ -57,10 +56,8 @@ trait MakesInvoiceHtml
$html = view('pdf.stub', $data)->render();
$html = $this->parseLabelsAndValues($labels, $values, $html);
// info($html);
$html = $this->parseLabelsAndValues($values_and_labels['labels'], $values_and_labels['values'], $html);
return $html;
}
@ -72,17 +69,13 @@ trait MakesInvoiceHtml
App::setLocale($client->preferredLocale());
$labels = $entity->makeLabels();
$values = $entity->makeValues($contact);
$data = $entity->buildLabelsAndValues($contact);
return $this->parseLabelsAndValues($labels, $values, $content);
return $this->parseLabelsAndValues($data['labels'], $data['values'], $content);
}
private function parseLabelsAndValues($labels, $values, $section) :string
{
// $section = str_replace(array_keys($labels), array_values($labels), $section);
// $section = str_replace(array_keys($values), array_values($values), $section);
$section = strtr($section, $labels);
$section = strtr($section, $values);

View File

@ -159,6 +159,20 @@ trait MakesInvoiceValues
return $data;
}
public function buildLabelsAndValues($contact)
{
$data = [];
$values = $this->makeLabelsAndValues($contact);
foreach ($values as $key => $value) {
$data['values'][$key] = $value['value'];
$data['labels'][$key.'_label'] = $value['label'];
}
return $data;
}
private function makeLabelsAndValues($contact = null) :array
{
if (!$this->client->currency() || !$this->client) {
@ -432,7 +446,7 @@ trait MakesInvoiceValues
$table_header .= '</tr>';
$table_header = str_replace(array_keys($data), array_values($data), $table_header);
$table_header = strtr($table_header, $data);// str_replace(array_keys($data), array_values($data), $table_header);
return $table_header;
}

View File

@ -37,6 +37,8 @@
"laravel/slack-notification-channel": "^2.0",
"laravel/socialite": "^4.0",
"laravel/tinker": "^1.0",
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-cached-adapter": "~1.0",
"league/fractal": "^0.17.0",
"league/omnipay": "^3.0",
"maennchen/zipstream-php": "^1.2",

View File

@ -61,7 +61,9 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'endpoint' => env('AWS_ENDPOINT'),
'url' => env('AWS_URL'),
'visibility' => 'public',
],
'gcs' => [
'driver' => 'gcs',

View File

@ -3197,4 +3197,5 @@ return [
'open_in_new_tab' => 'Open in new tab',
'complete_your_payment' => 'Complete payment',
'authorize_for_future_use' => 'Authorize payment method for future use',
'view_credit' => 'View Credit',
];

View File

@ -9,9 +9,15 @@
@lang($body)
@endslot
@slot('signature')
@if(isset($view_link))
@component('email.components.button', ['url' => $view_link])
{{ $view_text }}
@endcomponent
@endif
@if(isset($signature))
{{ $signature }}
@endslot
@endif
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])

View File

@ -9,6 +9,11 @@
@endcomponent
@if(isset($view_link))
@component('email.components.button', ['url' => $view_link])
{{$view_text}}
@endcomponent
@endif
@if($footer)
@component('email.components.button', ['url' => $view_link])

View File

@ -32,7 +32,7 @@
<div class="row mt-4">
<div class="col-md-12">
<embed src="{{ asset($invoice->pdf_url()) }}#toolbar=1&navpanes=1&scrollbar=1" type="application/pdf" width="100%" height="1180px" />
<embed src="{{ $invoice->pdf_file_path() }}#toolbar=1&navpanes=1&scrollbar=1" type="application/pdf" width="100%" height="1180px" />
<div id="pdfCanvas" style="display:none;width:100%;background-color:#525659;border:solid 2px #9a9a9a;padding-top:40px;text-align:center">
<canvas id="theCanvas" style="max-width:100%;border:solid 1px #CCCCCC;"></canvas>

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.view_invoice'))
@push('head')
<meta name="pdf-url" content="{{ asset($invoice->pdf_url()) }}">
<meta name="pdf-url" content="{{ $invoice->pdf_file_path() }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
@endpush
@ -84,7 +84,7 @@
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="{{ asset($invoice->pdf_url()) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
<a target="_blank" href="{{ $invoice->pdf_file_path() }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
</div>

View File

@ -75,7 +75,7 @@
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="{{ asset($invoice->pdf_url()) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
<a target="_blank" href="{{ $invoice->pdf_file_path() }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
</div>

View File

@ -51,9 +51,7 @@ Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', '
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
/*Invitation catches*/
Route::get('invoice/{invitation_key}', 'ClientPortal\InvitationController@router')->name('invoice.invitation_key');
Route::get('quote/{invitation_key}', 'ClientPortal\InvitationController@router')->name('quote.invitation_key');
Route::get('credit/{invitation_key}', 'ClientPortal\InvitationController@router')->name('credit.invitation_key');
Route::get('{entity}/{invitation_key}', 'ClientPortal\InvitationController@router');
Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf')->name('invoice.download_invitation_key');
Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf')->name('quote.download_invitation_key');
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');

View File

@ -65,6 +65,10 @@ class DesignTest extends TestCase
$this->invoice->uses_inclusive_taxes = false;
$this->invoice->service()->createInvitations()->markSent()->save();
$this->invoice->fresh();
$this->invoice->load('invitations');
$settings = $this->invoice->client->settings;
$settings->invoice_design_id = "VolejRejNm";
$settings->all_pages_header = true;
@ -73,7 +77,7 @@ class DesignTest extends TestCase
$this->client->settings = $settings;
$this->client->save();
CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->invoice->client->primary_contact()->first());
CreateInvoicePdf::dispatchNow($this->invoice->invitations->first());
}
public function testQuoteDesignExists()
@ -97,6 +101,10 @@ class DesignTest extends TestCase
$this->quote->uses_inclusive_taxes = false;
$this->quote->service()->createInvitations()->markSent()->save();
$this->quote->fresh();
$this->quote->load('invitations');
$settings = $this->quote->client->settings;
$settings->invoice_design_id = "VolejRejNm";
$settings->all_pages_header = true;
@ -105,7 +113,12 @@ class DesignTest extends TestCase
$this->client->settings = $settings;
$this->client->save();
CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
$this->quote->setRelation('client', $this->client);
$invitation = $this->quote->invitations->first();
$invitation->setRelation('quote', $this->quote);
CreateQuotePdf::dispatchNow($invitation);
}
@ -126,12 +139,17 @@ class DesignTest extends TestCase
$this->credit->client_id = $this->client->id;
$this->credit->setRelation('client', $this->client);
$this->credit->save();
$this->credit->service()->createInvitations()->markSent()->save();
$this->credit->fresh();
$this->credit->load('invitations');
$invitation = $this->credit->invitations->first();
$invitation->setRelation('credit', $this->credit);
$this->client->settings = $settings;
$this->client->save();
CreateCreditPdf::dispatchNow($this->credit, $this->credit->company, $this->credit->client->primary_contact()->first());
CreateCreditPdf::dispatchNow($invitation);
}
public function testAllDesigns()
@ -149,7 +167,14 @@ class DesignTest extends TestCase
$this->client->settings = $settings;
$this->client->save();
CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
$this->quote->service()->createInvitations()->markSent()->save();
$this->quote->fresh();
$this->quote->load('invitations');
$invitation = $this->quote->invitations->first();
$invitation->setRelation('quote', $this->quote);
CreateQuotePdf::dispatchNow($invitation);
$this->quote->number = $this->getNextQuoteNumber($this->quote->client);
$this->quote->save();
@ -158,117 +183,4 @@ class DesignTest extends TestCase
$this->assertTrue(true);
}
///////////////////////////////////////////////////////////////
// public function testQuoteDesignWithRepeatingHeader()
// {
// $modern = new Modern();
// $designer = new Designer($this->quote, $modern, $this->company->settings->pdf_variables, 'quote');
// $html = $designer->build()->getHtml();
// $this->assertNotNull($html);
// //\Log::error($html);
// $settings = $this->invoice->client->settings;
// $settings->quote_design_id = "4";
// $settings->all_pages_header = true;
// $this->quote->client_id = $this->client->id;
// $this->quote->setRelation('client', $this->client);
// $this->quote->save();
// $this->client->settings = $settings;
// $this->client->save();
// CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
// }
// public function testQuoteDesignWithRepeatingFooter()
// {
// $modern = new Modern();
// $designer = new Designer($this->quote, $modern, $this->company->settings->pdf_variables, 'quote');
// $html = $designer->build()->getHtml();
// $this->assertNotNull($html);
// //\Log::error($html);
// $settings = $this->invoice->client->settings;
// $settings->quote_design_id = "4";
// $settings->all_pages_footer = true;
// $this->quote->client_id = $this->client->id;
// $this->quote->setRelation('client', $this->client);
// $this->quote->save();
// $this->client->settings = $settings;
// $this->client->save();
// CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
// }
// public function testQuoteDesignWithRepeatingHeaderAndFooter()
// {
// $modern = new Modern();
// $designer = new Designer($this->quote, $modern, $this->company->settings->pdf_variables, 'quote');
// $html = $designer->build()->getHtml();
// $this->assertNotNull($html);
// //\Log::error($html);
// $settings = $this->invoice->client->settings;
// $settings->quote_design_id = "4";
// $settings->all_pages_header = true;
// $settings->all_pages_footer = true;
// $this->quote->client_id = $this->client->id;
// $this->quote->setRelation('client', $this->client);
// $this->quote->save();
// $this->client->settings = $settings;
// $this->client->save();
// CreateQuotePdf::dispatchNow($this->quote, $this->quote->company, $this->quote->client->primary_contact()->first());
// }
//
}

View File

@ -28,7 +28,7 @@ class InvoiceUploadTest extends TestCase
public function testInvoiceUploadWorks()
{
CreateInvoicePdf::dispatchNow($this->invoice, $this->invoice->company, $this->invoice->client->primary_contact()->first());
CreateInvoicePdf::dispatchNow($this->invoice->invitations->first());
$this->assertNotNull($this->invoice->service()->getInvoicePdf($this->invoice->client->primary_contact()->first()));
}

View File

@ -9,7 +9,6 @@ use Tests\TestCase;
/**
* @test
* @covers App\Http\ViewComposers\TranslationComposer
*/
class CollectionMergingTest extends TestCase
{

View File

@ -30,6 +30,9 @@ class CompanyDocumentsTest extends TestCase
public function testCompanyDocumentExists()
{
$company_key = $this->company->company_key;
$original_count = Document::whereCompanyId($this->company->id)->count();
$image = UploadedFile::fake()->image('avatar.jpg');
@ -44,16 +47,14 @@ class CompanyDocumentsTest extends TestCase
$this->assertNotNull($document);
$this->assertGreaterThan($original_count, Document::whereCompanyId($this->company->id)->count());
$this->assertTrue(Storage::exists($document->path));
$company_key = $this->company->company_key;
$this->assertGreaterThan($original_count, Document::whereCompanyId($this->company->id)->count());
$this->company->delete();
$this->assertEquals(0, Document::whereCompanyId($this->company->id)->count());
$path = sprintf('%s/%s', storage_path('app/public'), $company_key);
$this->assertFalse(file_exists($path));
$this->assertFalse(Storage::exists($document->path));
}
}

View File

@ -10,7 +10,7 @@ use Tests\TestCase;
/**
* @test
* @covers App\Utils\Traits\Invoice\ActionInvoice
* @covers App\Utils\Traits\Invoice\ActionsInvoice
*/
class InvoiceActionsTest extends TestCase
{