mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-11 13:42:49 +01:00
1038 lines
44 KiB
PHP
1038 lines
44 KiB
PHP
<?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;
|
|
|
|
use App\Models\Client;
|
|
use App\Models\Company;
|
|
use App\Models\Design;
|
|
use App\Models\Invoice;
|
|
use App\Models\Payment;
|
|
use App\Models\Project;
|
|
use App\Models\PurchaseOrder;
|
|
use App\Transformers\ProjectTransformer;
|
|
use App\Transformers\PurchaseOrderTransformer;
|
|
use App\Transformers\QuoteTransformer;
|
|
use App\Transformers\TaskTransformer;
|
|
use App\Utils\HostedPDF\NinjaPdf;
|
|
use App\Utils\HtmlEngine;
|
|
use App\Utils\Number;
|
|
use App\Utils\PaymentHtmlEngine;
|
|
use App\Utils\Traits\MakesDates;
|
|
use App\Utils\Traits\Pdf\PdfMaker;
|
|
use App\Utils\VendorHtmlEngine;
|
|
use League\Fractal\Manager;
|
|
use League\Fractal\Serializer\ArraySerializer;
|
|
use Twig\Environment;
|
|
use Twig\Error\Error;
|
|
use Twig\Error\LoaderError;
|
|
use Twig\Error\RuntimeError;
|
|
use Twig\Error\SyntaxError;
|
|
use Twig\Extension\DebugExtension;
|
|
use Twig\Extension\StringLoaderExtension;
|
|
use Twig\Extra\Intl\IntlExtension;
|
|
use Twig\Loader\FilesystemLoader;
|
|
use Twig\Sandbox\SecurityError;
|
|
use Twig\TwigFilter;
|
|
use Twig\TwigFunction;
|
|
|
|
class TemplateService
|
|
{
|
|
use MakesDates;
|
|
use PdfMaker;
|
|
|
|
private \DomDocument $document;
|
|
|
|
public Environment $twig;
|
|
|
|
private string $compiled_html = '';
|
|
|
|
private array $data = [];
|
|
|
|
private array $variables = [];
|
|
|
|
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
|
|
*
|
|
* @return self
|
|
*/
|
|
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());
|
|
$this->twig = new Environment($loader, [
|
|
'debug' => true,
|
|
]);
|
|
|
|
$string_extension = new StringLoaderExtension();
|
|
$this->twig->addExtension($string_extension);
|
|
$this->twig->addExtension(new IntlExtension());
|
|
$this->twig->addExtension(new DebugExtension());
|
|
|
|
$function = new TwigFunction('img', function ($string, $style = '') {
|
|
return '<img src="'.$string.'" style="'.$style.'"></img>';
|
|
});
|
|
$this->twig->addFunction($function);
|
|
|
|
$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, and field stacks
|
|
*
|
|
* @param array $data - the payload to be passed into the template
|
|
* @return self
|
|
*/
|
|
public function build(array $data): self
|
|
{
|
|
$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);
|
|
|
|
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
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getHtml(): string
|
|
{
|
|
return $this->compiled_html;
|
|
}
|
|
|
|
/**
|
|
* Returns the PDF string
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getPdf(): string
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
|
|
$this->data = $this->preProcessDataBlocks($data);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Parses all Ninja tags in the document
|
|
*
|
|
* @return self
|
|
*/
|
|
private function parseNinjaBlocks(): self
|
|
{
|
|
$replacements = [];
|
|
|
|
$contents = $this->document->getElementsByTagName('ninja');
|
|
|
|
foreach ($contents as $content) {
|
|
|
|
$template = $content->ownerDocument->saveHTML($content);
|
|
|
|
try {
|
|
$template = $this->twig->createTemplate(html_entity_decode($template));
|
|
} catch(SyntaxError $e) {
|
|
nlog($e->getMessage());
|
|
continue;
|
|
} catch(Error $e) {
|
|
nlog("error = " .$e->getMessage());
|
|
continue;
|
|
} catch(RuntimeError $e) {
|
|
nlog("runtime = " .$e->getMessage());
|
|
continue;
|
|
} catch(LoaderError $e) {
|
|
nlog("loader = " . $e->getMessage());
|
|
continue;
|
|
} catch(SecurityError $e) {
|
|
nlog("security = " . $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
$template = $template->render($this->data);
|
|
|
|
$f = $this->document->createDocumentFragment();
|
|
$f->appendXML(html_entity_decode($template));
|
|
|
|
$replacements[] = $f;
|
|
|
|
}
|
|
|
|
foreach($contents as $key => $content) {
|
|
$content->parentNode->replaceChild($replacements[$key], $content);
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses all variables in the document
|
|
*
|
|
* @return self
|
|
*/
|
|
public function parseVariables(): self
|
|
{
|
|
|
|
$html = $this->getHtml();
|
|
|
|
foreach($this->variables as $key => $variable) {
|
|
|
|
if(isset($variable['labels']) && isset($variable['values'])) {
|
|
$html = strtr($html, $variable['labels']);
|
|
$html = strtr($html, $variable['values']);
|
|
}
|
|
}
|
|
|
|
@$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
|
|
{
|
|
if(!$this->template) {
|
|
return $this;
|
|
}
|
|
|
|
$html = '';
|
|
$html .= $this->template->design->includes;
|
|
$html .= $this->template->design->header;
|
|
$html .= $this->template->design->body;
|
|
$html .= $this->template->design->footer;
|
|
|
|
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
/**
|
|
* Inject the template components
|
|
* manually
|
|
*
|
|
* @return self
|
|
*/
|
|
public function setTemplate(array $partials): self
|
|
{
|
|
|
|
$html = '';
|
|
$html .= $partials['design']['includes'];
|
|
$html .= $partials['design']['header'];
|
|
$html .= $partials['design']['body'];
|
|
$html .= $partials['design']['footer'];
|
|
|
|
@$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
/**
|
|
* Resolves the labels and values needed to replace the string
|
|
* holders in the template.
|
|
*
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
private function resolveHtmlEngine(array $data): array
|
|
{
|
|
return collect($data)->map(function ($value, $key) {
|
|
|
|
$processed = [];
|
|
|
|
if(in_array($key, ['tasks','projects','aging']) || !$value->first()) {
|
|
return $processed;
|
|
}
|
|
|
|
match ($key) {
|
|
'variables' => $processed = $value->first() ?? [],
|
|
'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() ?? [],
|
|
'tasks' => $processed = [],
|
|
'projects' => $processed = [],
|
|
'purchase_orders' => (new VendorHtmlEngine($value->first()->invitations()->first()))->generateLabelsAndValues() ?? [],
|
|
'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) {
|
|
|
|
$processed = [];
|
|
|
|
match ($key) {
|
|
'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),
|
|
'aging' => $processed = $value,
|
|
default => $processed = [],
|
|
};
|
|
|
|
return $processed;
|
|
|
|
})->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 = [];
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
/**
|
|
* Pads Line Items with raw and formatted content
|
|
*
|
|
* @param array $items
|
|
* @param Vendor | Client $client_or_vendor
|
|
* @return array
|
|
*/
|
|
public function padLineItems(array $items, Vendor | Client $client_or_vendor): array
|
|
{
|
|
return collect($items)->map(function ($item) use ($client) {
|
|
|
|
$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;
|
|
|
|
$item->cost = Number::formatMoney($item->cost_raw, $client);
|
|
|
|
if($item->is_amount_discount) {
|
|
$item->discount = Number::formatMoney($item->discount_raw, $client);
|
|
}
|
|
|
|
$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();
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
'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,
|
|
'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,
|
|
'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 ?? $payment->company->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,
|
|
'refund_activity' => $this->getPaymentRefundActivity($payment),
|
|
];
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
/**
|
|
* [
|
|
"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
|
|
*/
|
|
private function getPaymentRefundActivity(Payment $payment): array
|
|
{
|
|
|
|
return collect($payment->refund_meta ?? [])
|
|
->map(function ($refund) use ($payment) {
|
|
|
|
$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) {
|
|
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
|
|
$amount = Number::formatMoney($refunded_invoice['amount'], $payment->client);
|
|
$notes = ctrans('texts.status_partially_refunded_amount', ['amount' => $amount]);
|
|
|
|
array_push($map, "{$date} {$entity} #{$invoice->number} {$notes}\n");
|
|
|
|
}
|
|
|
|
return $map;
|
|
|
|
})->flatten()->toArray();
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param array | \Illuminate\Support\Collection $quotes
|
|
* @return array
|
|
*/
|
|
public function processQuotes($quotes): array
|
|
{
|
|
|
|
return collect($quotes)->map(function ($quote){
|
|
|
|
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,
|
|
];
|
|
|
|
})->toArray();
|
|
|
|
}
|
|
|
|
/**
|
|
* Pushes credits through the appropriate transformer
|
|
* and builds any required relationships
|
|
*
|
|
* @param array | \Illuminate\Support\Collection $credits
|
|
* @return array
|
|
*/
|
|
public function processCredits($credits): array
|
|
{
|
|
$credits = collect($credits)
|
|
->map(function ($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(),
|
|
];
|
|
|
|
});
|
|
|
|
return $credits->toArray();
|
|
|
|
}
|
|
|
|
/**
|
|
* Pushes payments through the appropriate transformer
|
|
*
|
|
* @param array | \Illuminate\Support\Collection $payments
|
|
* @return array
|
|
*/
|
|
public function processPayments($payments): array
|
|
{
|
|
|
|
$payments = collect($payments)->map(function ($payment) {
|
|
return $this->transformPayment($payment);
|
|
})->toArray();
|
|
|
|
return $payments;
|
|
|
|
}
|
|
|
|
/**
|
|
* @todo refactor
|
|
*
|
|
* @param mixed $tasks
|
|
* @return array
|
|
*/
|
|
public function processTasks($tasks, bool $nested = false): array
|
|
{
|
|
|
|
return collect($tasks)->map(function ($task) use ($nested){
|
|
|
|
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,
|
|
] : [],
|
|
];
|
|
|
|
|
|
})->toArray();
|
|
|
|
}
|
|
|
|
/**
|
|
* @todo refactor
|
|
*
|
|
* @param array | \Illuminate\Support\Collection $projects
|
|
* @return array
|
|
*/
|
|
public function processProjects($projects): array
|
|
{
|
|
|
|
return
|
|
collect($projects)->map(function ($project){
|
|
|
|
return $this->transformProject($project);
|
|
|
|
})->toArray();
|
|
|
|
}
|
|
|
|
private function transformProject(Project $project, bool $nested = false): array
|
|
{
|
|
|
|
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;
|
|
}
|
|
|
|
}
|