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 = '