Merge designer into design_changes

This commit is contained in:
David Bomba 2023-02-25 14:36:00 +11:00
commit d21ce4a567
17 changed files with 3046 additions and 24 deletions

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ yarn-error.log
local_version.txt
.env
.phpunit.result.cache
_ide_helper.php
/resources/assets/bower
/public/logo

View File

@ -22,7 +22,7 @@ class ClientContactFactory
$client_contact->first_name = '';
$client_contact->user_id = $user_id;
$client_contact->company_id = $company_id;
$client_contact->contact_key = Str::random(40);
$client_contact->contact_key = Str::random(32);
$client_contact->id = 0;
$client_contact->send_email = true;

View File

@ -22,7 +22,7 @@ class VendorContactFactory
$vendor_contact->first_name = '';
$vendor_contact->user_id = $user_id;
$vendor_contact->company_id = $company_id;
$vendor_contact->contact_key = Str::random(40);
$vendor_contact->contact_key = Str::random(32);
$vendor_contact->id = 0;
return $vendor_contact;

View File

@ -91,7 +91,7 @@ class InvitationController extends Controller
$file_name = $invitation->purchase_order->numberFormatter().'.pdf';
$file = (new CreatePurchaseOrderPdf($invitation))->rawPdf();
$file = (new CreatePurchaseOrderPdf($invitation))->handle();
$headers = ['Content-Type' => 'application/pdf'];

View File

