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..456b350fb526 100644
--- a/app/Http/Requests/Task/StoreTaskRequest.php
+++ b/app/Http/Requests/Task/StoreTaskRequest.php
@@ -54,14 +54,11 @@ 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) {
-
- if(is_string($values)) {
- $values = json_decode($values, 1);
- }
+ $rules['time_log'] = ['bail',function ($attribute, $values, $fail) {
- if(!is_array($values)) {
- return $fail('The '.$attribute.' is invalid. Must be an array.');
+ if(!is_array(json_decode($values, true))) {
+ $fail('The '.$attribute.' must be a valid array.');
+ return;
}
foreach ($values as $k) {
@@ -119,6 +116,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..f9d6099c2842 100644
--- a/app/Http/Requests/Task/UpdateTaskRequest.php
+++ b/app/Http/Requests/Task/UpdateTaskRequest.php
@@ -38,12 +38,14 @@ class UpdateTaskRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
+ /** @var \App\Models\User $user */
+ $user = auth()->user();
+
return $user->can('edit', $this->task);
}
public function rules()
{
-
/** @var \App\Models\User $user */
$user = auth()->user();
@@ -61,14 +63,11 @@ 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);
- }
-
- if(!is_array($values)) {
- return $fail('The '.$attribute.' is invalid. Must be an array.');
+ if(!is_array(json_decode($values, true))) {
+ $fail('The '.$attribute.' must be a valid array.');
+ return;
}
foreach ($values as $k) {
@@ -129,6 +128,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/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/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php
index 8061c014bd6d..db8438c49a55 100644
--- a/app/Services/Pdf/PdfBuilder.php
+++ b/app/Services/Pdf/PdfBuilder.php
@@ -1621,12 +1621,6 @@ class PdfBuilder
// Dom Traversal
///////////////////////////////////////
-
- public function getSectionNode(string $selector)
- {
- return $this->document->getElementById($selector);
- }
-
public function updateElementProperties() :self
{
foreach ($this->sections as $element) {
diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php
index c5b783b51399..f7e00ff5e8e3 100644
--- a/app/Services/Template/TemplateService.php
+++ b/app/Services/Template/TemplateService.php
@@ -11,12 +11,23 @@
namespace App\Services\Template;
+<<<<<<< HEAD
use App\Models\Client;
+=======
+use App\Models\Quote;
+use App\Utils\Number;
+use Twig\Error\Error;
+use App\Models\Client;
+use App\Models\Credit;
+use App\Models\Design;
+use App\Models\Vendor;
+>>>>>>> support_for_custom_statement_designs
use App\Models\Company;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Project;
+<<<<<<< HEAD
use App\Models\PurchaseOrder;
use App\Transformers\ProjectTransformer;
use App\Transformers\PurchaseOrderTransformer;
@@ -43,11 +54,28 @@ use Twig\Loader\FilesystemLoader;
use Twig\Sandbox\SecurityError;
use Twig\TwigFilter;
use Twig\TwigFunction;
+=======
+use App\Utils\HtmlEngine;
+use Twig\Error\LoaderError;
+use Twig\Error\SyntaxError;
+use Twig\Error\RuntimeError;
+use App\Models\PurchaseOrder;
+use App\Utils\VendorHtmlEngine;
+use Twig\Sandbox\SecurityError;
+use App\Models\RecurringInvoice;
+use App\Utils\PaymentHtmlEngine;
+use App\Utils\Traits\MakesDates;
+use App\Utils\HostedPDF\NinjaPdf;
+use App\Utils\Traits\Pdf\PdfMaker;
+use Twig\Extra\Intl\IntlExtension;
+use League\CommonMark\CommonMarkConverter;
+>>>>>>> support_for_custom_statement_designs
class TemplateService
{
- use MakesDates, PdfMaker;
-
+ use MakesDates;
+ use PdfMaker;
+
private \DomDocument $document;
public Environment $twig;
@@ -60,12 +88,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,6 +111,11 @@ class TemplateService
*/
private function init(): self
{
+
+ $this->commonmark = new CommonMarkConverter([
+ 'allow_unsafe_links' => false,
+ ]);
+
$this->document = new \DOMDocument();
$this->document->validateOnParse = true;
$loader = new FilesystemLoader(storage_path());
@@ -93,15 +136,15 @@ class TemplateService
$filter = new 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 +153,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 +174,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 +206,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 +224,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,13 +260,14 @@ 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());
+<<<<<<< HEAD
continue;
} catch(Error $e) {
nlog("error = " .$e->getMessage());
@@ -204,6 +278,18 @@ class TemplateService
} catch(LoaderError $e) {
nlog("loader = " . $e->getMessage());
continue;
+=======
+ throw ($e);
+ } catch(Error $e) {
+ nlog("error = " . $e->getMessage());
+ throw ($e);
+ } catch(RuntimeError $e) {
+ nlog("runtime = " . $e->getMessage());
+ throw ($e);
+ } catch(LoaderError $e) {
+ nlog("loader = " . $e->getMessage());
+ throw ($e);
+>>>>>>> support_for_custom_statement_designs
} catch(SecurityError $e) {
nlog("security = " . $e->getMessage());
continue;
@@ -213,7 +299,7 @@ class TemplateService
$f = $this->document->createDocumentFragment();
$f->appendXML(html_entity_decode($template));
-
+
$replacements[] = $f;
}
@@ -227,19 +313,22 @@ 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) {
+<<<<<<< HEAD
+=======
+>>>>>>> support_for_custom_statement_designs
if(isset($variable['labels']) && isset($variable['values'])) {
$html = strtr($html, $variable['labels']);
$html = strtr($html, $variable['values']);
@@ -251,7 +340,7 @@ class TemplateService
return $this;
}
-
+
/**
* Saves the document and updates the compiled string.
*
@@ -286,7 +375,7 @@ class TemplateService
return $this;
}
-
+
/**
* Inject the template components
* manually
@@ -312,15 +401,20 @@ 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 = [];
+<<<<<<< HEAD
if(in_array($key, ['tasks','projects','aging']) || !$value->first()) {
+=======
+ if(in_array($key, ['tasks', 'projects', 'aging']) || !$value->first()) {
+>>>>>>> support_for_custom_statement_designs
return $processed;
}
@@ -336,13 +430,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,12 +467,19 @@ 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 = [];
+<<<<<<< HEAD
if($invoice->payments ?? false) {
$payments = $invoice->payments->map(function ($payment) {
@@ -437,15 +545,96 @@ class TemplateService
'line_tax_map' => $invoice->calc()->getTaxMap(),
];
+=======
+ $this->entity = $invoice;
+
+ if($invoice->payments ?? false) {
+ $payments = $invoice->payments->map(function ($payment) {
+ return $this->transformPayment($payment);
+ })->toArray();
+ }
+
+ 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,
+ 'po_number' => $invoice->po_number ?: '',
+ 'date' => $this->translateDate($invoice->date, $invoice->client->date_format(), $invoice->client->locale()),
+ 'last_sent_date' => $this->translateDate($invoice->last_sent_date, $invoice->client->date_format(), $invoice->client->locale()),
+ 'next_send_date' => $this->translateDate($invoice->next_send_date, $invoice->client->date_format(), $invoice->client->locale()),
+ 'due_date' => $this->translateDate($invoice->due_date, $invoice->client->date_format(), $invoice->client->locale()),
+ 'terms' => $invoice->terms ?: '',
+ 'public_notes' => $invoice->public_notes ?: '',
+ 'private_notes' => $invoice->private_notes ?: '',
+ 'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes,
+ 'tax_name1' => $invoice->tax_name1 ?? '',
+ 'tax_rate1' => (float) $invoice->tax_rate1,
+ 'tax_name2' => $invoice->tax_name2 ?? '',
+ 'tax_rate2' => (float) $invoice->tax_rate2,
+ 'tax_name3' => $invoice->tax_name3 ?? '',
+ 'tax_rate3' => (float) $invoice->tax_rate3,
+ 'total_taxes' => Number::formatMoney($invoice->total_taxes, $invoice->client),
+ 'total_taxes_raw' => $invoice->total_taxes,
+ 'is_amount_discount' => (bool) $invoice->is_amount_discount ?? false,
+ 'footer' => $invoice->footer ?? '',
+ 'partial' => $invoice->partial ?? 0,
+ 'partial_due_date' => $this->translateDate($invoice->partial_due_date, $invoice->client->date_format(), $invoice->client->locale()),
+ 'custom_value1' => (string) $invoice->custom_value1 ?: '',
+ 'custom_value2' => (string) $invoice->custom_value2 ?: '',
+ 'custom_value3' => (string) $invoice->custom_value3 ?: '',
+ 'custom_value4' => (string) $invoice->custom_value4 ?: '',
+ 'custom_surcharge1' => (float) $invoice->custom_surcharge1,
+ 'custom_surcharge2' => (float) $invoice->custom_surcharge2,
+ 'custom_surcharge3' => (float) $invoice->custom_surcharge3,
+ 'custom_surcharge4' => (float) $invoice->custom_surcharge4,
+ 'exchange_rate' => (float) $invoice->exchange_rate,
+ 'custom_surcharge_tax1' => (bool) $invoice->custom_surcharge_tax1,
+ '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) [],
+ '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()),
+ 'reminder_last_sent' => $this->translateDate($invoice->reminder_last_sent, $invoice->client->date_format(), $invoice->client->locale()),
+ 'paid_to_date' => Number::formatMoney($invoice->paid_to_date, $invoice->client),
+ 'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
+ 'client' => [
+ 'name' => $invoice->client->present()->name(),
+ 'balance' => $invoice->client->balance,
+ 'payment_balance' => $invoice->client->payment_balance,
+ 'credit_balance' => $invoice->client->credit_balance,
+ ],
+ 'payments' => $payments,
+ 'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
+ 'line_tax_map' => $invoice->calc()->getTaxMap(),
+ ];
+
+>>>>>>> support_for_custom_statement_designs
});
return $invoices->toArray();
}
- 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
{
+<<<<<<< HEAD
return collect($items)->map(function ($item) use ($client) {
+=======
+ return collect($items)->map(function ($item) use ($client_or_vendor) {
+>>>>>>> support_for_custom_statement_designs
$item->cost_raw = $item->cost ?? 0;
$item->discount_raw = $item->discount ?? 0;
@@ -454,6 +643,7 @@ class TemplateService
$item->tax_amount_raw = $item->tax_amount ?? 0;
$item->product_cost_raw = $item->product_cost ?? 0;
+<<<<<<< HEAD
$item->cost = Number::formatMoney($item->cost_raw, $client);
if($item->is_amount_discount) {
@@ -464,17 +654,37 @@ class TemplateService
$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->cost = Number::formatMoney($item->cost_raw, $client_or_vendor);
+
+ if($item->is_amount_discount) {
+ $item->discount = Number::formatMoney($item->discount_raw, $client_or_vendor);
+ }
+
+ $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);
+>>>>>>> support_for_custom_statement_designs
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,
@@ -543,8 +753,6 @@ class TemplateService
'refund_activity' => $this->getPaymentRefundActivity($payment),
];
- nlog($data);
-
return $data;
}
@@ -596,31 +804,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 +888,7 @@ class TemplateService
$credits = collect($credits)
->map(function ($credit) {
+<<<<<<< HEAD
return [
'amount' => Number::formatMoney($credit->amount, $credit->client),
'balance' => Number::formatMoney($credit->balance, $credit->client),
@@ -686,18 +947,78 @@ class TemplateService
'line_tax_map' => $credit->calc()->getTaxMap(),
];
+=======
+ $this->entity = $credit;
+
+ return [
+ 'amount' => Number::formatMoney($credit->amount, $credit->client),
+ 'balance' => Number::formatMoney($credit->balance, $credit->client),
+ 'balance_raw' => $credit->balance,
+ 'number' => $credit->number ?: '',
+ 'discount' => $credit->discount,
+ 'po_number' => $credit->po_number ?: '',
+ 'date' => $this->translateDate($credit->date, $credit->client->date_format(), $credit->client->locale()),
+ 'last_sent_date' => $this->translateDate($credit->last_sent_date, $credit->client->date_format(), $credit->client->locale()),
+ 'next_send_date' => $this->translateDate($credit->next_send_date, $credit->client->date_format(), $credit->client->locale()),
+ 'due_date' => $this->translateDate($credit->due_date, $credit->client->date_format(), $credit->client->locale()),
+ 'terms' => $credit->terms ?: '',
+ 'public_notes' => $credit->public_notes ?: '',
+ 'private_notes' => $credit->private_notes ?: '',
+ 'uses_inclusive_taxes' => (bool) $credit->uses_inclusive_taxes,
+ 'tax_name1' => $credit->tax_name1 ?? '',
+ 'tax_rate1' => (float) $credit->tax_rate1,
+ 'tax_name2' => $credit->tax_name2 ?? '',
+ 'tax_rate2' => (float) $credit->tax_rate2,
+ 'tax_name3' => $credit->tax_name3 ?? '',
+ 'tax_rate3' => (float) $credit->tax_rate3,
+ 'total_taxes' => Number::formatMoney($credit->total_taxes, $credit->client),
+ 'total_taxes_raw' => $credit->total_taxes,
+ 'is_amount_discount' => (bool) $credit->is_amount_discount ?? false,
+ 'footer' => $credit->footer ?? '',
+ 'partial' => $credit->partial ?? 0,
+ 'partial_due_date' => $this->translateDate($credit->partial_due_date, $credit->client->date_format(), $credit->client->locale()),
+ 'custom_value1' => (string) $credit->custom_value1 ?: '',
+ 'custom_value2' => (string) $credit->custom_value2 ?: '',
+ 'custom_value3' => (string) $credit->custom_value3 ?: '',
+ 'custom_value4' => (string) $credit->custom_value4 ?: '',
+ 'custom_surcharge1' => (float) $credit->custom_surcharge1,
+ 'custom_surcharge2' => (float) $credit->custom_surcharge2,
+ 'custom_surcharge3' => (float) $credit->custom_surcharge3,
+ 'custom_surcharge4' => (float) $credit->custom_surcharge4,
+ 'exchange_rate' => (float) $credit->exchange_rate,
+ 'custom_surcharge_tax1' => (bool) $credit->custom_surcharge_tax1,
+ '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) [],
+ '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()),
+ 'reminder_last_sent' => $this->translateDate($credit->reminder_last_sent, $credit->client->date_format(), $credit->client->locale()),
+ 'paid_to_date' => Number::formatMoney($credit->paid_to_date, $credit->client),
+ 'auto_bill_enabled' => (bool) $credit->auto_bill_enabled,
+ 'client' => [
+ 'name' => $credit->client->present()->name(),
+ 'balance' => $credit->client->balance,
+ 'payment_balance' => $credit->client->payment_balance,
+ 'credit_balance' => $credit->client->credit_balance,
+ ],
+ 'payments' => [],
+ 'total_tax_map' => $credit->calc()->getTotalTaxMap(),
+ 'line_tax_map' => $credit->calc()->getTaxMap(),
+ ];
+
+>>>>>>> support_for_custom_statement_designs
});
return $credits->toArray();
}
-
-
/**
* Pushes payments through the appropriate transformer
*
- * @param mixed $payments
+ * @param array | \Illuminate\Support\Collection $payments
* @return array
*/
public function processPayments($payments): array
@@ -706,77 +1027,655 @@ 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;
}
+<<<<<<< HEAD
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> support_for_custom_statement_designs
}
diff --git a/composer.lock b/composer.lock
index 630eb189ccfd..4d2e70716810 100644
--- a/composer.lock
+++ b/composer.lock
@@ -485,6 +485,7 @@
},
{
"name": "aws/aws-sdk-php",
+<<<<<<< HEAD
"version": "3.285.3",
"source": {
"type": "git",
@@ -495,6 +496,18 @@
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/afa1e722f1b2c95644f375dc1a19072e4daf67be",
"reference": "afa1e722f1b2c95644f375dc1a19072e4daf67be",
+=======
+ "version": "3.284.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aws/aws-sdk-php.git",
+ "reference": "1b7e926acc990509e3d13fa708cdede686b40d90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1b7e926acc990509e3d13fa708cdede686b40d90",
+ "reference": "1b7e926acc990509e3d13fa708cdede686b40d90",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -574,9 +587,15 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
+<<<<<<< HEAD
"source": "https://github.com/aws/aws-sdk-php/tree/3.285.3"
},
"time": "2023-11-09T19:07:19+00:00"
+=======
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.284.0"
+ },
+ "time": "2023-11-03T18:13:48+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "bacon/bacon-qr-code",
@@ -686,6 +705,7 @@
},
{
"name": "braintree/braintree_php",
+<<<<<<< HEAD
"version": "6.15.0",
"source": {
"type": "git",
@@ -696,6 +716,18 @@
"type": "zip",
"url": "https://api.github.com/repos/braintree/braintree_php/zipball/16efb08e19cb6c579deba11e119ef6409d28eae3",
"reference": "16efb08e19cb6c579deba11e119ef6409d28eae3",
+=======
+ "version": "6.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/braintree/braintree_php.git",
+ "reference": "084ed5bb728bc32ad444c6d043c87b409cca72d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/braintree/braintree_php/zipball/084ed5bb728bc32ad444c6d043c87b409cca72d7",
+ "reference": "084ed5bb728bc32ad444c6d043c87b409cca72d7",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -729,9 +761,15 @@
"description": "Braintree PHP Client Library",
"support": {
"issues": "https://github.com/braintree/braintree_php/issues",
+<<<<<<< HEAD
"source": "https://github.com/braintree/braintree_php/tree/6.15.0"
},
"time": "2023-11-08T00:15:11+00:00"
+=======
+ "source": "https://github.com/braintree/braintree_php/tree/6.14.0"
+ },
+ "time": "2023-10-18T22:00:15+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "brick/math",
@@ -4230,6 +4268,7 @@
},
{
"name": "laravel/framework",
+<<<<<<< HEAD
"version": "v10.31.0",
"source": {
"type": "git",
@@ -4240,6 +4279,18 @@
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/507ce9b28bce4b5e4140c28943092ca38e9a52e4",
"reference": "507ce9b28bce4b5e4140c28943092ca38e9a52e4",
+=======
+ "version": "v10.30.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/framework.git",
+ "reference": "7a2da50258c4d0f693b738d3f3c69b2693aea6c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/7a2da50258c4d0f693b738d3f3c69b2693aea6c1",
+ "reference": "7a2da50258c4d0f693b738d3f3c69b2693aea6c1",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -4428,7 +4479,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
+<<<<<<< HEAD
"time": "2023-11-07T13:48:30+00:00"
+=======
+ "time": "2023-11-01T13:52:17+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "laravel/prompts",
@@ -5225,6 +5280,7 @@
},
{
"name": "league/flysystem",
+<<<<<<< HEAD
"version": "3.19.0",
"source": {
"type": "git",
@@ -5235,6 +5291,18 @@
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1b2aa10f2326e0351399b8ce68e287d8e9209a83",
"reference": "1b2aa10f2326e0351399b8ce68e287d8e9209a83",
+=======
+ "version": "3.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem.git",
+ "reference": "015633a05aee22490495159237a5944091d8281e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/015633a05aee22490495159237a5944091d8281e",
+ "reference": "015633a05aee22490495159237a5944091d8281e",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -5299,7 +5367,11 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
+<<<<<<< HEAD
"source": "https://github.com/thephpleague/flysystem/tree/3.19.0"
+=======
+ "source": "https://github.com/thephpleague/flysystem/tree/3.18.0"
+>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@@ -5311,7 +5383,11 @@
"type": "github"
}
],
+<<<<<<< HEAD
"time": "2023-11-07T09:04:28+00:00"
+=======
+ "time": "2023-10-20T17:59:40+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "league/flysystem-aws-s3-v3",
@@ -5381,6 +5457,7 @@
},
{
"name": "league/flysystem-local",
+<<<<<<< HEAD
"version": "3.19.0",
"source": {
"type": "git",
@@ -5391,6 +5468,18 @@
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/8d868217f9eeb4e9a7320db5ccad825e9a7a4076",
"reference": "8d868217f9eeb4e9a7320db5ccad825e9a7a4076",
+=======
+ "version": "3.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-local.git",
+ "reference": "e7381ef7643f658b87efb7dbe98fe538fb1bbf32"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e7381ef7643f658b87efb7dbe98fe538fb1bbf32",
+ "reference": "e7381ef7643f658b87efb7dbe98fe538fb1bbf32",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -5425,7 +5514,11 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-local/issues",
+<<<<<<< HEAD
"source": "https://github.com/thephpleague/flysystem-local/tree/3.19.0"
+=======
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.18.0"
+>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@@ -5437,7 +5530,11 @@
"type": "github"
}
],
+<<<<<<< HEAD
"time": "2023-11-06T20:35:28+00:00"
+=======
+ "time": "2023-10-19T20:07:13+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "league/fractal",
@@ -5831,6 +5928,7 @@
},
{
"name": "mollie/mollie-api-php",
+<<<<<<< HEAD
"version": "v2.63.0",
"source": {
"type": "git",
@@ -5841,6 +5939,18 @@
"type": "zip",
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/642f1b87624bd1535cd198134113e14bc01ba245",
"reference": "642f1b87624bd1535cd198134113e14bc01ba245",
+=======
+ "version": "v2.62.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mollie/mollie-api-php.git",
+ "reference": "feb6d52859ed1ea7a65b25bb6cbfaadb04b33827"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/feb6d52859ed1ea7a65b25bb6cbfaadb04b33827",
+ "reference": "feb6d52859ed1ea7a65b25bb6cbfaadb04b33827",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -5917,9 +6027,15 @@
],
"support": {
"issues": "https://github.com/mollie/mollie-api-php/issues",
+<<<<<<< HEAD
"source": "https://github.com/mollie/mollie-api-php/tree/v2.63.0"
},
"time": "2023-11-06T09:20:50+00:00"
+=======
+ "source": "https://github.com/mollie/mollie-api-php/tree/v2.62.0"
+ },
+ "time": "2023-10-23T11:22:58+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "moneyphp/money",
@@ -7613,6 +7729,7 @@
},
{
"name": "php-http/promise",
+<<<<<<< HEAD
"version": "1.2.1",
"source": {
"type": "git",
@@ -7623,6 +7740,18 @@
"type": "zip",
"url": "https://api.github.com/repos/php-http/promise/zipball/44a67cb59f708f826f3bec35f22030b3edb90119",
"reference": "44a67cb59f708f826f3bec35f22030b3edb90119",
+=======
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/promise.git",
+ "reference": "ef4905bfb492ff389eb7f12e26925a0f20073050"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/promise/zipball/ef4905bfb492ff389eb7f12e26925a0f20073050",
+ "reference": "ef4905bfb492ff389eb7f12e26925a0f20073050",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -7659,9 +7788,15 @@
],
"support": {
"issues": "https://github.com/php-http/promise/issues",
+<<<<<<< HEAD
"source": "https://github.com/php-http/promise/tree/1.2.1"
},
"time": "2023-11-08T12:57:08+00:00"
+=======
+ "source": "https://github.com/php-http/promise/tree/1.2.0"
+ },
+ "time": "2023-10-24T09:20:26+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "php-jsonpointer/php-jsonpointer",
@@ -14825,6 +14960,7 @@
},
{
"name": "friendsofphp/php-cs-fixer",
+<<<<<<< HEAD
"version": "v3.38.0",
"source": {
"type": "git",
@@ -14835,6 +14971,18 @@
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7e6070026e76aa09d77a47519625c86593fb8e31",
"reference": "7e6070026e76aa09d77a47519625c86593fb8e31",
+=======
+ "version": "v3.37.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
+ "reference": "c3fe76976081ab871aa654e872da588077e19679"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c3fe76976081ab871aa654e872da588077e19679",
+ "reference": "c3fe76976081ab871aa654e872da588077e19679",
+>>>>>>> support_for_custom_statement_designs
"shasum": ""
},
"require": {
@@ -14906,7 +15054,11 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
+<<<<<<< HEAD
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.38.0"
+=======
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.37.1"
+>>>>>>> support_for_custom_statement_designs
},
"funding": [
{
@@ -14914,7 +15066,11 @@
"type": "github"
}
],
+<<<<<<< HEAD
"time": "2023-11-07T08:44:54+00:00"
+=======
+ "time": "2023-10-29T20:51:23+00:00"
+>>>>>>> support_for_custom_statement_designs
},
{
"name": "hamcrest/hamcrest-php",
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..d91ad2e5f56e 100644
--- a/tests/Feature/Template/TemplateTest.php
+++ b/tests/Feature/Template/TemplateTest.php
@@ -18,6 +18,7 @@ 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;
@@ -165,6 +166,8 @@ class TemplateTest extends TestCase
';
+ private string $stack = '