diff --git a/VERSION.txt b/VERSION.txt index c212b57a0df3..13f1af3e031e 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.47 \ No newline at end of file +5.7.48 \ No newline at end of file diff --git a/app/Factory/PurchaseOrderFactory.php b/app/Factory/PurchaseOrderFactory.php index 4e68f9ad2f0f..ef242eb158a7 100644 --- a/app/Factory/PurchaseOrderFactory.php +++ b/app/Factory/PurchaseOrderFactory.php @@ -50,6 +50,7 @@ class PurchaseOrderFactory $purchase_order->company_id = $company_id; $purchase_order->recurring_id = null; $purchase_order->exchange_rate = 1; + $purchase_order->total_taxes = 0; return $purchase_order; } diff --git a/app/Helpers/Epc/EpcQrGenerator.php b/app/Helpers/Epc/EpcQrGenerator.php index a8f4f1518e97..fe67c1013438 100644 --- a/app/Helpers/Epc/EpcQrGenerator.php +++ b/app/Helpers/Epc/EpcQrGenerator.php @@ -69,7 +69,6 @@ class EpcQrGenerator return ''; } - } public function encodeMessage() @@ -86,7 +85,7 @@ class EpcQrGenerator $this->sepa['purpose'], substr($this->invoice->number, 0, 34), '', - '' + ' ' ]), "\n"); } diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 8f62169b0e4f..15533c7c2cdb 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -167,11 +167,11 @@ class InvitationController extends Controller { set_time_limit(45); - if (Ninja::isHosted()) { + // if (Ninja::isHosted()) { return $this->returnRawPdf($entity, $invitation_key); - } + // } - return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf'); + // return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf'); } private function returnRawPdf(string $entity, string $invitation_key) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index d680935af130..27e76dddccc9 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -388,6 +388,7 @@ class PreviewController extends BaseController $design_object = json_decode(json_encode(request()->input('design')), 1); $ts = (new TemplateService()); + try { $ts->setCompany($company) ->setTemplate($design_object) @@ -395,7 +396,6 @@ class PreviewController extends BaseController } catch(SyntaxError $e) { // return response()->json(['message' => 'Twig syntax is invalid.', 'errors' => new \stdClass], 422); - } $html = $ts->getHtml(); diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php index 5ae5e13dd481..d9ddff121d4e 100644 --- a/app/Http/Requests/Task/StoreTaskRequest.php +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -54,14 +54,14 @@ class StoreTaskRequest extends Request $rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.$user->company()->id.',is_deleted,0'; } - $rules['time_log'] = ['bail', function ($attribute, $values, $fail) { + $rules['time_log'] = ['bail',function ($attribute, $values, $fail) { - if(is_string($values)) { - $values = json_decode($values, 1); - } + if(is_string($values)) + $values = json_decode($values, true); if(!is_array($values)) { - return $fail('The '.$attribute.' is invalid. Must be an array.'); + $fail('The '.$attribute.' must be a valid array.'); + return; } foreach ($values as $k) { @@ -119,6 +119,10 @@ class StoreTaskRequest extends Request } } + if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}'){ + $input['time_log'] = json_encode([]); + } + $this->replace($input); } } diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 7779dc0340df..85b8254dd75a 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -43,7 +43,6 @@ class UpdateTaskRequest extends Request public function rules() { - /** @var \App\Models\User $user */ $user = auth()->user(); @@ -61,14 +60,15 @@ class UpdateTaskRequest extends Request $rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.$user->company()->id.',is_deleted,0'; } - $rules['time_log'] = ['bail',function ($attribute, $values, $fail) { + $rules['time_log'] = ['bail', function ($attribute, $values, $fail) { if(is_string($values)) { - $values = json_decode($values, 1); + $values = json_decode($values, true); } if(!is_array($values)) { - return $fail('The '.$attribute.' is invalid. Must be an array.'); + $fail('The '.$attribute.' must be a valid array.'); + return; } foreach ($values as $k) { @@ -129,6 +129,10 @@ class UpdateTaskRequest extends Request } + if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}') { + $input['time_log'] = json_encode([]); + } + $this->replace($input); } diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 7d353c429b53..f78942edc7a3 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -19,6 +19,7 @@ use Illuminate\Contracts\Validation\Rule; class BlackListRule implements Rule { private array $blacklist = [ + 'ckptr.com', 'pretreer.com', 'candassociates.com', 'vusra.com', diff --git a/app/Jobs/Inventory/AdjustProductInventory.php b/app/Jobs/Inventory/AdjustProductInventory.php index 8ee86b0cbda2..1f1ab6b7db83 100644 --- a/app/Jobs/Inventory/AdjustProductInventory.php +++ b/app/Jobs/Inventory/AdjustProductInventory.php @@ -31,6 +31,8 @@ class AdjustProductInventory implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies; + private array $notified_products = []; + public function __construct(public Company $company, public Invoice $invoice, public $old_invoice = []) { } @@ -56,18 +58,6 @@ class AdjustProductInventory implements ShouldQueue { MultiDB::setDb($this->company->db); - // foreach ($this->invoice->line_items as $item) { - // $p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first(); - - // if (! $p) { - // continue; - // } - - // $p->in_stock_quantity += $item->quantity; - - // $p->saveQuietly(); - // } - collect($this->invoice->line_items)->filter(function ($item) { return $item->type_id == '1'; })->each(function ($i) { @@ -147,11 +137,15 @@ class AdjustProductInventory implements ShouldQueue $nmo->company = $this->company; $nmo->settings = $this->company->settings; + $this->company->company_users->each(function ($cu) use ($product, $nmo, $notification_level) { - if ($this->checkNotificationExists($cu, $product, ['inventory_all', 'inventory_user', 'inventory_threshold_all', 'inventory_threshold_user'])) { + + /** @var \App\Models\CompanyUser $cu */ + if ($this->checkNotificationExists($cu, $product, ['inventory_all', 'inventory_user', 'inventory_threshold_all', 'inventory_threshold_user']) && (! in_array($product->id, $this->notified_products))) { $nmo->mailable = new NinjaMailer((new InventoryNotificationObject($product, $notification_level, $cu->portalType()))->build()); $nmo->to_user = $cu->user; NinjaMailerJob::dispatch($nmo); + $this->notified_products[] = $product->id; } }); } diff --git a/app/Jobs/Ninja/SystemMaintenance.php b/app/Jobs/Ninja/SystemMaintenance.php index 1959af92e5fc..c1cb51e204cb 100644 --- a/app/Jobs/Ninja/SystemMaintenance.php +++ b/app/Jobs/Ninja/SystemMaintenance.php @@ -157,4 +157,4 @@ class SystemMaintenance implements ShouldQueue } -} +} \ No newline at end of file diff --git a/app/Mail/Admin/InventoryNotificationObject.php b/app/Mail/Admin/InventoryNotificationObject.php index 2bdbc9ef916b..496c39902e61 100644 --- a/app/Mail/Admin/InventoryNotificationObject.php +++ b/app/Mail/Admin/InventoryNotificationObject.php @@ -69,7 +69,7 @@ class InventoryNotificationObject ] ), 'url' => $this->product->portalUrl($this->use_react_url), - 'button' => $this->use_react_url ? ctrans('texts.product_library') : ctrans('ninja.app_url'), + 'button' => ctrans('texts.view'), 'signature' => $this->product->company->settings->email_signature, 'logo' => $this->product->company->present()->logo(), 'settings' => $this->product->company->settings, diff --git a/app/Models/Document.php b/app/Models/Document.php index 636609058bff..5d4ab5923cb2 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -211,7 +211,7 @@ class Document extends BaseModel $image = $this->getFile(); $catch_image = $image; - if(extension_loaded('imagick')) + if(!extension_loaded('imagick')) return $catch_image; try { diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 0ac0ce6070be..15cf1faff44a 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -268,7 +268,8 @@ class PurchaseOrder extends BaseModel { return $this->belongsTo(Client::class)->withTrashed(); } - public function markInvitationsSent() + + public function markInvitationsSent(): void { $this->invitations->each(function ($invitation) { if (! isset($invitation->sent_date)) { diff --git a/app/Models/Task.php b/app/Models/Task.php index 9bbab1d15a79..df8fd59603b5 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -239,4 +239,23 @@ class Task extends BaseModel return $this->company->settings->default_task_rate ?? 0; } + + public function processLogs() + { + return + collect($this->time_log)->map(function ($log){ + + $parent_entity = $this->client ?? $this->company; + + if($log[0]) + $log[0] = Carbon::createFromTimestamp($log[0])->format($parent_entity->date_format()); + + if($log[1] && $log[1] != 0) + $log[1] = Carbon::createFromTimestamp($log[1])->format($parent_entity->date_format()); + else + $log[1] = ctrans('texts.running'); + + return $log; + })->toArray(); + } } diff --git a/app/PaymentDrivers/PayFastPaymentDriver.php b/app/PaymentDrivers/PayFastPaymentDriver.php index 707aabd999db..40fbaf00c33e 100644 --- a/app/PaymentDrivers/PayFastPaymentDriver.php +++ b/app/PaymentDrivers/PayFastPaymentDriver.php @@ -69,7 +69,7 @@ class PayFastPaymentDriver extends BaseDriver public function init() { try { - $this->payfast = new \PayFast\PayFastPayment( + $this->payfast = new \Payfast\PayFastPayment( [ 'merchantId' => $this->company_gateway->getConfigField('merchantId'), 'merchantKey' => $this->company_gateway->getConfigField('merchantKey'), diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 21e846438fc0..cdb9b5f5394c 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -37,7 +37,8 @@ class ClientService public function calculateBalance(?Invoice $invoice = null) { - $balance = Invoice::where('client_id', $this->client->id) + $balance = Invoice::withTrashed() + ->where('client_id', $this->client->id) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('is_deleted', false) ->sum('balance'); diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 45ae94ed7a7f..0484b5228aa2 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -273,7 +273,6 @@ class EmailDefaults return $this; // return $this->email->email_object->cc; // return [ - // ]; } diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index 32bc6dfba54e..dd453097e073 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -633,9 +633,7 @@ class PdfBuilder $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']]; } elseif ($cell == '$product.tax_rate3') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']]; - } - - elseif ($cell == '$task.discount' && !$this->service->company->enable_product_discount) { + } elseif ($cell == '$task.discount' && !$this->service->company->enable_product_discount) { $element['elements'][] = ['element' => 'td', 'content' => $row['$task.discount'], 'properties' => ['data-ref' => 'task_table-task.discount-td', 'style' => 'display: none;']]; } elseif ($cell == '$task.tax_rate1') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax1-td']]; @@ -643,10 +641,7 @@ class PdfBuilder $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax2-td']]; } elseif ($cell == '$task.tax_rate3') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax3-td']]; - } - - - elseif ($cell == '$product.unit_cost' || $cell == '$task.rate') { + } elseif ($cell == '$product.unit_cost' || $cell == '$task.rate') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['style' => 'white-space: nowrap;', 'data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']]; } else { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']]; @@ -677,7 +672,7 @@ class PdfBuilder $locale_info = localeconv(); foreach ($items as $key => $item) { - /** @var \App\DataMapper\InvoiceItem $item */ + /** @var \App\DataMapper\InvoiceItem $item */ if ($table_type == '$product' && $item->type_id != 1) { if ($item->type_id != 4 && $item->type_id != 6 && $item->type_id != 5) { @@ -712,9 +707,9 @@ class PdfBuilder $data[$key][$table_type.".{$_table_type}4"] = strlen($item->custom_value4) >= 1 ? $helpers->formatCustomFieldValue($this->service->company->custom_fields, "{$_table_type}4", $item->custom_value4, $this->service->config->currency_entity) : ''; if ($item->quantity > 0 || $item->cost > 0) { - $data[$key][$table_type.'.quantity'] = $item->quantity; + $data[$key][$table_type.'.quantity'] = $this->service->config->formatValueNoTrailingZeroes($item->quantity); - $data[$key][$table_type.'.unit_cost'] = $this->service->config->formatMoney($item->cost); + $data[$key][$table_type.'.unit_cost'] = $this->service->config->formatMoneyNoRounding($item->cost); $data[$key][$table_type.'.cost'] = $this->service->config->formatMoney($item->cost); @@ -820,9 +815,7 @@ class PdfBuilder $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } elseif ($column == '$product.tax_rate3') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; - } - - elseif ($column == '$task.discount' && !$this->service->company->enable_product_discount) { + } elseif ($column == '$task.discount' && !$this->service->company->enable_product_discount) { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']]; } elseif ($column == '$task.tax_rate1') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax1-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; @@ -830,9 +823,7 @@ class PdfBuilder $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax2-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } elseif ($column == '$task.tax_rate3') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax3-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; - } - - else { + } else { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } } @@ -1166,8 +1157,7 @@ class PdfBuilder } elseif (Str::startsWith($variable, '$custom_surcharge')) { $_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1 - // $visible = intval($this->service->config->entity->{$_variable}) != 0; - $visible = intval(str_replace(['0','.'], '', $this->service->config->entity->{$_variable})) != 0; + $visible = intval(str_replace(['0','.'], '', ($this->service->config->entity->{$_variable} ?? ''))) != 0; $elements[1]['elements'][] = ['element' => 'div', 'elements' => [ ['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']], @@ -1622,12 +1612,6 @@ class PdfBuilder // Dom Traversal /////////////////////////////////////// - - public function getSectionNode(string $selector) - { - return $this->document->getElementById($selector); - } - public function updateElementProperties() :self { foreach ($this->sections as $element) { @@ -1679,7 +1663,7 @@ class PdfBuilder if ($child['element'] !== 'script') { if ($this->service->company->markdown_enabled && array_key_exists('content', $child)) { - $child['content'] = str_replace('
', "\r", $child['content']); + $child['content'] = str_replace('
', "\r", ($child['content'] ?? '')); $child['content'] = $this->commonmark->convert($child['content'] ?? ''); } } diff --git a/app/Services/Pdf/PdfConfiguration.php b/app/Services/Pdf/PdfConfiguration.php index febe6d7d3ffe..3b97e841c1ab 100644 --- a/app/Services/Pdf/PdfConfiguration.php +++ b/app/Services/Pdf/PdfConfiguration.php @@ -12,6 +12,7 @@ namespace App\Services\Pdf; use App\DataMapper\CompanySettings; +use App\Libraries\MultiDB; use App\Models\Client; use App\Models\ClientContact; use App\Models\Country; @@ -94,6 +95,8 @@ class PdfConfiguration */ public function init(): self { + MultiDB::setDb($this->service->company->db); + $this->setEntityType() ->setDateFormat() ->setPdfVariables() @@ -271,9 +274,9 @@ class PdfConfiguration */ private function setDesign(): self { - $design_id = $this->entity->design_id ? : $this->decodePrimaryKey($this->settings_object->getSetting($this->entity_design_id)); - - $this->design = Design::withTrashed()->find($design_id ?: 2); + $design_id = $this->entity->design_id ?: $this->decodePrimaryKey($this->settings_object->getSetting($this->entity_design_id)); + + $this->design = Design::withTrashed()->find($design_id ?? 2); return $this; } @@ -327,6 +330,125 @@ class PdfConfiguration } } + /** + * Formats a given value based on the clients currency. + * + * @param float $value The number to be formatted + * + * @return string The formatted value + */ + public function formatValueNoTrailingZeroes($value) :string + { + $value = floatval($value); + + $thousand = $this->currency->thousand_separator; + $decimal = $this->currency->decimal_separator; + + /* Country settings override client settings */ + 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; + } + + $precision = 10; + + return rtrim(rtrim(number_format($value, $precision, $decimal, $thousand), '0'), $decimal); + } + + + /** + * Formats a given value based on the clients currency AND country. + * + * @param float $value The number to be formatted + * @return string The formatted value + */ + public function formatMoneyNoRounding($value) :string + { + + $_value = $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; + + /* Country settings override client settings */ + 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; + } + + /* 08-01-2022 allow increased precision for unit price*/ + $v = rtrim(sprintf('%f', $value), '0'); + $parts = explode('.', $v); + + /* 08-02-2023 special if block to render $0.5 to $0.50*/ + if ($v < 1 && strlen($v) == 3) { + $precision = 2; + } elseif ($v < 1) { + $precision = strlen($v) - strrpos($v, '.') - 1; + } + + if (is_array($parts) && $parts[0] != 0) { + $precision = 2; + } + + //04-04-2023 if currency = JPY override precision to 0 + if($this->currency->code == 'JPY') { + $precision = 0; + } + + $value = number_format($v, $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) { + if ($_value < 0) { + $value = substr($value, 1); + $symbol = "-{$symbol}"; + } + + return "{$symbol}{$value}"; + } else { + return $this->formatValue($value); + } + } + + /** + * Formats a given value based on the clients currency. + * + * @param float $value The number to be formatted + * + * @return string The formatted value + */ + public function formatValue($value) :string + { + $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 * diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index c5b783b51399..771885405fb6 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -13,15 +13,15 @@ namespace App\Services\Template; use App\Models\Client; use App\Models\Company; +use App\Models\Credit; use App\Models\Design; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; use App\Models\PurchaseOrder; -use App\Transformers\ProjectTransformer; -use App\Transformers\PurchaseOrderTransformer; -use App\Transformers\QuoteTransformer; -use App\Transformers\TaskTransformer; +use App\Models\Quote; +use App\Models\RecurringInvoice; +use App\Models\Vendor; use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HtmlEngine; use App\Utils\Number; @@ -29,28 +29,22 @@ use App\Utils\PaymentHtmlEngine; use App\Utils\Traits\MakesDates; use App\Utils\Traits\Pdf\PdfMaker; use App\Utils\VendorHtmlEngine; -use League\Fractal\Manager; -use League\Fractal\Serializer\ArraySerializer; -use Twig\Environment; +use League\CommonMark\CommonMarkConverter; use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -use Twig\Extension\DebugExtension; -use Twig\Extension\StringLoaderExtension; use Twig\Extra\Intl\IntlExtension; -use Twig\Loader\FilesystemLoader; use Twig\Sandbox\SecurityError; -use Twig\TwigFilter; -use Twig\TwigFunction; class TemplateService { - use MakesDates, PdfMaker; - + use MakesDates; + use PdfMaker; + private \DomDocument $document; - public Environment $twig; + public \Twig\Environment $twig; private string $compiled_html = ''; @@ -60,12 +54,22 @@ class TemplateService public ?Company $company; + private ?Client $client; + + private ?Vendor $vendor; + + private Invoice | Quote | Credit | PurchaseOrder | RecurringInvoice $entity; + + private Payment $payment; + + private CommonMarkConverter $commonmark; + public function __construct(public ?Design $template = null) { $this->template = $template; $this->init(); } - + /** * Boot Dom Document * @@ -73,35 +77,40 @@ class TemplateService */ private function init(): self { - $this->document = new \DOMDocument(); - $this->document->validateOnParse = true; - $loader = new FilesystemLoader(storage_path()); - $this->twig = new Environment($loader, [ - 'debug' => true, + + $this->commonmark = new CommonMarkConverter([ + 'allow_unsafe_links' => false, ]); - $string_extension = new StringLoaderExtension(); + $this->document = new \DOMDocument(); + $this->document->validateOnParse = true; + + $loader = new \Twig\Loader\FilesystemLoader(storage_path()); + $this->twig = new \Twig\Environment($loader, [ + 'debug' => true, + ]); + $string_extension = new \Twig\Extension\StringLoaderExtension(); $this->twig->addExtension($string_extension); $this->twig->addExtension(new IntlExtension()); - $this->twig->addExtension(new DebugExtension()); - - $function = new TwigFunction('img', function ($string, $style = '') { - return ''; + $this->twig->addExtension(new \Twig\Extension\DebugExtension()); + + $function = new \Twig\TwigFunction('img', function ($string, $style = '') { + return ''; }); $this->twig->addFunction($function); - $filter = new TwigFilter('sum', function (array $array, string $column) { + $filter = new \Twig\TwigFilter('sum', function (array $array, string $column) { return array_sum(array_column($array, $column)); }); - + $this->twig->addFilter($filter); return $this; } - + /** * Iterate through all of the - * ninja nodes + * ninja nodes, and field stacks * * @param array $data - the payload to be passed into the template * @return self @@ -110,13 +119,20 @@ class TemplateService { $this->compose() ->processData($data) + ->parseGlobalStacks() ->parseNinjaBlocks() ->processVariables($data) ->parseVariables(); return $this; } - + + /** + * Initialized a set of HTMLEngine variables + * + * @param array | \Illuminate\Support\Collection $data + * @return self + */ private function processVariables($data): self { $this->variables = $this->resolveHtmlEngine($data); @@ -124,21 +140,28 @@ class TemplateService return $this; } + /** + * Returns a Mock Template + * + * @return self + */ public function mock(): self { $tm = new TemplateMock($this->company); $tm->init(); + $this->entity = $this->company->invoices()->first(); + $this->data = $tm->engines; $this->variables = $tm->variables[0]; - $this->parseNinjaBlocks() + ->parseGlobalStacks() ->parseVariables(); return $this; } - + /** * Returns the HTML as string * @@ -149,7 +172,12 @@ class TemplateService return $this->compiled_html; } - public function getPdf(): mixed + /** + * Returns the PDF string + * + * @return string + */ + public function getPdf(): string { if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { @@ -162,11 +190,22 @@ class TemplateService } + /** + * Get the parsed data + * + * @return array + */ public function getData(): array { return $this->data; } - + + /** + * Process data variables + * + * @param array | \Illuminate\Support\Collection $data + * @return self + */ public function processData($data): self { @@ -187,33 +226,33 @@ class TemplateService $contents = $this->document->getElementsByTagName('ninja'); foreach ($contents as $content) { - + $template = $content->ownerDocument->saveHTML($content); try { $template = $this->twig->createTemplate(html_entity_decode($template)); } catch(SyntaxError $e) { nlog($e->getMessage()); - continue; + throw ($e); } catch(Error $e) { - nlog("error = " .$e->getMessage()); - continue; + nlog("error = " . $e->getMessage()); + throw ($e); } catch(RuntimeError $e) { - nlog("runtime = " .$e->getMessage()); - continue; + nlog("runtime = " . $e->getMessage()); + throw ($e); } catch(LoaderError $e) { nlog("loader = " . $e->getMessage()); - continue; + throw ($e); } catch(SecurityError $e) { nlog("security = " . $e->getMessage()); - continue; + throw ($e); } $template = $template->render($this->data); $f = $this->document->createDocumentFragment(); $f->appendXML(html_entity_decode($template)); - + $replacements[] = $f; } @@ -227,19 +266,18 @@ class TemplateService return $this; } - + /** * Parses all variables in the document * * @return self */ - private function parseVariables(): self + public function parseVariables(): self { $html = $this->getHtml(); foreach($this->variables as $key => $variable) { - if(isset($variable['labels']) && isset($variable['values'])) { $html = strtr($html, $variable['labels']); $html = strtr($html, $variable['values']); @@ -251,7 +289,7 @@ class TemplateService return $this; } - + /** * Saves the document and updates the compiled string. * @@ -286,7 +324,7 @@ class TemplateService return $this; } - + /** * Inject the template components * manually @@ -312,15 +350,16 @@ class TemplateService * Resolves the labels and values needed to replace the string * holders in the template. * + * @param array $data * @return array */ private function resolveHtmlEngine(array $data): array { return collect($data)->map(function ($value, $key) { - + $processed = []; - if(in_array($key, ['tasks','projects','aging']) || !$value->first()) { + if(in_array($key, ['tasks', 'projects', 'aging']) || !$value->first()) { return $processed; } @@ -336,13 +375,20 @@ class TemplateService 'aging' => $processed = [], default => $processed = [], }; - + return $processed; })->toArray(); } + /** + * Pre Processes the Data Blocks into + * Twig consumables + * + * @param array | \Illuminate\Support\Collection $data + * @return array + */ private function preProcessDataBlocks($data): array { return collect($data)->map(function ($value, $key) { @@ -366,13 +412,20 @@ class TemplateService })->toArray(); } + /** + * Process Invoices into consumable form for Twig templates + * + * @param array | \Illuminate\Support\Collection $invoices + * @return array + */ public function processInvoices($invoices): array { $invoices = collect($invoices) ->map(function ($invoice) { $payments = []; - + $this->entity = $invoice; + if($invoice->payments ?? false) { $payments = $invoice->payments->map(function ($payment) { return $this->transformPayment($payment); @@ -382,6 +435,8 @@ class TemplateService return [ 'amount' => Number::formatMoney($invoice->amount, $invoice->client), 'balance' => Number::formatMoney($invoice->balance, $invoice->client), + 'status_id' => $invoice->status_id, + 'status' => Invoice::stringStatus($invoice->status_id), 'balance_raw' => $invoice->balance, 'number' => $invoice->number ?: '', 'discount' => $invoice->discount, @@ -419,7 +474,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4, - 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client): (array) [], + 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client) : (array) [], 'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()), @@ -443,9 +498,16 @@ class TemplateService } - public function padLineItems(array $items, Client $client): array + /** + * Pads Line Items with raw and formatted content + * + * @param array $items + * @param Vendor | Client $client_or_vendor + * @return array + */ + public function padLineItems(array $items, Vendor | Client $client_or_vendor): array { - return collect($items)->map(function ($item) use ($client) { + return collect($items)->map(function ($item) use ($client_or_vendor) { $item->cost_raw = $item->cost ?? 0; $item->discount_raw = $item->discount ?? 0; @@ -454,27 +516,35 @@ class TemplateService $item->tax_amount_raw = $item->tax_amount ?? 0; $item->product_cost_raw = $item->product_cost ?? 0; - $item->cost = Number::formatMoney($item->cost_raw, $client); - + $item->cost = Number::formatMoney($item->cost_raw, $client_or_vendor); + if($item->is_amount_discount) { - $item->discount = Number::formatMoney($item->discount_raw, $client); + $item->discount = Number::formatMoney($item->discount_raw, $client_or_vendor); } - - $item->line_total = Number::formatMoney($item->line_total_raw, $client); - $item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client); - $item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client); - $item->product_cost = Number::formatMoney($item->product_cost_raw, $client); + + $item->line_total = Number::formatMoney($item->line_total_raw, $client_or_vendor); + $item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client_or_vendor); + $item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client_or_vendor); + $item->product_cost = Number::formatMoney($item->product_cost_raw, $client_or_vendor); return $item; })->toArray(); } + /** + * Transforms a Payment into consumable for twig + * + * @param Payment $payment + * @return array + */ private function transformPayment(Payment $payment): array { $data = []; - + + $this->payment = $payment; + $credits = $payment->credits->map(function ($credit) use ($payment) { return [ 'credit' => $credit->number, @@ -522,7 +592,7 @@ class TemplateService 'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied), 'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()), 'method' => $payment->translatedType(), - 'currency' => $payment->currency->code ?? $payment->company->currency()->code, + 'currency' => $payment->currency->code, 'exchange_rate' => $payment->exchange_rate, 'transaction_reference' => $payment->transaction_reference, 'is_manual' => $payment->is_manual, @@ -543,8 +613,6 @@ class TemplateService 'refund_activity' => $this->getPaymentRefundActivity($payment), ]; - nlog($data); - return $data; } @@ -596,31 +664,83 @@ class TemplateService } + /** + * + * + * @param array | \Illuminate\Support\Collection $quotes + * @return array + */ public function processQuotes($quotes): array { - $it = new QuoteTransformer(); - $it->setDefaultIncludes(['client']); - $manager = new Manager(); - $manager->parseIncludes(['client']); - $resource = new \League\Fractal\Resource\Collection($quotes, $it, null); - $resources = $manager->createData($resource)->toArray(); + + return collect($quotes)->map(function ($quote) { - foreach($resources['data'] as $key => $resource) { + return [ + 'amount' => Number::formatMoney($quote->amount, $quote->client), + 'balance' => Number::formatMoney($quote->balance, $quote->client), + 'balance_raw' => (float) $quote->balance, + 'client' => [ + 'name' => $quote->client->present()->name(), + 'balance' => $quote->client->balance, + 'payment_balance' => $quote->client->payment_balance, + 'credit_balance' => $quote->client->credit_balance, + ], + 'status_id' =>$quote->status_id, + 'status' => Quote::stringStatus($quote->status_id), + 'number' => $quote->number ?: '', + 'discount' => (float) $quote->discount, + 'po_number' => $quote->po_number ?: '', + 'date' => $quote->date ? $this->translateDate($quote->date, $quote->client->date_format(), $quote->client->locale()) : '', + 'last_sent_date' => $quote->last_sent_date ? $this->translateDate($quote->last_sent_date, $quote->client->date_format(), $quote->client->locale()) : '', + // 'next_send_date' => $quote->next_send_date ?: '', + // 'reminder1_sent' => $quote->reminder1_sent ?: '', + // 'reminder2_sent' => $quote->reminder2_sent ?: '', + // 'reminder3_sent' => $quote->reminder3_sent ?: '', + // 'reminder_last_sent' => $quote->reminder_last_sent ?: '', + 'due_date' => $quote->due_date ? $this->translateDate($quote->due_date, $quote->client->date_format(), $quote->client->locale()) : '', + 'terms' => $quote->terms ?: '', + 'public_notes' => $quote->public_notes ?: '', + 'private_notes' => $quote->private_notes ?: '', + 'is_deleted' => (bool) $quote->is_deleted, + 'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes, + 'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '', + 'tax_rate1' => (float) $quote->tax_rate1, + 'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '', + 'tax_rate2' => (float) $quote->tax_rate2, + 'tax_name3' => $quote->tax_name3 ? $quote->tax_name3 : '', + 'tax_rate3' => (float) $quote->tax_rate3, + 'total_taxes' => (float) $quote->total_taxes, + 'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false), + 'footer' => $quote->footer ?: '', + 'partial' => (float) ($quote->partial ?: 0.0), + 'partial_due_date' => $quote->partial_due_date ? $this->translateDate($quote->partial_due_date, $quote->client->date_format(), $quote->client->locale()) : '', + 'custom_value1' => (string) $quote->custom_value1 ?: '', + 'custom_value2' => (string) $quote->custom_value2 ?: '', + 'custom_value3' => (string) $quote->custom_value3 ?: '', + 'custom_value4' => (string) $quote->custom_value4 ?: '', + 'has_expenses' => (bool) $quote->has_expenses, + 'custom_surcharge1' => (float) $quote->custom_surcharge1, + 'custom_surcharge2' => (float) $quote->custom_surcharge2, + 'custom_surcharge3' => (float) $quote->custom_surcharge3, + 'custom_surcharge4' => (float) $quote->custom_surcharge4, + 'custom_surcharge_tax1' => (bool) $quote->custom_surcharge_tax1, + 'custom_surcharge_tax2' => (bool) $quote->custom_surcharge_tax2, + 'custom_surcharge_tax3' => (bool) $quote->custom_surcharge_tax3, + 'custom_surcharge_tax4' => (bool) $quote->custom_surcharge_tax4, + 'line_items' => $quote->line_items ? $this->padLineItems($quote->line_items, $quote->client) : (array) [], + 'exchange_rate' => (float) $quote->exchange_rate, + 'paid_to_date' => (float) $quote->paid_to_date, + ]; - $resources['data'][$key]['client'] = $resource['client']['data'] ?? []; - $resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? []; - - } - - return $resources['data']; + })->toArray(); } - + /** * Pushes credits through the appropriate transformer * and builds any required relationships * - * @param mixed $credits + * @param array | \Illuminate\Support\Collection $credits * @return array */ public function processCredits($credits): array @@ -628,6 +748,8 @@ class TemplateService $credits = collect($credits) ->map(function ($credit) { + $this->entity = $credit; + return [ 'amount' => Number::formatMoney($credit->amount, $credit->client), 'balance' => Number::formatMoney($credit->balance, $credit->client), @@ -668,7 +790,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $credit->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $credit->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $credit->custom_surcharge_tax4, - 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client): (array) [], + 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client) : (array) [], 'reminder1_sent' => $this->translateDate($credit->reminder1_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder2_sent' => $this->translateDate($credit->reminder2_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder3_sent' => $this->translateDate($credit->reminder3_sent, $credit->client->date_format(), $credit->client->locale()), @@ -692,12 +814,10 @@ class TemplateService } - - /** * Pushes payments through the appropriate transformer * - * @param mixed $payments + * @param array | \Illuminate\Support\Collection $payments * @return array */ public function processPayments($payments): array @@ -706,77 +826,626 @@ class TemplateService $payments = collect($payments)->map(function ($payment) { return $this->transformPayment($payment); })->toArray(); - + return $payments; - } - public function processTasks($tasks): array + /** + * @todo refactor + * + * @param mixed $tasks + * @return array + */ + public function processTasks($tasks, bool $nested = false): array { - $it = new TaskTransformer(); - $it->setDefaultIncludes(['client','project','invoice']); - $manager = new Manager(); - $resource = new \League\Fractal\Resource\Collection($tasks, $it, null); - $resources = $manager->createData($resource)->toArray(); - foreach($resources['data'] as $key => $resource) { + return collect($tasks)->map(function ($task) use ($nested) { - $resources['data'][$key]['client'] = $resource['client']['data'] ?? []; - $resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? []; - $resources['data'][$key]['project'] = $resource['project']['data'] ?? []; - $resources['data'][$key]['invoice'] = $resource['invoice'] ?? []; - - } + return [ + 'number' => (string) $task->number ?: '', + 'description' => (string) $task->description ?: '', + 'duration' => $task->duration ?: 0, + 'rate' => Number::formatMoney($task->rate ?? 0, $task->client ?? $task->company), + 'created_at' => $this->translateDate($task->created_at, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()), + 'updated_at' => $this->translateDate($task->updated_at, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()), + 'date' => $task->calculated_start_date ? $this->translateDate($task->calculated_start_date, $task->client ? $task->client->date_format() : $task->company->date_format(), $task->client ? $task->client->locale() : $task->company->locale()) : '', + // 'invoice_id' => $this->encodePrimaryKey($task->invoice_id) ?: '', + 'project' => ($task->project && !$nested) ? $this->transformProject($task->project, true) : [], + 'time_log' => $task->processLogs(), + 'custom_value1' => $task->custom_value1 ?: '', + 'custom_value2' => $task->custom_value2 ?: '', + 'custom_value3' => $task->custom_value3 ?: '', + 'custom_value4' => $task->custom_value4 ?: '', + 'status' => $task->status ? $task->status->name : '', + 'client' => $task->client ? [ + 'name' => $task->client->present()->name(), + 'balance' => $task->client->balance, + 'payment_balance' => $task->client->payment_balance, + 'credit_balance' => $task->client->credit_balance, + ] : [], + ]; - return $resources['data']; + })->toArray(); } + /** + * @todo refactor + * + * @param array | \Illuminate\Support\Collection $projects + * @return array + */ public function processProjects($projects): array { - $it = new ProjectTransformer(); - $it->setDefaultIncludes(['client','tasks']); - $manager = new Manager(); - $manager->setSerializer(new ArraySerializer()); - $resource = new \League\Fractal\Resource\Collection($projects, $it, Project::class); - $i = $manager->createData($resource)->toArray(); - return $i[Project::class]; + return + collect($projects)->map(function ($project) { + + return $this->transformProject($project); + + })->toArray(); } - public function processPurchaseOrders($purchase_orders): array + private function transformProject(Project $project, bool $nested = false): array { - $it = new PurchaseOrderTransformer(); - $it->setDefaultIncludes(['vendor','expense']); - $manager = new Manager(); - $manager->setSerializer(new ArraySerializer()); - $resource = new \League\Fractal\Resource\Collection($purchase_orders, $it, PurchaseOrder::class); - $i = $manager->createData($resource)->toArray(); - return $i[PurchaseOrder::class]; + return [ + 'name' => $project->name ?: '', + 'number' => $project->number ?: '', + 'created_at' => $this->translateDate($project->created_at, $project->client->date_format(), $project->client->locale()), + 'updated_at' => $this->translateDate($project->updated_at, $project->client->date_format(), $project->client->locale()), + 'task_rate' => Number::formatMoney($project->task_rate ?? 0, $project->client), + 'due_date' => $project->due_date ? $this->translateDate($project->due_date, $project->client->date_format(), $project->client->locale()) : '', + 'private_notes' => (string) $project->private_notes ?: '', + 'public_notes' => (string) $project->public_notes ?: '', + 'budgeted_hours' => (float) $project->budgeted_hours, + 'custom_value1' => (string) $project->custom_value1 ?: '', + 'custom_value2' => (string) $project->custom_value2 ?: '', + 'custom_value3' => (string) $project->custom_value3 ?: '', + 'custom_value4' => (string) $project->custom_value4 ?: '', + 'color' => (string) $project->color ?: '', + 'current_hours' => (int) $project->current_hours ?: 0, + 'tasks' => ($project->tasks && !$nested) ? $this->processTasks($project->tasks, true) : [], + 'client' => $project->client ? [ + 'name' => $project->client->present()->name(), + 'balance' => $project->client->balance, + 'payment_balance' => $project->client->payment_balance, + 'credit_balance' => $project->client->credit_balance, + ] : [], + + ]; } + /** + * + * @param array | \Illuminate\Support\Collection $purchase_orders + * @return array + */ + public function processPurchaseOrders($purchase_orders): array + { + + return collect($purchase_orders)->map(function ($purchase_order) { + + return [ + 'vendor' => $purchase_order->vendor ? [ + 'name' => $purchase_order->vendor->present()->name(), + ] : [], + 'amount' => (float)$purchase_order->amount, + 'balance' => (float)$purchase_order->balance, + 'client' => $purchase_order->client ? [ + 'name' => $purchase_order->client->present()->name(), + 'balance' => $purchase_order->client->balance, + 'payment_balance' => $purchase_order->client->payment_balance, + 'credit_balance' => $purchase_order->client->credit_balance, + ] : [], + 'status_id' => (string)($purchase_order->status_id ?: 1), + 'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1), + 'is_deleted' => (bool)$purchase_order->is_deleted, + 'number' => $purchase_order->number ?: '', + 'discount' => (float)$purchase_order->discount, + 'po_number' => $purchase_order->po_number ?: '', + 'date' => $purchase_order->date ? $this->translateDate($purchase_order->date, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'last_sent_date' => $purchase_order->last_sent_date ? $this->translateDate($purchase_order->last_sent_date, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'next_send_date' => $purchase_order->next_send_date ? $this->translateDate($purchase_order->next_send_date, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'reminder1_sent' => $purchase_order->reminder1_sent ? $this->translateDate($purchase_order->reminder1_sent, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'reminder2_sent' => $purchase_order->reminder2_sent ? $this->translateDate($purchase_order->reminder2_sent, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'reminder3_sent' => $purchase_order->reminder3_sent ? $this->translateDate($purchase_order->reminder3_sent, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'reminder_last_sent' => $purchase_order->reminder_last_sent ? $this->translateDate($purchase_order->reminder_last_sent, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'due_date' => $purchase_order->due_date ? $this->translateDate($purchase_order->due_date, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()) : '', + 'terms' => $purchase_order->terms ?: '', + 'public_notes' => $purchase_order->public_notes ?: '', + 'private_notes' => $purchase_order->private_notes ?: '', + 'uses_inclusive_taxes' => (bool)$purchase_order->uses_inclusive_taxes, + 'tax_name1' => $purchase_order->tax_name1 ? $purchase_order->tax_name1 : '', + 'tax_rate1' => (float)$purchase_order->tax_rate1, + 'tax_name2' => $purchase_order->tax_name2 ? $purchase_order->tax_name2 : '', + 'tax_rate2' => (float)$purchase_order->tax_rate2, + 'tax_name3' => $purchase_order->tax_name3 ? $purchase_order->tax_name3 : '', + 'tax_rate3' => (float)$purchase_order->tax_rate3, + 'total_taxes' => (float)$purchase_order->total_taxes, + 'is_amount_discount' => (bool)($purchase_order->is_amount_discount ?: false), + 'footer' => $purchase_order->footer ?: '', + 'partial' => (float)($purchase_order->partial ?: 0.0), + 'partial_due_date' => $purchase_order->partial_due_date ? $this->translateDate($purchase_order->partial_due_date, $purchase_order->vendor->date_format(), $purchase_order->vendor->locale()): '', + 'custom_value1' => (string)$purchase_order->custom_value1 ?: '', + 'custom_value2' => (string)$purchase_order->custom_value2 ?: '', + 'custom_value3' => (string)$purchase_order->custom_value3 ?: '', + 'custom_value4' => (string)$purchase_order->custom_value4 ?: '', + 'has_tasks' => (bool)$purchase_order->has_tasks, + 'has_expenses' => (bool)$purchase_order->has_expenses, + 'custom_surcharge1' => (float)$purchase_order->custom_surcharge1, + 'custom_surcharge2' => (float)$purchase_order->custom_surcharge2, + 'custom_surcharge3' => (float)$purchase_order->custom_surcharge3, + 'custom_surcharge4' => (float)$purchase_order->custom_surcharge4, + 'custom_surcharge_tax1' => (bool)$purchase_order->custom_surcharge_tax1, + 'custom_surcharge_tax2' => (bool)$purchase_order->custom_surcharge_tax2, + 'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3, + 'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4, + 'line_items' => $purchase_order->line_items ? $this->padLineItems($purchase_order->line_items, $purchase_order->vendor): (array)[], + 'exchange_rate' => (float)$purchase_order->exchange_rate, + 'currency_id' => $purchase_order->currency_id ? (string) $purchase_order->currency_id : '', + ]; + + })->toArray(); + + } + + /** + * Set Company + * + * @param Company $company + * @return self + */ public function setCompany(Company $company): self { $this->company = $company; - + return $this; } + /** + * Get Company + * + * @return Company + */ public function getCompany(): Company { return $this->company; } + /** + * Setter that allows external variables to override the + * resolved ones from this class + * + * @param mixed $variables + * @return self + */ public function overrideVariables($variables): self { $this->variables = $variables; + + return $this; + } + + /** + * Parses and finds any field stacks to inject into the DOM Document + * + * @return self + */ + public function parseGlobalStacks(): self + { + $stacks = [ + 'entity-details', + 'client-details', + 'vendor-details', + 'company-details', + 'company-address', + 'shipping-details', + ]; + + collect($stacks)->filter(function ($stack) { + return $this->document->getElementById($stack) ?? false; + }) + ->map(function ($stack) { + $node = $this->document->getElementById($stack); + return ['stack' => $stack, 'labels' => $node->getAttribute('labels')]; + }) + ->each(function ($stack) { + $this->parseStack($stack); + }); + + return $this; + + } + + /** + * Injects field stacks into Template + * + * @param array $stack + * @return self + */ + private function parseStack(array $stack): self + { + + match($stack['stack']) { + 'entity-details' => $this->entityDetails(), + 'client-details' => $this->clientDetails($stack['labels'] == 'true'), + 'vendor-details' => $this->vendorDetails($stack['labels'] == 'true'), + 'company-details' => $this->companyDetails($stack['labels'] == 'true'), + 'company-address' => $this->companyAddress($stack['labels'] == 'true'), + 'shipping-details' => $this->shippingDetails($stack['labels'] == 'true'), + }; + + $this->save(); + + return $this; + } + + /** + * Inject the Company Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function companyDetails(bool $include_labels): self + { + $var_set = $this->getVarSet(); + + $company_details = + collect($this->company->settings->pdf_variables->company_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + // nlog($company_details); + + $company_details = $include_labels ? $this->labelledFieldStack($company_details, 'company_details-') : $company_details; + + // nlog($company_details); + + $this->updateElementProperties('company-details', $company_details); + + return $this; + } + + private function companyAddress(bool $include_labels = false): self + { + + $var_set = $this->getVarSet(); + + $company_address = + collect($this->company->settings->pdf_variables->company_address) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]]; + }); + })->toArray(); + + $company_address = $include_labels ? $this->labelledFieldStack($company_address, 'company_address-') : $company_address; + + $this->updateElementProperties('company-address', $company_address); + + return $this; + } + + /** + * Injects the Shipping Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function shippingDetails(bool $include_labels = false): self + { + if(!$this->entity->client) { + return $this; + } + + $this->client = $this->entity->client; + + $shipping_address = [ + ['element' => 'p', 'content' => ctrans('texts.shipping_address'), 'properties' => ['data-ref' => 'shipping_address-label', 'style' => 'font-weight: bold; text-transform: uppercase']], + ['element' => 'p', 'content' => $this->client->name, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.name']], + ['element' => 'p', 'content' => $this->client->shipping_address1, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.shipping_address1']], + ['element' => 'p', 'content' => $this->client->shipping_address2, 'show_empty' => false, 'properties' => ['data-ref' => 'shipping_address-client.shipping_address2']], + ['element' => 'p', 'show_empty' => false, 'elements' => [ + ['element' => 'span', 'content' => "{$this->client->shipping_city} ", 'properties' => ['ref' => 'shipping_address-client.shipping_city']], + ['element' => 'span', 'content' => "{$this->client->shipping_state} ", 'properties' => ['ref' => 'shipping_address-client.shipping_state']], + ['element' => 'span', 'content' => "{$this->client->shipping_postal_code} ", 'properties' => ['ref' => 'shipping_address-client.shipping_postal_code']], + ]], + ['element' => 'p', 'content' => optional($this->client->shipping_country)->name, 'show_empty' => false], + ]; + + $shipping_address = + collect($shipping_address)->filter(function ($address) { + return isset($address['content']) && !empty($address['content']); + })->toArray(); + + $this->updateElementProperties('shipping-details', $shipping_address); + + return $this; + } + + /** + * Injects the Client Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function clientDetails(bool $include_labels = false): self + { + $var_set = $this->getVarSet(); + + $client_details = + collect($this->company->settings->pdf_variables->client_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + }) + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + $client_details = $include_labels ? $this->labelledFieldStack($client_details, 'client_details-') : $client_details; + + $this->updateElementProperties('client-details', $client_details); + + return $this; + } + + /** + * Resolves the entity. + * + * Only required for resolving the entity-details stack + * + * @return string + */ + private function resolveEntity(): string + { + $entity_string = ''; + + match($this->entity) { + ($this->entity instanceof Invoice) => $entity_string = 'invoice', + ($this->entity instanceof Quote) => $entity_string = 'quote', + ($this->entity instanceof Credit) => $entity_string = 'credit', + ($this->entity instanceof RecurringInvoice) => $entity_string = 'invoice', + ($this->entity instanceof PurchaseOrder) => $entity_string = 'purchase_order', + default => $entity_string = 'invoice', + }; + + return $entity_string; + + } + + /** + * Returns the variable array by first key, if it exists + * + * @return array + */ + private function getVarSet(): array + { + return array_key_exists(array_key_first($this->variables), $this->variables) ? $this->variables[array_key_first($this->variables)] : $this->variables; + } + + /** + * Injects the entity details to the DOM document + * + * @return self + */ + private function entityDetails(): self + { + $entity_string = $this->resolveEntity(); + $entity_string_prop = "{$entity_string}_details"; + $var_set = $this->getVarSet(); + + $entity_details = + collect($this->company->settings->pdf_variables->{$entity_string_prop}) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + })->toArray(); + + $this->updateElementProperties("entity-details", $this->labelledFieldStack($entity_details, 'entity_details-')); + + return $this; + } + + /** + * Generates the field stacks with labels + * + * @param array $variables + * @return array + */ + private function labelledFieldStack(array $variables, string $data_ref): array + { + + $elements = []; + + foreach ($variables as $variable) { + $_variable = explode('.', $variable)[1]; + $_customs = ['custom1', 'custom2', 'custom3', 'custom4']; + + $var = str_replace("custom", "custom_value", $_variable); + + $hidden_prop = ($data_ref == 'entity_details-') ? $this->entityVariableCheck($variable) : false; + + if (in_array($_variable, $_customs) && !empty($this->entity->{$var})) { + $elements[] = ['element' => 'tr', 'elements' => [ + ['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => $data_ref . substr($variable, 1) . '_label']], + ['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => $data_ref . substr($variable, 1)]], + ]]; + } else { + $elements[] = ['element' => 'tr', 'properties' => ['hidden' => $hidden_prop], 'elements' => [ + ['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => $data_ref . substr($variable, 1) . '_label']], + ['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => $data_ref . substr($variable, 1)]], + ]]; + } + } + + return $elements; + + } + + /** + * Inject Vendor Details into DOM Document + * + * @param bool $include_labels + * @return self + */ + private function vendorDetails(bool $include_labels = false): self + { + + $var_set = $this->getVarSet(); + + $vendor_details = + collect($this->company->settings->pdf_variables->vendor_details) + ->filter(function ($variable) use ($var_set) { + return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); + })->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + }); + })->toArray(); + + $vendor_details = $include_labels ? $this->labelledFieldStack($vendor_details, 'vendor_details-') : $vendor_details; + + $this->updateElementProperties('vendor-details', $vendor_details); + + return $this; + } + + + /** + * Performs a variable check to ensure + * the variable exists + * + * @param string $variable + * @return bool + * + */ + public function entityVariableCheck(string $variable): bool + { + // When it comes to invoice balance, we'll always show it. + if ($variable == '$invoice.total') { + return false; + } + + // Some variables don't map 1:1 to table columns. This gives us support for such cases. + $aliases = [ + '$quote.balance_due' => 'partial', + ]; + + try { + $_variable = explode('.', $variable)[1]; + } catch (\Exception $e) { + throw new \Exception('Company settings seems to be broken. Missing $this->service->config->entity.variable type.'); + } + + if (\in_array($variable, \array_keys($aliases))) { + $_variable = $aliases[$variable]; + } + + if (is_null($this->entity->{$_variable}) || empty($this->entity->{$_variable})) { + return true; + } + + return false; + } + + //////////////////////////////////////// + // Dom Traversal + /////////////////////////////////////// + + public function updateElementProperties(string $element_id, array $elements): self + { + $node = $this->document->getElementById($element_id); + + $this->createElementContent($node, $elements); + + return $this; + } + + public function updateElementProperty($element, string $attribute, ?string $value) + { + + if ($attribute == 'hidden' && ($value == false || $value == 'false')) { + return $element; + } + + $element->setAttribute($attribute, $value); + + if ($element->getAttribute($attribute) === $value) { + return $element; + } + + return $element; + + } + + public function createElementContent($element, $children): self + { + + foreach ($children as $child) { + $contains_html = false; + + //06-11-2023 for some reason this parses content as HTML + // if ($child['element'] !== 'script') { + // if ($this->company->markdown_enabled && array_key_exists('content', $child)) { + // $child['content'] = str_replace('
', "\r", $child['content']); + // $child['content'] = $this->commonmark->convert($child['content'] ?? ''); + // } + // } + + if (isset($child['content'])) { + if (isset($child['is_empty']) && $child['is_empty'] === true) { + continue; + } + + $contains_html = preg_match('#(?<=<)\w+(?=[^<]*?>)#', $child['content'], $m) != 0; + } + + if ($contains_html) { + // If the element contains the HTML, we gonna display it as is. Backend is going to + // encode it for us, preventing any errors on the processing stage. + // Later, we decode this using Javascript so it looks like it's normal HTML being injected. + // To get all elements that need frontend decoding, we use 'data-state' property. + + $_child = $this->document->createElement($child['element'], ''); + $_child->setAttribute('data-state', 'encoded-html'); + $_child->nodeValue = htmlspecialchars($child['content']); + } else { + // .. in case string doesn't contain any HTML, we'll just return + // raw $content. + + $_child = $this->document->createElement($child['element'], isset($child['content']) ? htmlspecialchars($child['content']) : ''); + } + + $element->appendChild($_child); + + if (isset($child['properties'])) { + foreach ($child['properties'] as $property => $value) { + $this->updateElementProperty($_child, $property, $value); + } + } + + if (isset($child['elements'])) { + $this->createElementContent($_child, $child['elements']); + } + + } return $this; } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 630eb189ccfd..d58e3cc15502 100644 --- a/composer.lock +++ b/composer.lock @@ -485,16 +485,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.285.3", + "version": "3.285.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "afa1e722f1b2c95644f375dc1a19072e4daf67be" + "reference": "c462af819d81cba49939949032b20799f5ef0fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/afa1e722f1b2c95644f375dc1a19072e4daf67be", - "reference": "afa1e722f1b2c95644f375dc1a19072e4daf67be", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c462af819d81cba49939949032b20799f5ef0fff", + "reference": "c462af819d81cba49939949032b20799f5ef0fff", "shasum": "" }, "require": { @@ -574,9 +574,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.285.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.285.4" }, - "time": "2023-11-09T19:07:19+00:00" + "time": "2023-11-10T19:25:49+00:00" }, { "name": "bacon/bacon-qr-code", @@ -790,16 +790,16 @@ }, { "name": "checkout/checkout-sdk-php", - "version": "3.0.17", + "version": "3.0.18", "source": { "type": "git", "url": "https://github.com/checkout/checkout-sdk-php.git", - "reference": "dabb6dd37ad80aaa9c34e60f48f9bf8b651bdc27" + "reference": "9e606ac8ad5371cfb571050e7ea2c0c05b2b3070" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/dabb6dd37ad80aaa9c34e60f48f9bf8b651bdc27", - "reference": "dabb6dd37ad80aaa9c34e60f48f9bf8b651bdc27", + "url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/9e606ac8ad5371cfb571050e7ea2c0c05b2b3070", + "reference": "9e606ac8ad5371cfb571050e7ea2c0c05b2b3070", "shasum": "" }, "require": { @@ -852,9 +852,9 @@ ], "support": { "issues": "https://github.com/checkout/checkout-sdk-php/issues", - "source": "https://github.com/checkout/checkout-sdk-php/tree/3.0.17" + "source": "https://github.com/checkout/checkout-sdk-php/tree/3.0.18" }, - "time": "2023-10-20T22:35:30+00:00" + "time": "2023-11-10T09:12:20+00:00" }, { "name": "cleverit/ubl_invoice", @@ -2487,16 +2487,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.323.0", + "version": "v0.324.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "d5497d30ddfafe7592102ca48bedaf222a4ca7a6" + "reference": "585cc823c3d59788e4a0829d5b7e41c76950d801" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/d5497d30ddfafe7592102ca48bedaf222a4ca7a6", - "reference": "d5497d30ddfafe7592102ca48bedaf222a4ca7a6", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/585cc823c3d59788e4a0829d5b7e41c76950d801", + "reference": "585cc823c3d59788e4a0829d5b7e41c76950d801", "shasum": "" }, "require": { @@ -2525,9 +2525,9 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.323.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.324.0" }, - "time": "2023-11-06T01:08:38+00:00" + "time": "2023-11-13T01:06:14+00:00" }, { "name": "google/auth", @@ -2589,24 +2589,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.1", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.1" + "phpoption/phpoption": "^1.9.2" }, "require-dev": { - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "autoload": { @@ -2635,7 +2635,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" }, "funding": [ { @@ -2647,7 +2647,7 @@ "type": "tidelift" } ], - "time": "2023-02-25T20:23:15+00:00" + "time": "2023-11-12T22:16:48+00:00" }, { "name": "graylog2/gelf-php", @@ -7832,16 +7832,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.1", + "version": "1.9.2", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", "shasum": "" }, "require": { @@ -7849,7 +7849,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "extra": { @@ -7891,7 +7891,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" }, "funding": [ { @@ -7903,7 +7903,7 @@ "type": "tidelift" } ], - "time": "2023-02-25T19:38:58+00:00" + "time": "2023-11-12T21:59:55+00:00" }, { "name": "phpseclib/phpseclib", @@ -10288,16 +10288,16 @@ }, { "name": "symfony/console", - "version": "v6.3.4", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", "shasum": "" }, "require": { @@ -10358,7 +10358,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.4" + "source": "https://github.com/symfony/console/tree/v6.3.8" }, "funding": [ { @@ -10374,7 +10374,7 @@ "type": "tidelift" } ], - "time": "2023-08-16T10:10:12+00:00" + "time": "2023-10-31T08:09:35+00:00" }, { "name": "symfony/css-selector", @@ -10867,16 +10867,16 @@ }, { "name": "symfony/http-client", - "version": "v6.3.7", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "cd67fcaf4524ec6ae5d9b2d9497682d7ad3ce57d" + "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/cd67fcaf4524ec6ae5d9b2d9497682d7ad3ce57d", - "reference": "cd67fcaf4524ec6ae5d9b2d9497682d7ad3ce57d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/0314e2d49939a9831929d6fc81c01c6df137fd0a", + "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a", "shasum": "" }, "require": { @@ -10939,7 +10939,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.3.7" + "source": "https://github.com/symfony/http-client/tree/v6.3.8" }, "funding": [ { @@ -10955,7 +10955,7 @@ "type": "tidelift" } ], - "time": "2023-10-29T12:41:36+00:00" + "time": "2023-11-06T18:31:59+00:00" }, { "name": "symfony/http-client-contracts", @@ -11037,16 +11037,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.3.7", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "59d1837d5d992d16c2628cd0d6b76acf8d69b33e" + "reference": "ce332676de1912c4389222987193c3ef38033df6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/59d1837d5d992d16c2628cd0d6b76acf8d69b33e", - "reference": "59d1837d5d992d16c2628cd0d6b76acf8d69b33e", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce332676de1912c4389222987193c3ef38033df6", + "reference": "ce332676de1912c4389222987193c3ef38033df6", "shasum": "" }, "require": { @@ -11094,7 +11094,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.3.7" + "source": "https://github.com/symfony/http-foundation/tree/v6.3.8" }, "funding": [ { @@ -11110,20 +11110,20 @@ "type": "tidelift" } ], - "time": "2023-10-28T23:55:27+00:00" + "time": "2023-11-07T10:17:15+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.3.7", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6d4098095f93279d9536a0e9124439560cc764d0" + "reference": "929202375ccf44a309c34aeca8305408442ebcc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6d4098095f93279d9536a0e9124439560cc764d0", - "reference": "6d4098095f93279d9536a0e9124439560cc764d0", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/929202375ccf44a309c34aeca8305408442ebcc1", + "reference": "929202375ccf44a309c34aeca8305408442ebcc1", "shasum": "" }, "require": { @@ -11207,7 +11207,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.3.7" + "source": "https://github.com/symfony/http-kernel/tree/v6.3.8" }, "funding": [ { @@ -11223,7 +11223,7 @@ "type": "tidelift" } ], - "time": "2023-10-29T14:31:45+00:00" + "time": "2023-11-10T13:47:32+00:00" }, { "name": "symfony/intl", @@ -12819,16 +12819,16 @@ }, { "name": "symfony/string", - "version": "v6.3.5", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339" + "reference": "13880a87790c76ef994c91e87efb96134522577a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339", - "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", "shasum": "" }, "require": { @@ -12885,7 +12885,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.5" + "source": "https://github.com/symfony/string/tree/v6.3.8" }, "funding": [ { @@ -12901,7 +12901,7 @@ "type": "tidelift" } ], - "time": "2023-09-18T10:38:32+00:00" + "time": "2023-11-09T08:28:21+00:00" }, { "name": "symfony/translation", @@ -13078,16 +13078,16 @@ }, { "name": "symfony/uid", - "version": "v6.3.0", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384" + "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/01b0f20b1351d997711c56f1638f7a8c3061e384", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384", + "url": "https://api.github.com/repos/symfony/uid/zipball/819fa5ac210fb7ddda4752b91a82f50be7493dd9", + "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9", "shasum": "" }, "require": { @@ -13132,7 +13132,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.3.0" + "source": "https://github.com/symfony/uid/tree/v6.3.8" }, "funding": [ { @@ -13148,20 +13148,20 @@ "type": "tidelift" } ], - "time": "2023-04-08T07:25:02+00:00" + "time": "2023-10-31T08:07:48+00:00" }, { "name": "symfony/validator", - "version": "v6.3.7", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "9cc736663fa5839b9710ac2c303bb0b951014fc1" + "reference": "f75b40e088d095db1e788b81605a76f4563cb80e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/9cc736663fa5839b9710ac2c303bb0b951014fc1", - "reference": "9cc736663fa5839b9710ac2c303bb0b951014fc1", + "url": "https://api.github.com/repos/symfony/validator/zipball/f75b40e088d095db1e788b81605a76f4563cb80e", + "reference": "f75b40e088d095db1e788b81605a76f4563cb80e", "shasum": "" }, "require": { @@ -13228,7 +13228,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.3.7" + "source": "https://github.com/symfony/validator/tree/v6.3.8" }, "funding": [ { @@ -13244,20 +13244,20 @@ "type": "tidelift" } ], - "time": "2023-10-28T23:11:45+00:00" + "time": "2023-11-07T10:17:15+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.6", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "999ede244507c32b8e43aebaa10e9fce20de7c97" + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/999ede244507c32b8e43aebaa10e9fce20de7c97", - "reference": "999ede244507c32b8e43aebaa10e9fce20de7c97", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/81acabba9046550e89634876ca64bfcd3c06aa0a", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a", "shasum": "" }, "require": { @@ -13312,7 +13312,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.6" + "source": "https://github.com/symfony/var-dumper/tree/v6.3.8" }, "funding": [ { @@ -13328,20 +13328,20 @@ "type": "tidelift" } ], - "time": "2023-10-12T18:45:56+00:00" + "time": "2023-11-08T10:42:36+00:00" }, { "name": "symfony/yaml", - "version": "v6.3.7", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "9758b6c69d179936435d0ffb577c3708d57e38a8" + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/9758b6c69d179936435d0ffb577c3708d57e38a8", - "reference": "9758b6c69d179936435d0ffb577c3708d57e38a8", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92", "shasum": "" }, "require": { @@ -13384,7 +13384,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.3.7" + "source": "https://github.com/symfony/yaml/tree/v6.3.8" }, "funding": [ { @@ -13400,7 +13400,7 @@ "type": "tidelift" } ], - "time": "2023-10-28T23:31:00+00:00" + "time": "2023-11-06T10:58:05+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -13703,31 +13703,31 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.5.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.0.2", - "php": "^7.1.3 || ^8.0", - "phpoption/phpoption": "^1.8", - "symfony/polyfill-ctype": "^1.23", - "symfony/polyfill-mbstring": "^1.23.1", - "symfony/polyfill-php80": "^1.23.1" + "graham-campbell/result-type": "^1.1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.2", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "suggest": { "ext-filter": "Required to use the boolean validator." @@ -13739,7 +13739,7 @@ "forward-command": true }, "branch-alias": { - "dev-master": "5.5-dev" + "dev-master": "5.6-dev" } }, "autoload": { @@ -13771,7 +13771,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" }, "funding": [ { @@ -13783,7 +13783,7 @@ "type": "tidelift" } ], - "time": "2022-10-16T01:01:54+00:00" + "time": "2023-11-12T22:43:29+00:00" }, { "name": "voku/portable-ascii", diff --git a/config/ninja.php b/config/ninja.php index edd4d3806be9..ba7c433f033d 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.47'), - 'app_tag' => env('APP_TAG','5.7.47'), + 'app_version' => env('APP_VERSION','5.7.48'), + 'app_tag' => env('APP_TAG','5.7.48'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/routes/client.php b/routes/client.php index 61c0aeb7e6c7..8d2d4d1c56fe 100644 --- a/routes/client.php +++ b/routes/client.php @@ -125,12 +125,12 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('invoice/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'invoiceRouter']); Route::get('quote/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'quoteRouter']); Route::get('credit/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'creditRouter']); - Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key');//->middleware('token_auth'); - Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key');//->middleware('token_auth'); - Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoice.download_e_invoice');//->middleware('token_auth'); - Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key');//->middleware('token_auth'); - Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key');//->middleware('token_auth'); - Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload']);//->middleware('token_auth'); + Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key')->middleware('token_auth'); + Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key')->middleware('token_auth'); + Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoice.download_e_invoice')->middleware('token_auth'); + Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key')->middleware('token_auth'); + Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key')->middleware('token_auth'); + Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload'])->middleware('token_auth'); Route::get('pay/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'payInvoice'])->name('pay.invoice'); Route::get('unsubscribe/{entity}/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'unsubscribe'])->name('unsubscribe'); diff --git a/tests/Feature/EInvoice/FacturaeTest.php b/tests/Feature/EInvoice/FacturaeTest.php index 0770768da14f..2d4053126341 100644 --- a/tests/Feature/EInvoice/FacturaeTest.php +++ b/tests/Feature/EInvoice/FacturaeTest.php @@ -47,7 +47,7 @@ class FacturaeTest extends TestCase $this->assertNotNull($f->run()); - nlog($f->run()); + // nlog($f->run()); // $this->assertTrue($this->validateInvoiceXML($path)); } diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index e93640852cb9..f9ce62a62f34 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -104,6 +104,90 @@ class TaskApiTest extends TestCase } } + public function testEmptyTimeLogArray() + { + + $data = [ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'description' => 'Test Task', + 'time_log' => null, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/tasks", $data); + + $response->assertStatus(200); + + $data = [ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'description' => 'Test Task', + 'time_log' => '', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/tasks", $data); + + $response->assertStatus(200); + + $data = [ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'description' => 'Test Task', + 'time_log' => '[]', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/tasks", $data); + + $response->assertStatus(200); + + $data = [ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'description' => 'Test Task', + 'time_log' => '{}', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/tasks", $data); + + $response->assertStatus(200); + } + + public function testFaultyTimeLogArray() + { + + $data = [ + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'description' => 'Test Task', + 'time_log' => 'ABBA is the best band in the world', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/tasks", $data); + + $response->assertStatus(422); + + } + public function testTaskClientRateSet() { $settings = ClientSettings::defaults(); @@ -282,6 +366,45 @@ class TaskApiTest extends TestCase $response->assertStatus(200); + $task->time_log = 'A very strange place'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/{$task->hashed_id}?stop=true", $task->toArray()); + + $response->assertStatus(422); + + $task->time_log = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/{$task->hashed_id}?stop=true", $task->toArray()); + + $response->assertStatus(200); + + $task->time_log = ''; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/{$task->hashed_id}?stop=true", $task->toArray()); + + $response->assertStatus(200); + + + $task->time_log = '{}'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/{$task->hashed_id}?stop=true", $task->toArray()); + + $response->assertStatus(200); + + + } public function testStoppingTaskWithDescription() diff --git a/tests/Feature/Template/TemplateTest.php b/tests/Feature/Template/TemplateTest.php index df95599ba37a..446362f10076 100644 --- a/tests/Feature/Template/TemplateTest.php +++ b/tests/Feature/Template/TemplateTest.php @@ -18,12 +18,14 @@ use App\Models\Credit; use App\Models\Design; use App\Models\Invoice; use App\Models\Payment; +use App\Models\Project; use App\Utils\HtmlEngine; use Tests\MockAccountData; use App\Utils\Traits\MakesDates; use App\Jobs\Entity\CreateRawPdf; use App\Services\PdfMaker\PdfMaker; use Illuminate\Support\Facades\App; +use App\Services\Template\TemplateMock; use App\Services\Template\TemplateService; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; @@ -165,6 +167,8 @@ class TemplateTest extends TestCase '; + private string $stack = '
'; + protected function setUp() :void { parent::setUp(); @@ -177,6 +181,136 @@ class TemplateTest extends TestCase } + + public function testPurchaseOrderDataParse() + { + $data = []; + + $p = \App\Models\PurchaseOrder::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $this->vendor->id, + ]); + + $data['purchase_orders'][] = $p; + + $ts = new TemplateService(); + $ts->processData($data); + + $this->assertNotNull($ts); + $this->assertIsArray($ts->getData()); + } + + public function testTaskDataParse() + { + $data = []; + + $p = \App\Models\Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + ]); + + $data['tasks'][] = $p; + + $ts = new TemplateService(); + $ts->processData($data); + + $this->assertNotNull($ts); + $this->assertIsArray($ts->getData()); + } + + public function testQuoteDataParse() + { + $data = []; + + $p = \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + ]); + + $data['quotes'][] = $p; + + $ts = new TemplateService(); + $ts->processData($data); + + $this->assertNotNull($ts); + $this->assertIsArray($ts->getData()); + + } + + public function testProjectDataParse() + { + $data = []; + + $p = Project::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + ]); + + $data['projects'][] = $p; + + $ts = new TemplateService(); + $ts->processData($data); + + $this->assertNotNull($ts); + $this->assertIsArray($ts->getData()); + + } + + public function testNegativeDivAttribute() + { + $dom = new \DOMDocument(); + @$dom->loadHTML(mb_convert_encoding($this->stack, 'HTML-ENTITIES', 'UTF-8')); + + $node = $dom->getElementById('company-details'); + $x = $node->getAttribute('nonexistentattribute'); + + $this->assertEquals('', $x); + + } + + public function testStackResolutionWithLabels() + { + + $dom = new \DOMDocument(); + @$dom->loadHTML(mb_convert_encoding($this->stack, 'HTML-ENTITIES', 'UTF-8')); + + $node = $dom->getElementById('company-details'); + $x = $node->getAttribute('labels'); + + $this->assertEquals('true', $x); + + } + + + public function testStackResolution() + { + + $partials['design']['includes'] = ''; + $partials['design']['header'] = ''; + $partials['design']['body'] = $this->stack; + $partials['design']['footer'] = ''; + + $tm = new TemplateMock($this->company); + $tm->init(); + + $variables = $tm->variables[0]; + + $ts = new TemplateService(); + $x = $ts->setTemplate($partials) + ->setCompany($this->company) + ->overrideVariables($variables) + ->parseGlobalStacks() + ->parseVariables() + ->getHtml(); + + $this->assertIsString($x); + + } + public function testDataMaps() { $start = microtime(true); @@ -319,19 +453,8 @@ class TemplateTest extends TestCase ]; }); - $queries = \DB::getQueryLog(); - $count = count($queries); - - nlog("query count = {$count}"); - $x = $invoices->toArray(); - // nlog(json_encode($x)); - // nlog(json_encode(htmlspecialchars(json_encode($x), ENT_QUOTES, 'UTF-8'))); - // nlog($invoices->toJson()); - $this->assertIsArray($invoices->toArray()); - nlog("end invoices = " . microtime(true) - $start); - } private function transformPayment(Payment $payment): array @@ -466,8 +589,6 @@ class TemplateTest extends TestCase $data['invoices'] = $invoices; $ts = $replicated_design->service()->build($data); - // nlog("results = "); - // nlog($ts->getHtml()); $this->assertNotNull($ts->getHtml()); }