@ -327,21 +327,6 @@ class Client extends BaseModel implements HasLocalePreference
return $this->service()->updateBalance($amount);
}
/**
* Adjusts client "balances" when a client
* makes a payment that goes on file, but does
* not effect the client.balance record.
*
* @param float $amount Adjustment amount
* @return Client
*/
// public function processUnappliedPayment($amount) :Client
// {
// return $this->service()->updatePaidToDate($amount)
// ->adjustCreditBalance($amount)
// ->save();
// }
/**
* Returns the entire filtered set
* of settings which have been merged from

View File

@ -169,6 +169,11 @@ class Vendor extends BaseModel
return '';
}
public function getMergedSettings() :object
{
return $this->company->settings;
}
public function purchase_order_filepath($invitation)
{
$contact_key = $invitation->contact->contact_key;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,352 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Pdf;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Design;
use App\Models\Vendor;
use App\Models\Country;
use App\Models\Invoice;
use App\Models\Currency;
use App\Models\ClientContact;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Utils\Traits\AppSetup;
use App\Models\QuoteInvitation;
use App\Utils\Traits\MakesHash;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\DataMapper\CompanySettings;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use App\Models\PurchaseOrderInvitation;
use App\Models\RecurringInvoiceInvitation;
use Illuminate\Support\Collection;
class PdfConfiguration
{
use MakesHash, AppSetup;
public ?Client $client;
public ?ClientContact $contact;
public Country $country;
public Currency $currency;
public Client | Vendor $currency_entity;
public Design $design;
public Invoice | Credit | Quote | PurchaseOrder $entity;
public string $entity_design_id;
public string $entity_string;
public ?string $path;
public array $pdf_variables;
public object $settings;
public $settings_object;
public ?Vendor $vendor;
public ?VendorContact $vendor_contact;
public string $date_format;
public string $locale;
public Collection $tax_map;
public ?array $total_tax_map;
/**
* __construct
*
* @param PdfService $service
* @return void
*/
public function __construct(public PdfService $service)
{
}
/**
* init
*
* @return self
*/
public function init(): self
{
$this->setEntityType()
->setDateFormat()
->setPdfVariables()
->setDesign()
->setCurrencyForPdf()
->setLocale();
return $this;
}
/**
* setLocale
*
* @return self
*/
private function setLocale(): self
{
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->settings_object->locale());
$t->replace(Ninja::transformTranslations($this->settings));
$this->locale = $this->settings_object->locale();
return $this;
}
/**
* setCurrency
*
* @return self
*/
private function setCurrencyForPdf(): self
{
$this->currency = $this->client ? $this->client->currency() : $this->vendor->currency();
$this->currency_entity = $this->client ? $this->client : $this->vendor;
return $this;
}
/**
* setPdfVariables
*
* @return self
*/
public function setPdfVariables() :self
{
$default = (array) CompanySettings::getEntityVariableDefaults();
// $variables = (array)$this->service->company->settings->pdf_variables;
$variables = (array)$this->settings->pdf_variables;
foreach ($default as $property => $value) {
if (array_key_exists($property, $variables)) {
continue;
}
$variables[$property] = $value;
}
$this->pdf_variables = $variables;
return $this;
}
/**
* setEntityType
*
* @return self
*/
private function setEntityType(): self
{
$entity_design_id = '';
if ($this->service->invitation instanceof InvoiceInvitation) {
$this->entity = $this->service->invitation->invoice;
$this->entity_string = 'invoice';
$this->client = $this->entity->client;
$this->contact = $this->service->invitation->contact;
$this->path = $this->client->invoice_filepath($this->service->invitation);
$this->entity_design_id = 'invoice_design_id';
$this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client;
$this->country = $this->client->country;
} elseif ($this->service->invitation instanceof QuoteInvitation) {
$this->entity = $this->service->invitation->quote;
$this->entity_string = 'quote';
$this->client = $this->entity->client;
$this->contact = $this->service->invitation->contact;
$this->path = $this->client->quote_filepath($this->service->invitation);
$this->entity_design_id = 'quote_design_id';
$this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client;
$this->country = $this->client->country;
} elseif ($this->service->invitation instanceof CreditInvitation) {
$this->entity = $this->service->invitation->credit;
$this->entity_string = 'credit';
$this->client = $this->entity->client;
$this->contact = $this->service->invitation->contact;
$this->path = $this->client->credit_filepath($this->service->invitation);
$this->entity_design_id = 'credit_design_id';
$this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client;
$this->country = $this->client->country;
} elseif ($this->service->invitation instanceof RecurringInvoiceInvitation) {
$this->entity = $this->service->invitation->recurring_invoice;
$this->entity_string = 'recurring_invoice';
$this->client = $this->entity->client;
$this->contact = $this->service->invitation->contact;
$this->path = $this->client->recurring_invoice_filepath($this->service->invitation);
$this->entity_design_id = 'invoice_design_id';
$this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client;
$this->country = $this->client->country;
} elseif ($this->service->invitation instanceof PurchaseOrderInvitation) {
$this->entity = $this->service->invitation->purchase_order;
$this->entity_string = 'purchase_order';
$this->vendor = $this->entity->vendor;
$this->vendor_contact = $this->service->invitation->contact;
$this->path = $this->vendor->purchase_order_filepath($this->service->invitation);
$this->entity_design_id = 'invoice_design_id';
$this->entity_design_id = 'purchase_order_design_id';
$this->settings = $this->vendor->company->settings;
$this->settings_object = $this->vendor;
$this->client = null;
$this->country = $this->vendor->country ?: $this->vendor->company->country();
} else {
throw new \Exception('Unable to resolve entity', 500);
}
$this->setTaxMap($this->entity->calc()->getTaxMap());
$this->setTotalTaxMap($this->entity->calc()->getTotalTaxMap());
$this->path = $this->path.$this->entity->numberFormatter().'.pdf';
return $this;
}
public function setTaxMap($map): self
{
$this->tax_map = $map;
return $this;
}
public function setTotalTaxMap($map): self
{
$this->total_tax_map = $map;
return $this;
}
public function setCurrency(Currency $currency): self
{
$this->currency = $currency;
return $this;
}
public function setCountry(Country $country): self
{
$this->country = $country;
return $this;
}
/**
* setDesign
*
* @return self
*/
private function setDesign(): self
{
$design_id = $this->entity->design_id ? : $this->decodePrimaryKey($this->settings_object->getSetting($this->entity_design_id));
$this->design = Design::find($design_id ?: 2);
return $this;
}
/**
* formatMoney
*
* @param float $value
* @return string
*/
public function formatMoney($value): string
{
$value = floatval($value);
$thousand = $this->currency->thousand_separator;
$decimal = $this->currency->decimal_separator;
$precision = $this->currency->precision;
$code = $this->currency->code;
$swapSymbol = $this->currency->swap_currency_symbol;
if (isset($this->country->thousand_separator) && strlen($this->country->thousand_separator) >= 1) {
$thousand = $this->country->thousand_separator;
}
if (isset($this->country->decimal_separator) && strlen($this->country->decimal_separator) >= 1) {
$decimal = $this->country->decimal_separator;
}
if (isset($this->country->swap_currency_symbol) && strlen($this->country->swap_currency_symbol) >= 1) {
$swapSymbol = $this->country->swap_currency_symbol;
}
$value = number_format($value, $precision, $decimal, $thousand);
$symbol = $this->currency->symbol;
if ($this->settings->show_currency_code === true && $this->currency->code == 'CHF') {
return "{$code} {$value}";
} elseif ($this->settings->show_currency_code === true) {
return "{$value} {$code}";
} elseif ($swapSymbol) {
return "{$value} ".trim($symbol);
} elseif ($this->settings->show_currency_code === false) {
return "{$symbol}{$value}";
} else {
$value = floatval($value);
$thousand = $this->currency->thousand_separator;
$decimal = $this->currency->decimal_separator;
$precision = $this->currency->precision;
return number_format($value, $precision, $decimal, $thousand);
}
}
/**
* date_format
*
* @return self
*/
public function setDateFormat(): self
{
$date_formats = Cache::get('date_formats');
if (! $date_formats) {
$this->buildCache(true);
}
$this->date_format = $date_formats->filter(function ($item) {
return $item->id == $this->settings->date_format_id;
})->first()->format;
return $this;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Pdf;
class PdfDesigner
{
const BOLD = 'bold';
const BUSINESS = 'business';
const CLEAN = 'clean';
const CREATIVE = 'creative';
const ELEGANT = 'elegant';
const HIPSTER = 'hipster';
const MODERN = 'modern';
const PLAIN = 'plain';
const PLAYFUL = 'playful';
const CUSTOM = 'custom';
const CALM = 'calm';
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';
public string $template;
public function __construct(public PdfService $service)
{
}
public function build() :self
{
/*If the design is custom*/
if ($this->service->config->design->is_custom) {
$this->template = $this->composeFromPartials(json_decode(json_encode($this->service->config->design->design), true));
} else {
$this->template = file_get_contents(config('ninja.designs.base_path') . strtolower($this->service->config->design->name) . '.html');
}
return $this;
}
/**
* If the user has implemented a custom design, then we need to rebuild the design at this point
*/
/**
* Returns the custom HTML design as
* a string
*
* @param array
* @return string
*
*/
private function composeFromPartials(array $partials) :string
{
$html = '';
$html .= $partials['includes'];
$html .= $partials['header'];
$html .= $partials['body'];
$html .= $partials['footer'];
return $html;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,157 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Pdf;
use App\Models\Account;
use App\Models\Company;
use App\Utils\HtmlEngine;
use App\Models\QuoteInvitation;
use App\Utils\VendorHtmlEngine;
use App\Models\CreditInvitation;
use App\Utils\PhantomJS\Phantom;
use App\Models\InvoiceInvitation;
use App\Services\Pdf\PdfDesigner;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Models\PurchaseOrderInvitation;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Models\RecurringInvoiceInvitation;
class PdfService
{
use PdfMaker, PageNumbering;
public InvoiceInvitation | QuoteInvitation | CreditInvitation | RecurringInvoiceInvitation | PurchaseOrderInvitation $invitation;
public Company $company;
public Account $account;
public PdfConfiguration $config;
public PdfBuilder $builder;
public PdfDesigner $designer;
public array $html_variables;
public string $document_type;
public array $options;
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';
const PRODUCT = 'product';
public function __construct($invitation, $document_type = 'product', $options = [])
{
$this->invitation = $invitation;
$this->company = $invitation->company;
$this->account = $this->company->account;
$this->document_type = $document_type;
$this->options = $options;
}
/**
* Resolves the PDF generation type and
* attempts to generate a PDF from the HTML
* string.
*
* @return mixed | Exception
*
*/
public function getPdf()
{
try {
$pdf = $this->resolvePdfEngine();
$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;
}
/**
* Renders the dom document to HTML
*
* @return string
*
*/
public function getHtml(): string
{
$html = $this->builder->getCompiledHTML();
if (config('ninja.log_pdf_html')) {
info($html);
}
return $html;
}
/**
* Initialize all the services to build the PDF
*
* @return self
*/
public function init(): self
{
$this->config = (new PdfConfiguration($this))->init();
$this->html_variables = $this->config->client ?
(new HtmlEngine($this->invitation))->generateLabelsAndValues() :
(new VendorHtmlEngine($this->invitation))->generateLabelsAndValues();
$this->designer = (new PdfDesigner($this))->build();
$this->builder = (new PdfBuilder($this))->build();
return $this;
}
/**
* resolvePdfEngine
*
* @return mixed
*/
private function resolvePdfEngine(): mixed
{
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());
}
return $pdf;
}
}

View File

@ -175,10 +175,6 @@ class Design extends BaseDesign
$this->sharedFooterElements(),
],
],
// 'swiss-qr' => [
// 'id' => 'swiss-qr',
// 'elements' => $this->swissQrCodeElement(),
// ]
];
}

