1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-11 13:42:49 +01:00
invoiceninja/app/Services/Template/TemplateService.php

1042 lines
37 KiB
PHP
Raw Normal View History

2023-09-22 08:14:25 +02:00
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Template;
2023-11-06 02:54:29 +01:00
use App\Models\Quote;
use App\Utils\Number;
2023-10-03 07:38:36 +02:00
use App\Models\Client;
2023-09-22 15:31:12 +02:00
use App\Models\Credit;
2023-09-22 08:14:25 +02:00
use App\Models\Design;
2023-11-06 02:54:29 +01:00
use App\Models\Vendor;
use App\Models\Company;
use App\Models\Invoice;
2023-09-22 15:31:12 +02:00
use App\Models\Payment;
use App\Models\Project;
2023-11-06 02:54:29 +01:00
use App\Utils\HtmlEngine;
use League\Fractal\Manager;
2023-09-22 15:31:12 +02:00
use App\Models\PurchaseOrder;
2023-11-06 02:54:29 +01:00
use App\Utils\VendorHtmlEngine;
2023-11-05 22:50:36 +01:00
use App\Models\RecurringInvoice;
2023-09-22 14:08:57 +02:00
use App\Utils\PaymentHtmlEngine;
use App\Utils\Traits\MakesDates;
2023-11-06 02:54:29 +01:00
use App\Utils\HostedPDF\NinjaPdf;
2023-10-08 10:46:00 +02:00
use App\Utils\Traits\Pdf\PdfMaker;
2023-11-05 22:50:36 +01:00
use Twig\Extra\Intl\IntlExtension;
2023-11-06 02:54:29 +01:00
use App\Transformers\TaskTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\ProjectTransformer;
use League\CommonMark\CommonMarkConverter;
use App\Transformers\PurchaseOrderTransformer;
use League\Fractal\Serializer\ArraySerializer;
2023-09-22 08:14:25 +02:00
class TemplateService
{
2023-10-04 06:29:31 +02:00
use MakesDates, PdfMaker;
2023-09-22 08:14:25 +02:00
private \DomDocument $document;
2023-09-25 07:56:32 +02:00
public \Twig\Environment $twig;
2023-09-22 08:14:25 +02:00
private string $compiled_html = '';
2023-09-27 12:36:08 +02:00
private array $data = [];
2023-09-27 13:44:09 +02:00
private array $variables = [];
2023-09-27 13:32:17 +02:00
public ?Company $company;
2023-11-05 22:50:36 +01:00
private ?Client $client;
private ?Vendor $vendor;
private Invoice | Quote | Credit | PurchaseOrder | RecurringInvoice $entity;
private Payment $payment;
2023-11-06 02:54:29 +01:00
private CommonMarkConverter $commonmark;
2023-09-25 07:56:32 +02:00
public function __construct(public ?Design $template = null)
2023-09-22 08:14:25 +02:00
{
$this->template = $template;
$this->init();
}
/**
* Boot Dom Document
*
* @return self
*/
private function init(): self
{
2023-11-06 02:54:29 +01:00
$this->commonmark = new CommonMarkConverter([
'allow_unsafe_links' => false,
]);
2023-09-22 08:14:25 +02:00
$this->document = new \DOMDocument();
$this->document->validateOnParse = true;
2023-09-25 07:56:32 +02:00
$loader = new \Twig\Loader\FilesystemLoader(storage_path());
2023-11-05 22:50:36 +01:00
$this->twig = new \Twig\Environment($loader, [
2023-10-12 00:17:28 +02:00
'debug' => true,
]);
2023-09-25 07:56:32 +02:00
$string_extension = new \Twig\Extension\StringLoaderExtension();
$this->twig->addExtension($string_extension);
$this->twig->addExtension(new IntlExtension());
2023-10-12 00:17:28 +02:00
$this->twig->addExtension(new \Twig\Extension\DebugExtension());
2023-09-25 07:56:32 +02:00
2023-10-02 04:43:07 +02:00
$function = new \Twig\TwigFunction('img', function ($string, $style = '') {
return '<img src="'.$string.'" style="'.$style.'"></img>';
});
$this->twig->addFunction($function);
2023-10-13 04:43:59 +02:00
$filter = new \Twig\TwigFilter('sum', function (array $array, string $column) {
return array_sum(array_column($array, $column));
});
$this->twig->addFilter($filter);
2023-09-22 08:14:25 +02:00
return $this;
}
/**
* Iterate through all of the
* ninja nodes
*
* @param array $data - the payload to be passed into the template
* @return self
*/
2023-09-22 14:35:43 +02:00
public function build(array $data): self
2023-09-22 08:14:25 +02:00
{
$this->compose()
2023-09-27 12:36:08 +02:00
->processData($data)
2023-11-06 02:54:29 +01:00
->parseGlobalStacks()
2023-09-27 12:36:08 +02:00
->parseNinjaBlocks()
2023-09-27 13:44:09 +02:00
->processVariables($data)
2023-11-05 22:50:36 +01:00
->parseVariables();
2023-09-22 08:14:25 +02:00
return $this;
}
2023-09-27 13:44:09 +02:00
private function processVariables($data): self
{
$this->variables = $this->resolveHtmlEngine($data);
return $this;
}
2023-09-27 12:36:08 +02:00
public function mock(): self
{
2023-09-27 13:32:17 +02:00
$tm = new TemplateMock($this->company);
2023-09-27 13:44:09 +02:00
$tm->init();
2023-09-27 13:32:17 +02:00
$this->data = $tm->engines;
2023-09-27 13:44:09 +02:00
$this->variables = $tm->variables[0];
2023-09-27 13:32:17 +02:00
2023-09-27 13:32:17 +02:00
$this->parseNinjaBlocks()
2023-11-06 02:54:29 +01:00
->parseGlobalStacks()
2023-09-27 13:44:09 +02:00
->parseVariables();
2023-09-27 12:36:08 +02:00
return $this;
}
2023-10-12 00:17:28 +02:00
/**
* Returns the HTML as string
*
* @return string
*/
2023-09-22 08:14:25 +02:00
public function getHtml(): string
{
return $this->compiled_html;
}
2023-11-05 22:50:36 +01:00
/**
* Returns the PDF string
*
* @return mixed
*/
2023-10-04 06:29:31 +02:00
public function getPdf(): mixed
{
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($this->compiled_html);
} else {
$pdf = $this->makePdf(null, null, $this->compiled_html);
}
return $pdf;
}
2023-11-05 22:50:36 +01:00
/**
* Get the parsed data
*
* @return array
*/
public function getData(): array
{
return $this->data;
}
2023-11-05 22:50:36 +01:00
/**
* Process data variables
*
* @param mixed $data
* @return self
*/
public function processData($data): self
2023-09-27 12:36:08 +02:00
{
$this->data = $this->preProcessDataBlocks($data);
return $this;
}
2023-09-22 08:14:25 +02:00
/**
* Parses all Ninja tags in the document
2023-11-05 22:50:36 +01:00
*
2023-09-22 08:14:25 +02:00
* @return self
*/
2023-09-27 12:36:08 +02:00
private function parseNinjaBlocks(): self
2023-09-22 08:14:25 +02:00
{
$replacements = [];
2023-09-25 07:56:32 +02:00
2023-09-22 08:14:25 +02:00
$contents = $this->document->getElementsByTagName('ninja');
foreach ($contents as $content) {
$template = $content->ownerDocument->saveHTML($content);
2023-10-02 02:06:24 +02:00
try {
$template = $this->twig->createTemplate(html_entity_decode($template));
2023-11-05 22:50:36 +01:00
} catch(\Twig\Error\SyntaxError $e) {
2023-10-02 02:06:24 +02:00
nlog($e->getMessage());
throw ($e);
2023-11-05 22:50:36 +01:00
} catch(\Twig\Error\Error $e) {
2023-10-12 00:17:28 +02:00
nlog("error = " .$e->getMessage());
throw ($e);
2023-11-05 22:50:36 +01:00
} catch(\Twig\Error\RuntimeError $e) {
2023-10-12 00:17:28 +02:00
nlog("runtime = " .$e->getMessage());
throw ($e);
2023-11-05 22:50:36 +01:00
} catch(\Twig\Error\LoaderError $e) {
2023-10-12 00:17:28 +02:00
nlog("loader = " . $e->getMessage());
throw ($e);
2023-11-05 22:50:36 +01:00
} catch(\Twig\Error\SecurityError $e) {
2023-10-12 00:17:28 +02:00
nlog("security = " . $e->getMessage());
throw ($e);
}
2023-10-02 02:06:24 +02:00
2023-09-27 12:36:08 +02:00
$template = $template->render($this->data);
2023-10-02 04:43:07 +02:00
2023-10-03 07:38:36 +02:00
$f = $this->document->createDocumentFragment();
$f->appendXML(html_entity_decode($template));
2023-09-22 08:14:25 +02:00
$replacements[] = $f;
}
foreach($contents as $key => $content) {
$content->parentNode->replaceChild($replacements[$key], $content);
}
$this->save();
return $this;
}
/**
* Parses all variables in the document
2023-11-05 22:50:36 +01:00
*
2023-09-22 08:14:25 +02:00
* @return self
*/
2023-09-27 13:44:09 +02:00
private function parseVariables(): self
2023-09-22 08:14:25 +02:00
{
2023-09-22 14:35:43 +02:00
$html = $this->getHtml();
2023-09-22 08:14:25 +02:00
2023-09-27 13:44:09 +02:00
foreach($this->variables as $key => $variable) {
2023-09-25 07:56:32 +02:00
2023-11-05 22:50:36 +01:00
if(isset($variable['labels']) && isset($variable['values'])) {
2023-09-25 07:56:32 +02:00
$html = strtr($html, $variable['labels']);
$html = strtr($html, $variable['values']);
}
2023-09-22 14:08:57 +02:00
}
2023-09-22 08:14:25 +02:00
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$this->save();
return $this;
}
/**
* Saves the document and updates the compiled string.
*
* @return self
*/
private function save(): self
{
$this->compiled_html = str_replace('%24', '$', $this->document->saveHTML());
return $this;
}
/**
* compose
*
* @return self
*/
private function compose(): self
{
2023-11-05 22:50:36 +01:00
if(!$this->template) {
2023-09-25 07:56:32 +02:00
return $this;
2023-11-05 22:50:36 +01:00
}
2023-09-25 07:56:32 +02:00
2023-09-22 08:14:25 +02:00
$html = '';
$html .= $this->template->design->includes;
$html .= $this->template->design->header;
$html .= $this->template->design->body;
$html .= $this->template->design->footer;
2023-10-01 12:21:57 +02:00
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
2023-09-22 08:14:25 +02:00
return $this;
}
2023-09-25 07:56:32 +02:00
/**
2023-11-05 22:50:36 +01:00
* Inject the template components
2023-09-25 07:56:32 +02:00
* manually
*
* @return self
*/
public function setTemplate(array $partials): self
2023-09-27 12:36:08 +02:00
{
2023-09-25 07:56:32 +02:00
$html = '';
$html .= $partials['design']['includes'];
$html .= $partials['design']['header'];
$html .= $partials['design']['body'];
$html .= $partials['design']['footer'];
2023-10-01 12:21:57 +02:00
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
2023-09-25 07:56:32 +02:00
return $this;
}
2023-09-22 08:14:25 +02:00
/**
* Resolves the labels and values needed to replace the string
* holders in the template.
*
* @return array
*/
2023-09-22 14:08:57 +02:00
private function resolveHtmlEngine(array $data): array
2023-09-22 08:14:25 +02:00
{
2023-09-22 14:35:43 +02:00
return collect($data)->map(function ($value, $key) {
$processed = [];
2023-09-22 14:08:57 +02:00
2023-11-05 22:50:36 +01:00
if(in_array($key, ['tasks','projects','aging']) || !$value->first()) {
2023-09-26 13:21:10 +02:00
return $processed;
2023-11-05 22:50:36 +01:00
}
2023-09-26 13:21:10 +02:00
2023-09-22 14:08:57 +02:00
match ($key) {
2023-11-05 22:50:36 +01:00
'variables' => $processed = $value->first() ?? [],
2023-09-26 13:21:10 +02:00
'invoices' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'quotes' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'credits' => $processed = (new HtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
'payments' => $processed = (new PaymentHtmlEngine($value->first(), $value->first()->client->contacts()->first()))->generateLabelsAndValues() ?? [],
2023-09-22 14:35:43 +02:00
'tasks' => $processed = [],
'projects' => $processed = [],
2023-09-26 13:21:10 +02:00
'purchase_orders' => (new VendorHtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
2023-10-12 00:17:28 +02:00
'aging' => $processed = [],
default => $processed = [],
2023-09-22 14:08:57 +02:00
};
2023-10-03 09:52:46 +02:00
2023-09-22 14:08:57 +02:00
return $processed;
})->toArray();
2023-09-22 08:14:25 +02:00
}
private function preProcessDataBlocks($data): array
{
2023-11-05 22:50:36 +01:00
return collect($data)->map(function ($value, $key) {
2023-09-22 08:14:25 +02:00
2023-09-22 14:35:43 +02:00
$processed = [];
2023-09-22 08:14:25 +02:00
match ($key) {
2023-09-22 14:35:43 +02:00
'invoices' => $processed = $this->processInvoices($value),
'quotes' => $processed = $this->processQuotes($value),
'credits' => $processed = $this->processCredits($value),
'payments' => $processed = $this->processPayments($value),
'tasks' => $processed = $this->processTasks($value),
'projects' => $processed = $this->processProjects($value),
'purchase_orders' => $processed = $this->processPurchaseOrders($value),
2023-10-12 00:17:28 +02:00
'aging' => $processed = $value,
default => $processed = [],
2023-09-22 08:14:25 +02:00
};
return $processed;
})->toArray();
}
2023-09-27 12:36:08 +02:00
public function processInvoices($invoices): array
{
$invoices = collect($invoices)
2023-11-05 22:50:36 +01:00
->map(function ($invoice) {
$payments = [];
$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),
'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(),
];
});
return $invoices->toArray();
}
2023-10-03 07:38:36 +02:00
public function padLineItems(array $items, Client $client): array
{
2023-11-05 22:50:36 +01:00
return collect($items)->map(function ($item) use ($client) {
2023-10-03 07:38:36 +02:00
2023-10-04 06:29:31 +02:00
$item->cost_raw = $item->cost ?? 0;
$item->discount_raw = $item->discount ?? 0;
$item->line_total_raw = $item->line_total ?? 0;
$item->gross_line_total_raw = $item->gross_line_total ?? 0;
$item->tax_amount_raw = $item->tax_amount ?? 0;
$item->product_cost_raw = $item->product_cost ?? 0;
2023-10-03 07:38:36 +02:00
$item->cost = Number::formatMoney($item->cost_raw, $client);
2023-11-05 22:50:36 +01:00
if($item->is_amount_discount) {
2023-10-03 07:38:36 +02:00
$item->discount = Number::formatMoney($item->discount_raw, $client);
2023-11-05 22:50:36 +01:00
}
2023-10-03 07:38:36 +02:00
$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);
return $item;
})->toArray();
}
2023-10-01 12:21:57 +02:00
private function transformPayment(Payment $payment): array
{
$data = [];
2023-11-05 22:50:36 +01:00
$this->payment = $payment;
$credits = $payment->credits->map(function ($credit) use ($payment) {
return [
'credit' => $credit->number,
'amount_raw' => $credit->pivot->amount,
'refunded_raw' => $credit->pivot->refunded,
'net_raw' => $credit->pivot->amount - $credit->pivot->refunded,
'amount' => Number::formatMoney($credit->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($credit->pivot->refunded, $payment->client),
'net' => Number::formatMoney($credit->pivot->amount - $credit->pivot->refunded, $payment->client),
'is_credit' => true,
2023-10-08 07:06:37 +02:00
'date' => $this->translateDate($credit->date, $payment->client->date_format(), $payment->client->locale()),
'created_at' => $this->translateDate($credit->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($credit->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $credit->pivot->created_at->timestamp,
];
});
$pivot = $payment->invoices->map(function ($invoice) use ($payment) {
return [
'invoice' => $invoice->number,
'amount_raw' => $invoice->pivot->amount,
'refunded_raw' => $invoice->pivot->refunded,
'net_raw' => $invoice->pivot->amount - $invoice->pivot->refunded,
'amount' => Number::formatMoney($invoice->pivot->amount, $payment->client),
'refunded' => Number::formatMoney($invoice->pivot->refunded, $payment->client),
'net' => Number::formatMoney($invoice->pivot->amount - $invoice->pivot->refunded, $payment->client),
'is_credit' => false,
2023-10-08 07:06:37 +02:00
'date' => $this->translateDate($invoice->date, $payment->client->date_format(), $payment->client->locale()),
'created_at' => $this->translateDate($invoice->pivot->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($invoice->pivot->updated_at, $payment->client->date_format(), $payment->client->locale()),
'timestamp' => $invoice->pivot->created_at->timestamp,
];
})->merge($credits)->sortBy('timestamp')->toArray();
return [
'status' => $payment->stringStatus($payment->status_id),
'badge' => $payment->badgeForStatus($payment->status_id),
'amount' => Number::formatMoney($payment->amount, $payment->client),
'applied' => Number::formatMoney($payment->applied, $payment->client),
'balance' => Number::formatMoney(($payment->amount - $payment->refunded - $payment->applied), $payment->client),
'refunded' => Number::formatMoney($payment->refunded, $payment->client),
'amount_raw' => $payment->amount,
'applied_raw' => $payment->applied,
'refunded_raw' => $payment->refunded,
'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,
'exchange_rate' => $payment->exchange_rate,
'transaction_reference' => $payment->transaction_reference,
'is_manual' => $payment->is_manual,
'number' => $payment->number,
'custom_value1' => $payment->custom_value1 ?? '',
'custom_value2' => $payment->custom_value2 ?? '',
'custom_value3' => $payment->custom_value3 ?? '',
'custom_value4' => $payment->custom_value4 ?? '',
'created_at' => $this->translateDate($payment->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($payment->updated_at, $payment->client->date_format(), $payment->client->locale()),
'client' => [
'name' => $payment->client->present()->name(),
'balance' => $payment->client->balance,
'payment_balance' => $payment->client->payment_balance,
'credit_balance' => $payment->client->credit_balance,
],
'paymentables' => $pivot,
2023-10-08 10:46:00 +02:00
'refund_activity' => $this->getPaymentRefundActivity($payment),
];
2023-10-08 10:46:00 +02:00
return $data;
2023-10-08 10:46:00 +02:00
}
/**
* [
"id" => 12,
"date" => "2023-10-08",
"invoices" => [
[
"amount" => 1,
"invoice_id" => 23,
"id" => null,
],
],
"q" => "/api/v1/payments/refund",
"email_receipt" => "true",
"gateway_refund" => false,
"send_email" => false,
],
*
* @param Payment $payment
* @return array
*/
2023-10-08 10:46:00 +02:00
private function getPaymentRefundActivity(Payment $payment): array
{
return collect($payment->refund_meta ?? [])
2023-11-05 22:50:36 +01:00
->map(function ($refund) use ($payment) {
2023-10-08 10:46:00 +02:00
$date = \Carbon\Carbon::parse($refund['date'])->addSeconds($payment->client->timezone_offset());
$date = $this->translateDate($date, $payment->client->date_format(), $payment->client->locale());
$entity = ctrans('texts.invoice');
$map = [];
foreach($refund['invoices'] as $refunded_invoice) {
2023-11-05 22:50:36 +01:00
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
2023-10-08 10:46:00 +02:00
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
array_push($map, "{$date} {$entity} #{$invoice->number} {$notes}\n");
}
2023-10-08 10:46:00 +02:00
return $map;
2023-10-08 10:46:00 +02:00
})->flatten()->toArray();
}
2023-09-27 13:11:47 +02:00
public function processQuotes($quotes): array
2023-09-22 08:14:25 +02:00
{
2023-09-22 15:31:12 +02:00
$it = new QuoteTransformer();
$it->setDefaultIncludes(['client']);
$manager = new Manager();
2023-09-25 07:56:32 +02:00
$manager->parseIncludes(['client']);
$resource = new \League\Fractal\Resource\Collection($quotes, $it, null);
$resources = $manager->createData($resource)->toArray();
foreach($resources['data'] as $key => $resource) {
$resources['data'][$key]['client'] = $resource['client']['data'] ?? [];
$resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? [];
}
return $resources['data'];
2023-09-22 08:14:25 +02:00
}
2023-09-27 10:10:22 +02:00
/**
* Pushes credits through the appropriate transformer
* and builds any required relationships
*
* @param mixed $credits
* @return array
*/
2023-09-27 13:11:47 +02:00
public function processCredits($credits): array
2023-09-22 08:14:25 +02:00
{
2023-10-12 03:13:08 +02:00
$credits = collect($credits)
2023-11-05 22:50:36 +01:00
->map(function ($credit) {
2023-09-25 07:56:32 +02:00
2023-11-05 22:50:36 +01:00
$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(),
];
});
2023-09-25 07:56:32 +02:00
2023-10-12 03:13:08 +02:00
return $credits->toArray();
}
2023-09-25 07:56:32 +02:00
2023-09-22 15:31:12 +02:00
2023-09-27 10:10:22 +02:00
/**
* Pushes payments through the appropriate transformer
*
* @param mixed $payments
* @return array
*/
2023-09-27 13:11:47 +02:00
public function processPayments($payments): array
2023-09-22 08:14:25 +02:00
{
2023-09-25 07:56:32 +02:00
2023-10-12 03:13:08 +02:00
$payments = collect($payments)->map(function ($payment) {
2023-10-01 12:21:57 +02:00
return $this->transformPayment($payment);
})->toArray();
2023-10-01 12:21:57 +02:00
return $payments;
2023-09-22 15:31:12 +02:00
2023-09-22 08:14:25 +02:00
}
2023-09-27 13:11:47 +02:00
public function processTasks($tasks): array
2023-09-22 08:14:25 +02:00
{
2023-09-22 15:31:12 +02:00
$it = new TaskTransformer();
2023-09-25 07:56:32 +02:00
$it->setDefaultIncludes(['client','project','invoice']);
2023-09-22 15:31:12 +02:00
$manager = new Manager();
2023-09-25 07:56:32 +02:00
$resource = new \League\Fractal\Resource\Collection($tasks, $it, null);
$resources = $manager->createData($resource)->toArray();
foreach($resources['data'] as $key => $resource) {
$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 $resources['data'];
2023-09-22 15:31:12 +02:00
2023-09-22 08:14:25 +02:00
}
2023-09-27 13:11:47 +02:00
public function processProjects($projects): array
2023-09-22 08:14:25 +02:00
{
2023-09-22 14:08:57 +02:00
2023-09-22 15:31:12 +02:00
$it = new ProjectTransformer();
$it->setDefaultIncludes(['client','tasks']);
$manager = new Manager();
2023-09-25 05:19:08 +02:00
$manager->setSerializer(new ArraySerializer());
2023-09-22 15:31:12 +02:00
$resource = new \League\Fractal\Resource\Collection($projects, $it, Project::class);
$i = $manager->createData($resource)->toArray();
2023-09-25 05:19:08 +02:00
return $i[Project::class];
2023-09-22 15:31:12 +02:00
2023-09-22 08:14:25 +02:00
}
2023-09-27 13:11:47 +02:00
public function processPurchaseOrders($purchase_orders): array
2023-09-22 08:14:25 +02:00
{
2023-09-22 15:31:12 +02:00
$it = new PurchaseOrderTransformer();
$it->setDefaultIncludes(['vendor','expense']);
$manager = new Manager();
2023-09-25 05:19:08 +02:00
$manager->setSerializer(new ArraySerializer());
2023-09-22 15:31:12 +02:00
$resource = new \League\Fractal\Resource\Collection($purchase_orders, $it, PurchaseOrder::class);
$i = $manager->createData($resource)->toArray();
2023-09-25 05:19:08 +02:00
return $i[PurchaseOrder::class];
2023-09-22 08:14:25 +02:00
}
2023-09-27 13:32:17 +02:00
public function setCompany(Company $company): self
{
$this->company = $company;
return $this;
}
public function getCompany(): Company
{
return $this->company;
}
2023-10-12 03:13:08 +02:00
public function overrideVariables($variables): self
{
$this->variables = $variables;
return $this;
}
2023-11-05 22:50:36 +01:00
/**
* Parses and finds any stacks to replace
*
* @return self
*/
2023-11-06 02:54:29 +01:00
public function parseGlobalStacks(): self
2023-11-05 22:50:36 +01:00
{
$stacks = [
'entity-details',
'client-details',
'vendor-details',
'company-details',
'company-address',
'shipping-details',
];
collect($stacks)->filter(function ($stack) {
$this->document->getElementById($stack);
})->each(function ($stack){
$this->parseStack($stack);
});
return $this;
}
/**
* Injects field stacks into Template
*
* @param string $stack
* @return self
*/
private function parseStack(string $stack): self
{
match($stack){
'entity-details' => $this->entityDetails(),
'client-details' => $this->clientDetails(),
'vendor-details' => $this->vendorDetails(),
'company-details' => $this->companyDetails(),
'company-address' => $this->companyAddress(),
'shipping-details' => $this->shippingDetails(),
};
return $this;
}
private function companyDetails(): self
{
2023-11-06 02:56:43 +01:00
$company_details =
2023-11-05 22:50:36 +01:00
collect($this->company->settings->pdf_variables['company_details'])
->filter(function ($variable) {
return isset($this->variables['values'][$variable]) && !empty($this->variables['values'][$variable]);
})
->map(function ($variable) {
return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]];
2023-11-06 02:54:29 +01:00
})->toArray();
2023-11-05 22:50:36 +01:00
2023-11-06 02:56:43 +01:00
$this->parseStack()
2023-11-05 22:50:36 +01:00
return $this;
}
private function companyAddress(): self
{
$variables = $this->company->settings->pdf_variables['company_address'];
$elements = [];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]];
}
return $elements;
return $this;
}
private function shippingDetails(): self
{
return $this;
}
private function clientDetails(): self
{
return $this;
}
private function entityDetails(): self
{
return $this;
}
private function vendorDetails(): self
{
2023-11-06 02:54:29 +01:00
// $elements = [];
// if (!$this->vendor) {
// return $elements;
// }
// $variables = $this->context['pdf_variables']['vendor_details'];
// foreach ($variables as $variable) {
// $elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]];
// }
2023-11-05 22:50:36 +01:00
2023-11-06 02:54:29 +01:00
// return $elements;
return $this;
}
////////////////////////////////////////
// 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;
2023-11-05 22:50:36 +01:00
}
2023-11-06 02:54:29 +01:00
$element->setAttribute($attribute, $value);
2023-11-05 22:50:36 +01:00
2023-11-06 02:54:29 +01:00
if ($element->getAttribute($attribute) === $value) {
return $element;
2023-11-05 22:50:36 +01:00
}
2023-11-06 02:54:29 +01:00
return $element;
}
public function createElementContent($element, $children) :self
{
foreach ($children as $child) {
$contains_html = false;
if ($child['element'] !== 'script') {
if ($this->company->markdown_enabled && array_key_exists('content', $child)) {
$child['content'] = str_replace('<br>', "\r", $child['content']);
$child['content'] = $this->commonmark->convert($child['content'] ?? '');
}
}
if (isset($child['content'])) {
if (isset($child['is_empty']) && $child['is_empty'] === true) {
continue;
}
2023-11-05 22:50:36 +01:00
2023-11-06 02:54:29 +01:00
$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']);
}
}
2023-11-05 22:50:36 +01:00
return $this;
}
2023-10-12 03:13:08 +02:00
2023-11-06 02:54:29 +01:00
2023-11-05 22:50:36 +01:00
}