invoices: unified money, number and date formats and fully respect configured language (#1814)
This commit is contained in:
@@ -10,8 +10,7 @@
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use App\Twig\LocaleExtensions;
|
||||
|
||||
final class DefaultInvoiceFormatter implements InvoiceFormatter
|
||||
{
|
||||
@@ -19,25 +18,13 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter
|
||||
* @var DateExtensions
|
||||
*/
|
||||
private $dateExtension;
|
||||
|
||||
/**
|
||||
* @var Extensions
|
||||
* @var LocaleExtensions
|
||||
*/
|
||||
private $extension;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
* @param DateExtensions $dateExtension
|
||||
* @param Extensions $extensions
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
||||
public function __construct(DateExtensions $dateExtension, LocaleExtensions $extensions)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
}
|
||||
@@ -60,23 +47,24 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter
|
||||
return $this->dateExtension->time($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $amount
|
||||
* @param string $currency
|
||||
* @return string
|
||||
*/
|
||||
public function getFormattedMoney($amount, $currency)
|
||||
{
|
||||
return $this->extension->money($amount, $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFormattedMonthName(\DateTime $date)
|
||||
{
|
||||
return $this->translator->trans($this->dateExtension->monthName($date));
|
||||
return $this->dateExtension->monthName($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|int $amount
|
||||
* @param string|null $currency
|
||||
* @param bool $withCurrency
|
||||
* @return string
|
||||
*/
|
||||
public function getFormattedMoney($amount, ?string $currency, bool $withCurrency = true)
|
||||
{
|
||||
return $this->extension->money($amount, $currency, $withCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,13 +64,13 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
'entry.type' => $item->getType(),
|
||||
'entry.category' => $item->getCategory(),
|
||||
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
|
||||
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
|
||||
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, $currency, false),
|
||||
'entry.rate_plain' => $appliedRate,
|
||||
'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency),
|
||||
'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, null),
|
||||
'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false),
|
||||
'entry.rate_internal_plain' => $internalRate,
|
||||
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
|
||||
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
|
||||
'entry.total_nc' => $formatter->getFormattedMoney($rate, $currency, false),
|
||||
'entry.total_plain' => $rate,
|
||||
'entry.currency' => $currency,
|
||||
'entry.duration' => $item->getDuration(),
|
||||
|
||||
@@ -31,15 +31,15 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
||||
'invoice.vat' => $model->getCalculator()->getVat(),
|
||||
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
|
||||
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null),
|
||||
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false),
|
||||
'invoice.tax_plain' => $tax,
|
||||
'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
|
||||
'invoice.total' => $formatter->getFormattedMoney($total, $currency),
|
||||
'invoice.total_nc' => $formatter->getFormattedMoney($total, null),
|
||||
'invoice.total_nc' => $formatter->getFormattedMoney($total, $currency, false),
|
||||
'invoice.total_plain' => $total,
|
||||
'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency),
|
||||
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null),
|
||||
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, $currency, false),
|
||||
'invoice.subtotal_plain' => $subtotal,
|
||||
|
||||
'template.name' => $model->getTemplate()->getName(),
|
||||
|
||||
@@ -53,7 +53,7 @@ class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'end_date' => null !== $project->getEnd() ? $formatter->getFormattedDateTime($project->getEnd()) : '',
|
||||
$prefix . 'order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||
$prefix . 'budget_money' => $formatter->getFormattedMoney($project->getBudget(), $currency),
|
||||
$prefix . 'budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), null),
|
||||
$prefix . 'budget_money_nc' => $formatter->getFormattedMoney($project->getBudget(), $currency, false),
|
||||
$prefix . 'budget_money_plain' => $project->getBudget(),
|
||||
$prefix . 'budget_time' => $project->getTimeBudget(),
|
||||
$prefix . 'budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()),
|
||||
|
||||
@@ -29,9 +29,10 @@ interface InvoiceFormatter
|
||||
/**
|
||||
* @param int|float $amount
|
||||
* @param string|null $currency
|
||||
* @return mixed
|
||||
* @param bool $withCurrency
|
||||
* @return string
|
||||
*/
|
||||
public function getFormattedMoney($amount, $currency);
|
||||
public function getFormattedMoney($amount, ?string $currency, bool $withCurrency = true);
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
|
||||
74
src/Invoice/Renderer/AbstractTwigRenderer.php
Normal file
74
src/Invoice/Renderer/AbstractTwigRenderer.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice\Renderer;
|
||||
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\LocaleExtensions;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractTwigRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model): string
|
||||
{
|
||||
$previousLocale = $this->changeTwigLocale($this->twig, $model->getTemplate()->getLanguage());
|
||||
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
|
||||
$this->changeTwigLocale($this->twig, $previousLocale);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function changeTwigLocale(Environment $twig, ?string $locale = null): ?string
|
||||
{
|
||||
// for invoices that don't have a language configured (using request locale)
|
||||
if (null === $locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var TranslationExtension $extension */
|
||||
$extension = $twig->getExtension(TranslationExtension::class);
|
||||
/** @var LocaleAwareInterface $translator */
|
||||
$translator = $extension->getTranslator();
|
||||
$previousLocale = $translator->getLocale();
|
||||
|
||||
$translator->setLocale($locale);
|
||||
|
||||
/** @var LocaleExtensions $extension */
|
||||
$extension = $twig->getExtension(LocaleExtensions::class);
|
||||
$extension->setLocale($locale);
|
||||
|
||||
/** @var DateExtensions $extension */
|
||||
$extension = $twig->getExtension(DateExtensions::class);
|
||||
$extension->setLocale($locale);
|
||||
|
||||
return $previousLocale;
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,11 @@ namespace App\Invoice\Renderer;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
final class JsonRenderer implements RendererInterface
|
||||
final class JsonRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
return stripos($document->getFilename(), '.json.twig') !== false;
|
||||
@@ -36,9 +24,7 @@ final class JsonRenderer implements RendererInterface
|
||||
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->renderTwigTemplate($document, $model);
|
||||
$filename = (string) new InvoiceFilename($model);
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
@@ -12,18 +12,13 @@ namespace App\Invoice\Renderer;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Utils\HtmlToPdfConverter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
final class PdfRenderer implements RendererInterface
|
||||
final class PdfRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
/**
|
||||
* @var HtmlToPdfConverter
|
||||
*/
|
||||
@@ -31,7 +26,7 @@ final class PdfRenderer implements RendererInterface
|
||||
|
||||
public function __construct(Environment $twig, HtmlToPdfConverter $converter)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
parent::__construct($twig);
|
||||
$this->converter = $converter;
|
||||
}
|
||||
|
||||
@@ -42,9 +37,7 @@ final class PdfRenderer implements RendererInterface
|
||||
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->renderTwigTemplate($document, $model);
|
||||
|
||||
$content = $this->converter->convertToPdf($content, [
|
||||
'setAutoTopMargin' => 'pad',
|
||||
|
||||
@@ -12,23 +12,11 @@ namespace App\Invoice\Renderer;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
final class TextRenderer implements RendererInterface
|
||||
final class TextRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
return stripos($document->getFilename(), '.txt.twig') !== false;
|
||||
@@ -36,9 +24,7 @@ final class TextRenderer implements RendererInterface
|
||||
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->renderTwigTemplate($document, $model);
|
||||
$filename = (string) new InvoiceFilename($model);
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
@@ -11,22 +11,10 @@ namespace App\Invoice\Renderer;
|
||||
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
final class TwigRenderer implements RendererInterface
|
||||
final class TwigRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
return stripos($document->getFilename(), '.html.twig') !== false;
|
||||
@@ -34,9 +22,7 @@ final class TwigRenderer implements RendererInterface
|
||||
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->renderTwigTemplate($document, $model);
|
||||
|
||||
$response = new Response();
|
||||
$response->setContent($content);
|
||||
|
||||
@@ -12,23 +12,11 @@ namespace App\Invoice\Renderer;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
final class XmlRenderer implements RendererInterface
|
||||
final class XmlRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
/**
|
||||
* @var Environment
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
return stripos($document->getFilename(), '.xml.twig') !== false;
|
||||
@@ -36,9 +24,7 @@ final class XmlRenderer implements RendererInterface
|
||||
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
$content = $this->twig->render('@invoice/' . basename($document->getFilename()), [
|
||||
'model' => $model
|
||||
]);
|
||||
$content = $this->renderTwigTemplate($document, $model);
|
||||
$filename = (string) new InvoiceFilename($model);
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
Reference in New Issue
Block a user