View File

@ -105,7 +105,7 @@ class Helpers
* Process reserved keywords on PDF.
*
* @param string $value
* @param Client|Company $entity
* @param Client|Company|Vendor $entity
* @param null|Carbon $currentDateTime
* @return null|string
*/

View File

@ -26,7 +26,12 @@ class VendorContactFactory extends Factory
'first_name' => $this->faker->firstName(),
'last_name' => $this->faker->lastName(),
'phone' => $this->faker->phoneNumber(),
'email_verified_at' => now(),
'email' => $this->faker->unique()->safeEmail(),
'send_email' => true,
'password' => bcrypt('password'),
'remember_token' => \Illuminate\Support\Str::random(10),
'contact_key' => \Illuminate\Support\Str::random(32),
];
}
}

View File

@ -17,7 +17,7 @@ use Tests\TestCase;
/**
* @test
//@covers App\DataMapper\BaseSettings
* @covers App\DataMapper\BaseSettings
*/
class PdfGenerationTest extends TestCase
{

View File

@ -0,0 +1,111 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Pdf;
use App\Services\Pdf\PdfConfiguration;
use App\Services\Pdf\PdfService;
use Beganovich\Snappdf\Snappdf;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Pdf\PdfService
*/
class PdfServiceTest extends TestCase
{
use MockAccountData;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
}
public function testPdfGeneration()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertNotNull($service->getPdf());
}
public function testHtmlGeneration()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertIsString($service->getHtml());
}
public function testInitOfClass()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertInstanceOf(PdfService::class, $service);
}
public function testEntityResolution()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertInstanceOf(PdfConfiguration::class, $service->config);
}
public function testDefaultDesign()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertEquals(2, $service->config->design->id);
}
public function testHtmlIsArray()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertIsArray($service->html_variables);
}
public function testTemplateResolution()
{
$invitation = $this->invoice->invitations->first();
$service = new PdfService($invitation);
$this->assertIsString($service->designer->template);
}
}

