From ea3c5236c60ad2aa9642bc9f00bdd9097d4fb5f9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Nov 2023 10:57:02 +1100 Subject: [PATCH] Fixes for rounding --- app/Services/Pdf/PdfBuilder.php | 23 ++--- app/Services/Pdf/PdfConfiguration.php | 119 ++++++++++++++++++++++ app/Services/Template/TemplateService.php | 67 ++++++------ 3 files changed, 159 insertions(+), 50 deletions(-) diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index db8438c49a55..dd453097e073 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -633,9 +633,7 @@ class PdfBuilder $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']]; } elseif ($cell == '$product.tax_rate3') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']]; - } - - elseif ($cell == '$task.discount' && !$this->service->company->enable_product_discount) { + } elseif ($cell == '$task.discount' && !$this->service->company->enable_product_discount) { $element['elements'][] = ['element' => 'td', 'content' => $row['$task.discount'], 'properties' => ['data-ref' => 'task_table-task.discount-td', 'style' => 'display: none;']]; } elseif ($cell == '$task.tax_rate1') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax1-td']]; @@ -643,10 +641,7 @@ class PdfBuilder $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax2-td']]; } elseif ($cell == '$task.tax_rate3') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'task_table-task.tax3-td']]; - } - - - elseif ($cell == '$product.unit_cost' || $cell == '$task.rate') { + } elseif ($cell == '$product.unit_cost' || $cell == '$task.rate') { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['style' => 'white-space: nowrap;', 'data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']]; } else { $element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']]; @@ -677,7 +672,7 @@ class PdfBuilder $locale_info = localeconv(); foreach ($items as $key => $item) { - /** @var \App\DataMapper\InvoiceItem $item */ + /** @var \App\DataMapper\InvoiceItem $item */ if ($table_type == '$product' && $item->type_id != 1) { if ($item->type_id != 4 && $item->type_id != 6 && $item->type_id != 5) { @@ -712,9 +707,9 @@ class PdfBuilder $data[$key][$table_type.".{$_table_type}4"] = strlen($item->custom_value4) >= 1 ? $helpers->formatCustomFieldValue($this->service->company->custom_fields, "{$_table_type}4", $item->custom_value4, $this->service->config->currency_entity) : ''; if ($item->quantity > 0 || $item->cost > 0) { - $data[$key][$table_type.'.quantity'] = $item->quantity; + $data[$key][$table_type.'.quantity'] = $this->service->config->formatValueNoTrailingZeroes($item->quantity); - $data[$key][$table_type.'.unit_cost'] = $this->service->config->formatMoney($item->cost); + $data[$key][$table_type.'.unit_cost'] = $this->service->config->formatMoneyNoRounding($item->cost); $data[$key][$table_type.'.cost'] = $this->service->config->formatMoney($item->cost); @@ -820,9 +815,7 @@ class PdfBuilder $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } elseif ($column == '$product.tax_rate3') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; - } - - elseif ($column == '$task.discount' && !$this->service->company->enable_product_discount) { + } elseif ($column == '$task.discount' && !$this->service->company->enable_product_discount) { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']]; } elseif ($column == '$task.tax_rate1') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax1-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; @@ -830,9 +823,7 @@ class PdfBuilder $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax2-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } elseif ($column == '$task.tax_rate3') { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-task.tax3-th", 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; - } - - else { + } else { $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->service->config->settings->hide_empty_columns_on_pdf]]; } } diff --git a/app/Services/Pdf/PdfConfiguration.php b/app/Services/Pdf/PdfConfiguration.php index a1982fc13193..3b97e841c1ab 100644 --- a/app/Services/Pdf/PdfConfiguration.php +++ b/app/Services/Pdf/PdfConfiguration.php @@ -330,6 +330,125 @@ class PdfConfiguration } } + /** + * Formats a given value based on the clients currency. + * + * @param float $value The number to be formatted + * + * @return string The formatted value + */ + public function formatValueNoTrailingZeroes($value) :string + { + $value = floatval($value); + + $thousand = $this->currency->thousand_separator; + $decimal = $this->currency->decimal_separator; + + /* Country settings override client settings */ + if (isset($this->country->thousand_separator) && strlen($this->country->thousand_separator) >= 1) { + $thousand = $this->country->thousand_separator; + } + + if (isset($this->country->decimal_separator) && strlen($this->country->decimal_separator) >= 1) { + $decimal = $this->country->decimal_separator; + } + + $precision = 10; + + return rtrim(rtrim(number_format($value, $precision, $decimal, $thousand), '0'), $decimal); + } + + + /** + * Formats a given value based on the clients currency AND country. + * + * @param float $value The number to be formatted + * @return string The formatted value + */ + public function formatMoneyNoRounding($value) :string + { + + $_value = $value; + + $thousand = $this->currency->thousand_separator; + $decimal = $this->currency->decimal_separator; + $precision = $this->currency->precision; + $code = $this->currency->code; + $swapSymbol = $this->currency->swap_currency_symbol; + + /* Country settings override client settings */ + if (isset($this->country->thousand_separator) && strlen($this->country->thousand_separator) >= 1) { + $thousand = $this->country->thousand_separator; + } + + if (isset($this->country->decimal_separator) && strlen($this->country->decimal_separator) >= 1) { + $decimal = $this->country->decimal_separator; + } + + if (isset($this->country->swap_currency_symbol) && strlen($this->country->swap_currency_symbol) >= 1) { + $swapSymbol = $this->country->swap_currency_symbol; + } + + /* 08-01-2022 allow increased precision for unit price*/ + $v = rtrim(sprintf('%f', $value), '0'); + $parts = explode('.', $v); + + /* 08-02-2023 special if block to render $0.5 to $0.50*/ + if ($v < 1 && strlen($v) == 3) { + $precision = 2; + } elseif ($v < 1) { + $precision = strlen($v) - strrpos($v, '.') - 1; + } + + if (is_array($parts) && $parts[0] != 0) { + $precision = 2; + } + + //04-04-2023 if currency = JPY override precision to 0 + if($this->currency->code == 'JPY') { + $precision = 0; + } + + $value = number_format($v, $precision, $decimal, $thousand); + $symbol = $this->currency->symbol; + + if ($this->settings->show_currency_code === true && $this->currency->code == 'CHF') { + return "{$code} {$value}"; + } elseif ($this->settings->show_currency_code === true) { + return "{$value} {$code}"; + } elseif ($swapSymbol) { + return "{$value} ".trim($symbol); + } elseif ($this->settings->show_currency_code === false) { + if ($_value < 0) { + $value = substr($value, 1); + $symbol = "-{$symbol}"; + } + + return "{$symbol}{$value}"; + } else { + return $this->formatValue($value); + } + } + + /** + * Formats a given value based on the clients currency. + * + * @param float $value The number to be formatted + * + * @return string The formatted value + */ + public function formatValue($value) :string + { + $value = floatval($value); + + $thousand = $this->currency->thousand_separator; + $decimal = $this->currency->decimal_separator; + $precision = $this->currency->precision; + + return number_format($value, $precision, $decimal, $thousand); + } + + /** * date_format * diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 01792e4279dc..771885405fb6 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -11,35 +11,31 @@ namespace App\Services\Template; -use Twig\TwigFilter; -use App\Models\Quote; -use App\Utils\Number; -use Twig\Environment; -use Twig\Error\Error; use App\Models\Client; +use App\Models\Company; use App\Models\Credit; use App\Models\Design; -use App\Models\Vendor; -use Twig\TwigFunction; -use App\Models\Company; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; -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\Quote; use App\Models\RecurringInvoice; +use App\Models\Vendor; +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\HostedPDF\NinjaPdf; -use Twig\Loader\FilesystemLoader; use App\Utils\Traits\Pdf\PdfMaker; -use Twig\Extra\Intl\IntlExtension; +use App\Utils\VendorHtmlEngine; use League\CommonMark\CommonMarkConverter; +use Twig\Error\Error; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; +use Twig\Extra\Intl\IntlExtension; +use Twig\Sandbox\SecurityError; class TemplateService { @@ -88,17 +84,18 @@ class TemplateService $this->document = new \DOMDocument(); $this->document->validateOnParse = true; - $loader = new FilesystemLoader(storage_path()); - $this->twig = new Environment($loader, [ + + $loader = new \Twig\Loader\FilesystemLoader(storage_path()); + $this->twig = new \Twig\Environment($loader, [ 'debug' => true, ]); $string_extension = new \Twig\Extension\StringLoaderExtension(); $this->twig->addExtension($string_extension); $this->twig->addExtension(new IntlExtension()); $this->twig->addExtension(new \Twig\Extension\DebugExtension()); - - $function = new TwigFunction('img', function ($string, $style = '') { - return ''; + + $function = new \Twig\TwigFunction('img', function ($string, $style = '') { + return ''; }); $this->twig->addFunction($function); @@ -236,16 +233,16 @@ class TemplateService $template = $this->twig->createTemplate(html_entity_decode($template)); } catch(SyntaxError $e) { nlog($e->getMessage()); - continue; + throw ($e); } catch(Error $e) { - nlog("error = " .$e->getMessage()); - continue; + nlog("error = " . $e->getMessage()); + throw ($e); } catch(RuntimeError $e) { - nlog("runtime = " .$e->getMessage()); - continue; + nlog("runtime = " . $e->getMessage()); + throw ($e); } catch(LoaderError $e) { nlog("loader = " . $e->getMessage()); - continue; + throw ($e); } catch(SecurityError $e) { nlog("security = " . $e->getMessage()); throw ($e); @@ -281,7 +278,6 @@ class TemplateService $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']); @@ -363,7 +359,7 @@ class TemplateService $processed = []; - if(in_array($key, ['tasks','projects','aging']) || !$value->first()) { + if(in_array($key, ['tasks', 'projects', 'aging']) || !$value->first()) { return $processed; } @@ -428,7 +424,8 @@ class TemplateService ->map(function ($invoice) { $payments = []; - + $this->entity = $invoice; + if($invoice->payments ?? false) { $payments = $invoice->payments->map(function ($payment) { return $this->transformPayment($payment); @@ -438,6 +435,8 @@ class TemplateService return [ 'amount' => Number::formatMoney($invoice->amount, $invoice->client), 'balance' => Number::formatMoney($invoice->balance, $invoice->client), + 'status_id' => $invoice->status_id, + 'status' => Invoice::stringStatus($invoice->status_id), 'balance_raw' => $invoice->balance, 'number' => $invoice->number ?: '', 'discount' => $invoice->discount, @@ -475,7 +474,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4, - 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client): (array) [], + 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client) : (array) [], 'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()), @@ -791,7 +790,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $credit->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $credit->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $credit->custom_surcharge_tax4, - 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client): (array) [], + 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client) : (array) [], 'reminder1_sent' => $this->translateDate($credit->reminder1_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder2_sent' => $this->translateDate($credit->reminder2_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder3_sent' => $this->translateDate($credit->reminder3_sent, $credit->client->date_format(), $credit->client->locale()), @@ -1449,4 +1448,4 @@ class TemplateService return $this; } -} +} \ No newline at end of file