Release 2.27 (#5212)
This commit is contained in:
@@ -11,13 +11,13 @@ namespace App\Configuration;
|
||||
|
||||
final class MailConfiguration
|
||||
{
|
||||
public function __construct(private string $mailFrom)
|
||||
public function __construct(private readonly string $mailFrom)
|
||||
{
|
||||
}
|
||||
|
||||
public function getFromAddress(): ?string
|
||||
{
|
||||
if (empty($this->mailFrom)) {
|
||||
if (trim($this->mailFrom) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.26.0';
|
||||
public const VERSION = '2.27.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 22600;
|
||||
public const VERSION_ID = 22700;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,6 @@ use App\Model\CustomerBudgetStatisticModel;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
@@ -36,7 +35,7 @@ class CustomerStatisticService
|
||||
/**
|
||||
* WARNING: this method does not respect the budget type. Your results will always be with the "full lifetime data" or the "selected date-range".
|
||||
*/
|
||||
public function getCustomerStatistics(Customer $customer, ?DateTime $begin = null, ?DateTime $end = null): CustomerStatistic
|
||||
public function getCustomerStatistics(Customer $customer, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): CustomerStatistic
|
||||
{
|
||||
$statistics = $this->getBudgetStatistic([$customer], $begin, $end);
|
||||
$event = new CustomerStatisticEvent($customer, array_pop($statistics), $begin, $end);
|
||||
@@ -51,7 +50,7 @@ class CustomerStatisticService
|
||||
$stats->setStatisticTotal($this->getCustomerStatistics($customer));
|
||||
|
||||
$begin = null;
|
||||
$end = DateTime::createFromInterface($today);
|
||||
$end = DateTimeImmutable::createFromInterface($today);
|
||||
|
||||
if ($customer->isMonthlyBudget()) {
|
||||
$dateFactory = new DateTimeFactory($today->getTimezone());
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Model\CustomerStatistic;
|
||||
|
||||
final class CustomerStatisticEvent extends AbstractCustomerEvent
|
||||
{
|
||||
public function __construct(Customer $customer, private CustomerStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
|
||||
public function __construct(Customer $customer, private readonly CustomerStatistic $statistic, private readonly ?\DateTimeInterface $begin = null, private readonly ?\DateTimeInterface $end = null)
|
||||
{
|
||||
parent::__construct($customer);
|
||||
}
|
||||
@@ -24,12 +24,12 @@ final class CustomerStatisticEvent extends AbstractCustomerEvent
|
||||
return $this->statistic;
|
||||
}
|
||||
|
||||
public function getBegin(): ?\DateTime
|
||||
public function getBegin(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->begin;
|
||||
}
|
||||
|
||||
public function getEnd(): ?\DateTime
|
||||
public function getEnd(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
/**
|
||||
* Working time for every day of the given year.
|
||||
* Will be reflected in the working-time summary row.
|
||||
*
|
||||
* Only to be used with already approved entries.
|
||||
*
|
||||
* Can be locked before, but also can be locked by the system.
|
||||
*/
|
||||
final class WorkingTimeYearEvent extends Event
|
||||
{
|
||||
public function __construct(private Year $year, private \DateTimeInterface $until)
|
||||
public function __construct(private readonly Year $year, private readonly \DateTimeInterface $until)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ trait ToolbarFormTrait
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($name, $multiProject, $activityOptions) {
|
||||
function (FormEvent $event) use ($name, $multiProject, $activityOptions, $options) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add($name, ActivityType::class, array_merge($activityOptions, [
|
||||
@@ -299,7 +299,7 @@ trait ToolbarFormTrait
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]));
|
||||
], $options));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
use BudgetHydratorTrait;
|
||||
|
||||
public function __construct(private ActivityStatisticService $activityStatistic)
|
||||
public function __construct(private readonly ActivityStatisticService $activityStatistic)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -70,8 +70,9 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'invoice_text' => $activity->getInvoiceText() ?? '',
|
||||
];
|
||||
|
||||
if ($model->getQuery()?->getEnd() !== null) {
|
||||
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $model->getQuery()->getEnd());
|
||||
$end = $model->getQuery()?->getEnd();
|
||||
if ($end !== null) {
|
||||
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $end);
|
||||
|
||||
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
use BudgetHydratorTrait;
|
||||
|
||||
public function __construct(private CustomerStatisticService $customerStatisticService)
|
||||
public function __construct(private readonly CustomerStatisticService $customerStatisticService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -29,34 +29,37 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
return [];
|
||||
}
|
||||
|
||||
$prefix = 'customer.';
|
||||
|
||||
$values = [
|
||||
'customer.id' => $customer->getId(),
|
||||
'customer.address' => $customer->getAddress() ?? '',
|
||||
'customer.name' => $customer->getName() ?? '',
|
||||
'customer.contact' => $customer->getContact() ?? '',
|
||||
'customer.company' => $customer->getCompany() ?? '',
|
||||
'customer.vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
|
||||
'customer.vat_id' => $customer->getVatId() ?? '',
|
||||
'customer.number' => $customer->getNumber() ?? '',
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage() ?? '',
|
||||
'customer.comment' => $customer->getComment() ?? '',
|
||||
'customer.email' => $customer->getEmail() ?? '',
|
||||
'customer.fax' => $customer->getFax() ?? '',
|
||||
'customer.phone' => $customer->getPhone() ?? '',
|
||||
'customer.mobile' => $customer->getMobile() ?? '',
|
||||
'customer.invoice_text' => $customer->getInvoiceText() ?? '',
|
||||
$prefix . 'id' => $customer->getId(),
|
||||
$prefix . 'address' => $customer->getAddress() ?? '',
|
||||
$prefix . 'name' => $customer->getName() ?? '',
|
||||
$prefix . 'contact' => $customer->getContact() ?? '',
|
||||
$prefix . 'company' => $customer->getCompany() ?? '',
|
||||
$prefix . 'vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
|
||||
$prefix . 'vat_id' => $customer->getVatId() ?? '',
|
||||
$prefix . 'number' => $customer->getNumber() ?? '',
|
||||
$prefix . 'country' => $customer->getCountry(),
|
||||
$prefix . 'homepage' => $customer->getHomepage() ?? '',
|
||||
$prefix . 'comment' => $customer->getComment() ?? '',
|
||||
$prefix . 'email' => $customer->getEmail() ?? '',
|
||||
$prefix . 'fax' => $customer->getFax() ?? '',
|
||||
$prefix . 'phone' => $customer->getPhone() ?? '',
|
||||
$prefix . 'mobile' => $customer->getMobile() ?? '',
|
||||
$prefix . 'invoice_text' => $customer->getInvoiceText() ?? '',
|
||||
];
|
||||
|
||||
/** @var \DateTime $end */
|
||||
$end = $model->getQuery()->getEnd();
|
||||
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end);
|
||||
$end = $model->getQuery()?->getEnd();
|
||||
if ($end !== null) {
|
||||
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end);
|
||||
|
||||
$values = array_merge($values, $this->getBudgetValues('customer.', $statistic, $model));
|
||||
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
|
||||
}
|
||||
|
||||
foreach ($customer->getMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
$prefix . 'meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
use BudgetHydratorTrait;
|
||||
|
||||
public function __construct(private ProjectStatisticService $projectStatistic)
|
||||
public function __construct(private readonly ProjectStatisticService $projectStatistic)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -83,8 +83,9 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'invoice_text' => $project->getInvoiceText() ?? '',
|
||||
];
|
||||
|
||||
if ($model->getQuery()?->getEnd() !== null) {
|
||||
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $model->getQuery()->getEnd());
|
||||
$end = $model->getQuery()?->getEnd();
|
||||
if ($end !== null) {
|
||||
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $end);
|
||||
|
||||
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
|
||||
}
|
||||
|
||||
@@ -12,17 +12,19 @@ namespace App\Invoice\Renderer;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceDocument;
|
||||
use App\Twig\TwigRendererTrait;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractTwigRenderer implements RendererInterface
|
||||
{
|
||||
use TwigRendererTrait;
|
||||
|
||||
public function __construct(private Environment $twig)
|
||||
public function __construct(private readonly Environment $twig)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,6 +47,65 @@ abstract class AbstractTwigRenderer implements RendererInterface
|
||||
'entries' => $entries
|
||||
], $options);
|
||||
|
||||
// cloning twig, because we don't want to change the
|
||||
return $this->renderTwigTemplateWithLanguage($this->twig, $template, $options, $language, $formatLocale);
|
||||
}
|
||||
|
||||
private function renderTwigTemplateWithLanguage(Environment $twig, string $template, array $options = [], ?string $language = null, ?string $formatLocale = null): string
|
||||
{
|
||||
$previousTranslation = null;
|
||||
$previousFormatLocale = null;
|
||||
|
||||
if ($language !== null) {
|
||||
$previousTranslation = $this->switchTranslationLocale($twig, $language);
|
||||
}
|
||||
if ($formatLocale !== null) {
|
||||
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
|
||||
}
|
||||
|
||||
if (!$twig->hasExtension(SandboxExtension::class)) {
|
||||
$twig->addExtension(new SandboxExtension(new InvoicePolicy()));
|
||||
}
|
||||
|
||||
$sandbox = $twig->getExtension(SandboxExtension::class);
|
||||
$sandbox->enableSandbox();
|
||||
|
||||
$content = $twig->render($template, $options);
|
||||
|
||||
$sandbox->disableSandbox();
|
||||
|
||||
if ($previousTranslation !== null) {
|
||||
$this->switchTranslationLocale($twig, $previousTranslation);
|
||||
}
|
||||
if ($previousFormatLocale !== null) {
|
||||
$this->switchFormatLocale($twig, $previousFormatLocale);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function switchTranslationLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var TranslationExtension $extension */
|
||||
$extension = $twig->getExtension(TranslationExtension::class);
|
||||
|
||||
$translator = $extension->getTranslator();
|
||||
if (!$translator instanceof LocaleAwareInterface) {
|
||||
throw new \Exception('Translator is expected to be of type LocaleAwareInterface');
|
||||
}
|
||||
$previous = $translator->getLocale();
|
||||
$translator->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
private function switchFormatLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var LocaleFormatExtensions $extension */
|
||||
$extension = $twig->getExtension(LocaleFormatExtensions::class);
|
||||
$previous = $extension->getLocale();
|
||||
$extension->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Configuration\MailConfiguration;
|
||||
use App\Constants;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
@@ -36,7 +37,7 @@ final class KimaiMailer implements MailerInterface
|
||||
if ($fallback === null) {
|
||||
throw new \RuntimeException('Missing email "from" address');
|
||||
}
|
||||
$message->from(new Address($fallback, 'Kimai'));
|
||||
$message->from(new Address($fallback, Constants::SOFTWARE));
|
||||
}
|
||||
|
||||
$this->mailer->send($message);
|
||||
|
||||
@@ -199,7 +199,7 @@ class ProjectStatisticService
|
||||
$stats->setStatisticTotal($this->getProjectStatistics($project));
|
||||
|
||||
$begin = null;
|
||||
$end = $today;
|
||||
$end = DateTimeImmutable::createFromInterface($today);
|
||||
|
||||
if ($project->isMonthlyBudget()) {
|
||||
$dateFactory = new DateTimeFactory($today->getTimezone());
|
||||
|
||||
@@ -114,7 +114,7 @@ class TagRepository extends EntityRepository
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->count('tag.id'))
|
||||
->select($qb->expr()->count('tag'))
|
||||
;
|
||||
/** @var int<0, max> $counter */
|
||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
@@ -459,7 +459,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->select($qb->expr()->count('t.id'))
|
||||
->select($qb->expr()->count('t'))
|
||||
;
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
|
||||
@@ -868,7 +868,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select($qb->expr()->count('t.id'))
|
||||
->select($qb->expr()->count('t'))
|
||||
->from(Timesheet::class, 't')
|
||||
;
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<?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\Twig;
|
||||
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait TwigRendererTrait
|
||||
{
|
||||
protected function renderTwigTemplateWithLanguage(Environment $twig, string $template, array $options = [], ?string $language = null, ?string $formatLocale = null): string
|
||||
{
|
||||
$previousTranslation = null;
|
||||
$previousFormatLocale = null;
|
||||
|
||||
if ($language !== null) {
|
||||
$previousTranslation = $this->switchTranslationLocale($twig, $language);
|
||||
}
|
||||
if ($formatLocale !== null) {
|
||||
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
|
||||
}
|
||||
|
||||
// enable basic security measures
|
||||
if (!$twig->hasExtension(SandboxExtension::class)) {
|
||||
$sandbox = new SandboxExtension(new InvoicePolicy());
|
||||
$sandbox->enableSandbox();
|
||||
$twig->addExtension($sandbox);
|
||||
}
|
||||
|
||||
$content = $twig->render($template, $options);
|
||||
|
||||
if ($previousTranslation !== null) {
|
||||
$this->switchTranslationLocale($twig, $previousTranslation);
|
||||
}
|
||||
if ($previousFormatLocale !== null) {
|
||||
$this->switchFormatLocale($twig, $previousFormatLocale);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function switchTranslationLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var TranslationExtension $extension */
|
||||
$extension = $twig->getExtension(TranslationExtension::class);
|
||||
|
||||
$translator = $extension->getTranslator();
|
||||
if (!$translator instanceof LocaleAwareInterface) {
|
||||
throw new \Exception('Translator is expected to be of type LocaleAwareInterface');
|
||||
}
|
||||
$previous = $translator->getLocale();
|
||||
$translator->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
protected function switchFormatLocale(Environment $twig, string $language): string
|
||||
{
|
||||
/** @var LocaleFormatExtensions $extension */
|
||||
$extension = $twig->getExtension(LocaleFormatExtensions::class);
|
||||
$previous = $extension->getLocale();
|
||||
$extension->setLocale($language);
|
||||
|
||||
return $previous;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace App\Utils;
|
||||
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
final class Pagination extends Pagerfanta
|
||||
@@ -19,6 +20,10 @@ final class Pagination extends Pagerfanta
|
||||
{
|
||||
parent::__construct($adapter);
|
||||
|
||||
if ($adapter instanceof ArrayAdapter && ($size = $adapter->getNbResults()) > 0) {
|
||||
$this->setMaxPerPage($size);
|
||||
}
|
||||
|
||||
if ($query === null || !$query->isApiCall()) {
|
||||
$this->setNormalizeOutOfRangePages(true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user