99
tests/Pdf/PdfmockTest.php Normal file
View File

@ -0,0 +1,99 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Pdf;
use Tests\TestCase;
use App\Models\Design;
use App\Models\Country;
use App\Models\Invoice;
use App\Models\Currency;
use App\Services\Pdf\PdfBuilder;
use Tests\MockAccountData;
use App\Services\Pdf\PdfMock;
use Beganovich\Snappdf\Snappdf;
use App\Services\Pdf\PdfService;
use App\Services\Pdf\PdfConfiguration;
/**
* @test
* @covers App\Services\Pdf\PdfService
*/
class PdfmockTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
}
public function testPdfInstance ()
{
$entity = (new \App\Services\Pdf\PdfMock())->build();
$this->assertInstanceOf(Invoice::class, $entity);
$this->assertNotNull($entity->client);
$pdf_service = new PdfService($entity->invitation);
$this->assertNotNull($pdf_service);
$pdf_config = (new PdfConfiguration($pdf_service));
$this->assertNotNull($pdf_config);
}
public function testHtmlGeneration()
{
$pdf_mock = (new PdfMock());
$mock = $pdf_mock->build();
$pdf_service = new PdfService($mock->invitation);
$pdf_config = (new PdfConfiguration($pdf_service));
$pdf_config->entity = $mock;
$pdf_config->setTaxMap($mock->tax_map);
$pdf_config->setTotalTaxMap($mock->total_tax_map);
$pdf_config->setCurrency(Currency::find(1));
$pdf_config->setCountry(Country::find(840));
$pdf_config->client = $mock->client;
$pdf_config->entity_design_id = 'invoice_design_id';
$pdf_config->settings_object = $mock->client;
$pdf_config->entity_string = 'invoice';
$pdf_config->settings = (object)$pdf_config->service->company->settings;
$pdf_config->setPdfVariables();
$pdf_config->design = Design::find(2);
$pdf_config->currency_entity = $mock->client;
$pdf_service->config = $pdf_config;
$pdf_designer = (new \App\Services\Pdf\PdfDesigner($pdf_service))->build();
$pdf_service->designer = $pdf_designer;
$pdf_service->html_variables = $pdf_mock->getStubVariables();
$pdf_builder = (new PdfBuilder($pdf_service))->build();
$pdf_service->builder = $pdf_builder;
$this->assertNotNull($pdf_config);
$html = $pdf_service->getHtml();
nlog($html);
$this->assertNotNull($html);
}
}