diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index 0f388ade0b3c..60ec71508a04 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -24,6 +24,7 @@ use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringInvoice; use App\Models\RecurringInvoiceInvitation; +use App\Services\Pdf\PdfService; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; use App\Services\PdfMaker\PdfMaker as PdfMakerService; @@ -49,22 +50,14 @@ use setasign\Fpdi\PdfParser\StreamReader; class CreateEntityPdf implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml, PdfMaker, MakesHash, PageNumbering; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $entity; - public $company; - - public $contact; - private $disk; public $invitation; - public $entity_string = ''; - - public $client; - /** * Create a new job instance. * @@ -72,145 +65,57 @@ class CreateEntityPdf implements ShouldQueue */ public function __construct($invitation, $disk = null) { + $this->invitation = $invitation; - if ($invitation instanceof InvoiceInvitation) { - // $invitation->load('contact.client.company','invoice.client','invoice.user.account'); - $this->entity = $invitation->invoice; - $this->entity_string = 'invoice'; - } elseif ($invitation instanceof QuoteInvitation) { - // $invitation->load('contact.client.company','quote.client','quote.user.account'); - $this->entity = $invitation->quote; - $this->entity_string = 'quote'; - } elseif ($invitation instanceof CreditInvitation) { - // $invitation->load('contact.client.company','credit.client','credit.user.account'); - $this->entity = $invitation->credit; - $this->entity_string = 'credit'; - } elseif ($invitation instanceof RecurringInvoiceInvitation) { - // $invitation->load('contact.client.company','recurring_invoice'); - $this->entity = $invitation->recurring_invoice; - $this->entity_string = 'recurring_invoice'; - } - - $this->company = $invitation->company; - - $this->contact = $invitation->contact; - - $this->client = $invitation->contact->client; - $this->client->load('company'); - $this->disk = $disk ?? config('filesystems.default'); + } public function handle() { - MultiDB::setDb($this->company->db); - /* Forget the singleton*/ - App::forgetInstance('translator'); + $starttime = microtime(true); - /* Init a new copy of the translator*/ - $t = app('translator'); - - /* Set the locale*/ - App::setLocale($this->client->locale()); + MultiDB::setDb($this->invitation->company->db); - /* Set customized translations _NOW_ */ - $t->replace(Ninja::transformTranslations($this->client->getMergedSettings())); + if ($this->invitation instanceof InvoiceInvitation) { + $this->entity = $this->invitation->invoice; + $path = $this->invitation->contact->client->invoice_filepath($this->invitation); + } elseif ($this->invitation instanceof QuoteInvitation) { + $this->entity = $this->invitation->quote; + $path = $this->invitation->contact->client->quote_filepath($this->invitation); + } elseif ($this->invitation instanceof CreditInvitation) { + $this->entity = $this->invitation->credit; + $path = $this->invitation->contact->client->credit_filepath($this->invitation); + } elseif ($this->invitation instanceof RecurringInvoiceInvitation) { + $this->entity = $this->invitation->recurring_invoice; + $path = $this->invitation->contact->client->recurring_invoice_filepath($this->invitation); - if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { - return (new Phantom)->generate($this->invitation); - } - - $entity_design_id = ''; - - if ($this->entity instanceof Invoice) { - $path = $this->client->invoice_filepath($this->invitation); - $entity_design_id = 'invoice_design_id'; - } elseif ($this->entity instanceof Quote) { - $path = $this->client->quote_filepath($this->invitation); - $entity_design_id = 'quote_design_id'; - } elseif ($this->entity instanceof Credit) { - $path = $this->client->credit_filepath($this->invitation); - $entity_design_id = 'credit_design_id'; - } elseif ($this->entity instanceof RecurringInvoice) { - $path = $this->client->recurring_invoice_filepath($this->invitation); - $entity_design_id = 'invoice_design_id'; } $file_path = $path.$this->entity->numberFormatter().'.pdf'; - $entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->client->getSetting($entity_design_id)); - - $design = Design::find($entity_design_id); - - /* Catch all in case migration doesn't pass back a valid design */ - if (! $design) { - $design = Design::find(2); - } - - $html = new HtmlEngine($this->invitation); - - 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)); - } - - $variables = $html->generateLabelsAndValues(); - - $state = [ - 'template' => $template->elements([ - 'client' => $this->client, - 'entity' => $this->entity, - 'pdf_variables' => (array) $this->company->settings->pdf_variables, - '$product' => $design->design->product, - 'variables' => $variables, - ]), - 'variables' => $variables, - 'options' => [ - 'all_pages_header' => $this->entity->client->getSetting('all_pages_header'), - 'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'), - ], - 'process_markdown' => $this->entity->client->company->markdown_enabled, - ]; - - $maker = new PdfMakerService($state); - - $maker - ->design($template) - ->build(); - - $pdf = null; - - try { - if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { - $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); - - $numbered_pdf = $this->pageNumbering($pdf, $this->company); - - if ($numbered_pdf) { - $pdf = $numbered_pdf; - } - } else { - $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); - - $numbered_pdf = $this->pageNumbering($pdf, $this->company); - - if ($numbered_pdf) { - $pdf = $numbered_pdf; - } - } - } catch (\Exception $e) { - nlog(print_r($e->getMessage(), 1)); - } - - if (config('ninja.log_pdf_html')) { - info($maker->getCompiledHTML()); - } + // $state = [ + // 'template' => $template->elements([ + // 'client' => $this->client, + // 'entity' => $this->entity, + // 'pdf_variables' => (array) $this->company->settings->pdf_variables, + // '$product' => $design->design->product, + // 'variables' => $variables, + // ]), + // 'variables' => $variables, + // 'options' => [ + // 'all_pages_header' => $this->entity->client->getSetting('all_pages_header'), + // 'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'), + // ], + // 'process_markdown' => $this->entity->client->company->markdown_enabled, + // ]; + + $pdf = (new PdfService($this->invitation))->getPdf(); + + $endtime = microtime(true); + nlog($endtime - $starttime); if ($pdf) { try { @@ -220,13 +125,6 @@ class CreateEntityPdf implements ShouldQueue } } - $this->invitation = null; - $this->entity = null; - $this->company = null; - $this->client = null; - $this->contact = null; - $maker = null; - $state = null; return $file_path; } diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index 0dcac235bd5c..8f8f5582ca68 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -20,6 +20,7 @@ use DOMDocument; use DOMXPath; use Illuminate\Support\Carbon; use Illuminate\Support\Str; +use League\CommonMark\CommonMarkConverter; class PdfBuilder { @@ -27,19 +28,35 @@ class PdfBuilder public PdfService $service; + private CommonMarkConverter $commonmark; + /** * an array of sections to be injected into the template + * * @var array */ public array $sections = []; + /** + * The DOM Document; + * + * @var $document + */ + public DomDocument $document; + /** * @param PdfService $service * @return void */ public function __construct(PdfService $service) { + $this->service = $service; + + $this->commonmark = new CommonMarkConverter([ + 'allow_unsafe_links' => false, + ]); + } /** @@ -52,11 +69,7 @@ class PdfBuilder { $this->getTemplate() - ->buildSections(); - - nlog($this->sections); - - $this + ->buildSections() ->getEmptyElements() ->updateElementProperties() ->updateVariables(); @@ -97,6 +110,7 @@ class PdfBuilder $this->xpath = new DOMXPath($document); return $this; + } /** @@ -120,9 +134,11 @@ class PdfBuilder private function mergeSections(array $section) :self { + $this->sections = array_merge($this->sections, $section); return $this; + } /** @@ -212,6 +228,7 @@ class PdfBuilder return [ ['element' => 'p', 'content' => '$outstanding_label: ' . Number::formatMoney($outstanding, $this->service->config->client)], ]; + } @@ -222,6 +239,7 @@ class PdfBuilder */ public function statementPaymentTable(): array { + if (is_null($this->service->option['payments'])) { return []; } @@ -255,6 +273,7 @@ class PdfBuilder ['element' => 'thead', 'elements' => $this->buildTableHeader('statement_payment')], ['element' => 'tbody', 'elements' => $tbody], ]; + } /** @@ -666,6 +685,7 @@ class PdfBuilder } return $elements; + } /** @@ -680,6 +700,7 @@ class PdfBuilder */ public function processTaxColumns(string $type): void { + if ($type == 'product') { $type_id = 1; } @@ -722,6 +743,7 @@ class PdfBuilder array_splice($this->service->config->pdf_variables["{$type}_columns"], $key, 1, $taxes); } } + } /** @@ -752,6 +774,7 @@ class PdfBuilder ['element' => 'script', 'content' => $javascript], ['element' => 'script', 'content' => $html_decode], ]]; + } /** @@ -772,6 +795,7 @@ class PdfBuilder ]); return $this; + } /** @@ -786,7 +810,6 @@ class PdfBuilder private function getProductEntityDetails(): self { - if($this->service->config->entity_string == 'invoice') { $this->mergeSections( [ @@ -821,7 +844,6 @@ class PdfBuilder return $this; - } /** @@ -850,6 +872,7 @@ class PdfBuilder */ private function statementTableTotals(): array { + return [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: block; align-items: flex-start; page-break-inside: avoid; visible !important;'], 'elements' => [ @@ -857,6 +880,7 @@ class PdfBuilder ]], ]], ]; + } /** @@ -898,6 +922,7 @@ class PdfBuilder } return false; + } //First pass done, need a second pass to abstract this content completely. @@ -1053,6 +1078,7 @@ class PdfBuilder ]); return $this; + } /** @@ -1063,6 +1089,7 @@ class PdfBuilder */ public function getClientDetails(): self { + $this->mergeSections( [ 'client-details' => [ 'id' => 'client-details', @@ -1071,6 +1098,7 @@ class PdfBuilder ]); return $this; + } /** @@ -1080,6 +1108,7 @@ class PdfBuilder */ public function productTable(): array { + $product_items = collect($this->service->config->entity->line_items)->filter(function ($item) { return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5; }); @@ -1092,6 +1121,7 @@ class PdfBuilder ['element' => 'thead', 'elements' => $this->buildTableHeader('product')], ['element' => 'tbody', 'elements' => $this->buildTableBody('$product')], ]; + } /** @@ -1101,6 +1131,7 @@ class PdfBuilder */ public function taskTable(): array { + $task_items = collect($this->service->config->entity->line_items)->filter(function ($item) { return $item->type_id == 2; }); @@ -1113,6 +1144,7 @@ class PdfBuilder ['element' => 'thead', 'elements' => $this->buildTableHeader('task')], ['element' => 'tbody', 'elements' => $this->buildTableBody('$task')], ]; + } @@ -1156,6 +1188,7 @@ class PdfBuilder $variables = $this->service->config->pdf_variables['invoice_details']; return $this->genericDetailsBuilder($variables); + } /** @@ -1166,6 +1199,7 @@ class PdfBuilder */ public function quoteDetails(): array { + $variables = $this->service->config->pdf_variables['quote_details']; if ($this->service->config->entity->partial > 0) { @@ -1173,6 +1207,7 @@ class PdfBuilder } return $this->genericDetailsBuilder($variables); + } @@ -1188,6 +1223,7 @@ class PdfBuilder $variables = $this->service->config->pdf_variables['credit_details']; return $this->genericDetailsBuilder($variables); + } /** @@ -1220,6 +1256,7 @@ class PdfBuilder }); return $this->genericDetailsBuilder($variables); + } /** @@ -1255,6 +1292,7 @@ class PdfBuilder } return $elements; + } @@ -1301,6 +1339,7 @@ class PdfBuilder */ public function clientDetails(): array { + $elements = []; if(!$this->service->config->client) @@ -1313,6 +1352,7 @@ class PdfBuilder } return $elements; + } /** @@ -1323,7 +1363,6 @@ class PdfBuilder public function deliveryNoteTable(): array { /* Static array of delivery note columns*/ - $thead = [ ['element' => 'th', 'content' => '$item_label', 'properties' => ['data-ref' => 'delivery_note-item_label']], ['element' => 'th', 'content' => '$description_label', 'properties' => ['data-ref' => 'delivery_note-description_label']], @@ -1354,6 +1393,7 @@ class PdfBuilder ['element' => 'thead', 'elements' => $thead], ['element' => 'tbody', 'elements' => $this->buildTableBody(PdfService::DELIVERY_NOTE)], ]; + } /** @@ -1366,6 +1406,7 @@ class PdfBuilder */ public function processNewLines(array &$items): void { + foreach ($items as $key => $item) { foreach ($item as $variable => $value) { $item[$variable] = str_replace("\n", '
', $value); @@ -1373,6 +1414,7 @@ class PdfBuilder $items[$key] = $item; } + } /** @@ -1383,6 +1425,7 @@ class PdfBuilder */ public function companyDetails(): array { + $variables = $this->service->config->pdf_variables['company_details']; $elements = []; @@ -1392,6 +1435,7 @@ class PdfBuilder } return $elements; + } /** @@ -1403,6 +1447,7 @@ class PdfBuilder */ public function companyAddress(): array { + $variables = $this->service->config->pdf_variables['company_address']; $elements = []; @@ -1412,6 +1457,7 @@ class PdfBuilder } return $elements; + } /** @@ -1423,6 +1469,7 @@ class PdfBuilder */ public function vendorDetails(): array { + $elements = []; $variables = $this->service->config->pdf_variables['vendor_details']; @@ -1432,6 +1479,7 @@ class PdfBuilder } return $elements; + } @@ -1442,11 +1490,14 @@ class PdfBuilder public function getSectionNode(string $selector) { + return $this->document->getElementById($selector); + } public function updateElementProperties() :self { + foreach ($this->sections as $element) { if (isset($element['tag'])) { $node = $this->document->getElementsByTagName($element['tag'])->item(0); @@ -1468,6 +1519,7 @@ class PdfBuilder } return $this; + } public function updateElementProperty($element, string $attribute, ?string $value) @@ -1487,15 +1539,17 @@ class PdfBuilder } return $element; + } public function createElementContent($element, $children) :self { + foreach ($children as $child) { $contains_html = false; if ($child['element'] !== 'script') { - if (array_key_exists('process_markdown', $this->service->options) && array_key_exists('content', $child) && $this->service->options['process_markdown']) { + if ($this->service->company->markdown_enabled && array_key_exists('content', $child)) { $child['content'] = str_replace('
', "\r", $child['content']); $child['content'] = $this->commonmark->convert($child['content'] ?? ''); } @@ -1539,10 +1593,12 @@ class PdfBuilder } return $this; + } public function updateVariables() { + $html = strtr($this->getCompiledHTML(), $this->service->html_variables['labels']); $html = strtr($html, $this->service->html_variables['values']); @@ -1552,10 +1608,12 @@ class PdfBuilder $this->document->saveHTML(); return $this; + } public function updateVariable(string $element, string $variable, string $value) { + $element = $this->document->getElementById($element); $original = $element->nodeValue; @@ -1569,10 +1627,12 @@ class PdfBuilder ); return $element; + } public function getEmptyElements() :self { + foreach ($this->sections as $element) { if (isset($element['elements'])) { $this->getEmptyChildrens($element['elements'], $this->service->html_variables); @@ -1580,10 +1640,12 @@ class PdfBuilder } return $this; + } public function getEmptyChildrens(array $children) { + foreach ($children as $key => $child) { if (isset($child['content']) && isset($child['show_empty']) && $child['show_empty'] === false) { $value = strtr($child['content'], $this->service->html_variables['values']); @@ -1598,6 +1660,7 @@ class PdfBuilder } return $this; + } } \ No newline at end of file diff --git a/app/Services/Pdf/PdfConfiguration.php b/app/Services/Pdf/PdfConfiguration.php index 61fe0acccba9..9b908a487d6a 100644 --- a/app/Services/Pdf/PdfConfiguration.php +++ b/app/Services/Pdf/PdfConfiguration.php @@ -29,7 +29,9 @@ use App\Models\RecurringInvoiceInvitation; use App\Models\Vendor; use App\Models\VendorContact; use App\Services\Pdf\PdfService; +use App\Utils\Ninja; use App\Utils\Traits\MakesHash; +use Illuminate\Support\Facades\App; class PdfConfiguration { @@ -61,7 +63,9 @@ class PdfConfiguration /** * The parent object of the currency + * * @var App\Models\Client | App\Models\Vendor + * */ public $currency_entity; @@ -72,20 +76,36 @@ class PdfConfiguration } - public function init() + public function init(): self { $this->setEntityType() ->setEntityProperties() ->setPdfVariables() ->setDesign() - ->setCurrency(); + ->setCurrency() + ->setLocale(); return $this; } - private function setCurrency() :self + private function setLocale(): self + { + + App::forgetInstance('translator'); + + $t = app('translator'); + + App::setLocale($this->settings_object->locale()); + + $t->replace(Ninja::transformTranslations($this->settings)); + + return $this; + + } + + private function setCurrency(): self { $this->currency = $this->client ? $this->client->currency() : $this->vendor->currency(); @@ -100,6 +120,7 @@ class PdfConfiguration { $default = (array) CompanySettings::getEntityVariableDefaults(); + $variables = (array)$this->service->company->settings->pdf_variables; foreach ($default as $property => $value) { @@ -139,11 +160,13 @@ class PdfConfiguration } return $this; + } private function setEntityProperties() { - $entity_design_id = ''; + + $entity_design_id = ''; if ($this->entity instanceof Invoice) { @@ -198,6 +221,7 @@ class PdfConfiguration $this->path = $this->path.$this->entity->numberFormatter().'.pdf'; return $this; + } private function setDesign() diff --git a/app/Services/Pdf/PdfService.php b/app/Services/Pdf/PdfService.php index 205c60c76c87..7ee6c2511ee9 100644 --- a/app/Services/Pdf/PdfService.php +++ b/app/Services/Pdf/PdfService.php @@ -12,13 +12,19 @@ namespace App\Services\Pdf; use App\Models\Account; -use App\Services\Pdf\PdfConfiguration; -use App\Utils\HtmlEngine; use App\Models\Company; +use App\Services\Pdf\PdfConfiguration; +use App\Utils\HostedPDF\NinjaPdf; +use App\Utils\HtmlEngine; +use App\Utils\PhantomJS\Phantom; +use App\Utils\Traits\Pdf\PageNumbering; +use App\Utils\Traits\Pdf\PdfMaker; class PdfService { + use PdfMaker, PageNumbering; + public $invitation; public Company $company; @@ -55,58 +61,90 @@ class PdfService $this->html_variables = (new HtmlEngine($invitation))->generateLabelsAndValues(); - $this->builder = (new PdfBuilder($this)); - $this->designer = (new PdfDesigner($this))->build(); $this->document_type = $document_type; $this->options = $options; - } - - public function build() - { - $this->builder->build(); - - return $this; + $this->builder = (new PdfBuilder($this))->build(); } + /** + * Resolves the PDF generation type and + * attempts to generate a PDF from the HTML + * string. + * + * @return mixed + * + */ public function getPdf() { + try { + + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') + { + + $pdf = (new Phantom)->convertHtmlToPdf($this->getHtml()); + + } + elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') + { + + $pdf = (new NinjaPdf())->build($this->getHtml()); + + } + else + { + + $pdf = $this->makePdf(null, null, $this->getHtml()); + + } + + $numbered_pdf = $this->pageNumbering($pdf, $this->company); + + if ($numbered_pdf) + { + + $pdf = $numbered_pdf; + + } + + + } catch (\Exception $e) { + + nlog(print_r($e->getMessage(), 1)); + + throw new \Exception($e->getMessage(), $e->getCode()); + + } + + return $pdf; + } - public function getHtml() + /** + * Renders the dom document to HTML + * + * @return string + * + */ + public function getHtml(): string { - return $this->builder->getCompiledHTML(); + + $html = $this->builder->getCompiledHTML(); + + if (config('ninja.log_pdf_html')) + { + + info($html); + + } + + return $html; + } - - // $state = [ - // 'template' => $template->elements([ - // 'client' => $this->client, - // 'entity' => $this->entity, - // 'pdf_variables' => (array) $this->company->settings->pdf_variables, - // '$product' => $design->design->product, - // 'variables' => $variables, - // ]), - // 'variables' => $variables, - // 'options' => [ - // 'all_pages_header' => $this->entity->client->getSetting('all_pages_header'), - // 'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'), - // ], - // 'process_markdown' => $this->entity->client->company->markdown_enabled, - // ]; - - // $maker = new PdfMakerService($state); - - // $maker - // ->design($template) - // ->build(); - - - - } \ No newline at end of file diff --git a/tests/Pdf/PdfServiceTest.php b/tests/Pdf/PdfServiceTest.php index 2ff72370ba38..d892bcc4c779 100644 --- a/tests/Pdf/PdfServiceTest.php +++ b/tests/Pdf/PdfServiceTest.php @@ -33,6 +33,17 @@ class PdfServiceTest extends TestCase $this->makeTestData(); } + public function testPdfGeneration() + { + + $invitation = $this->invoice->invitations->first(); + + $service = new PdfService($invitation); + + $this->assertNotNull($service->getPdf()); + + } + public function testHtmlGeneration() { @@ -40,9 +51,7 @@ class PdfServiceTest extends TestCase $service = new PdfService($invitation); - $this->assertIsString($service->build()->getHtml()); - - nlog($service->build()->getHtml()); + $this->assertIsString($service->getHtml()); }