Release 2.32 (#5411)
* bump packages * dynamic invoice options * make sure that invoice previews can be detected * support for mpdf associated files * do not include any future times in work contract calculation * re-add username column in Excel spreadsheet * deactivate internal rate editing * show if plugin update exists * shorten name to Kimai only, without Time-Tracking * remove check for existing id in work contract * fix metafield already defined in search * helper methods to unlock months * new translation * send event on unlock month
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^2.8",
|
||||
"easybill/zugferd-php": "^2.1",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
|
||||
630
composer.lock
generated
630
composer.lock
generated
File diff suppressed because it is too large
Load Diff
20
phpstan.neon
20
phpstan.neon
@@ -3432,26 +3432,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Model/Statistic/Month.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Pdf\\\\HtmlToPdfConverter\\:\\:convertToPdf\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Pdf/HtmlToPdfConverter.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:convertToPdf\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Pdf/MPdfConverter.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:sanitizeOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Pdf/MPdfConverter.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Pdf\\\\MPdfConverter\\:\\:sanitizeOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Pdf/MPdfConverter.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Pdf\\\\PdfContext\\:\\:getOption\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Kimai Time-Tracker",
|
||||
"name": "Kimai",
|
||||
"short_name": "Kimai",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -9,18 +9,13 @@
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
{
|
||||
/**
|
||||
* @param string|null $plaintextApiToken
|
||||
* @param PasswordUpgraderInterface<User> $passwordUpgrader
|
||||
*/
|
||||
public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader)
|
||||
public function __construct(private ?string $plaintextApiToken, private readonly PasswordUpgraderInterface $passwordUpgrader)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,9 +31,6 @@ final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PasswordUpgraderInterface<User>
|
||||
*/
|
||||
public function getPasswordUpgrader(): PasswordUpgraderInterface
|
||||
{
|
||||
return $this->passwordUpgrader;
|
||||
|
||||
@@ -454,10 +454,12 @@ final class TranslationCommand extends Command
|
||||
|
||||
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
||||
$source = $unit->source;
|
||||
if (!isset($unit['resname'])) {
|
||||
if (!isset($unit['resname']) && $source !== null) {
|
||||
$unit['resname'] = $source;
|
||||
}
|
||||
$unit['id'] = $this->generateId($unit['resname']);
|
||||
if ($unit['resname'] !== null) {
|
||||
$unit['id'] = $this->generateId($unit['resname']); // @phpstan-ignore offsetAssign.valueType
|
||||
}
|
||||
}
|
||||
|
||||
$xmlDocument = new \DOMDocument('1.0');
|
||||
@@ -491,7 +493,7 @@ final class TranslationCommand extends Command
|
||||
);
|
||||
}
|
||||
$unit->target[0] = $translations[$key];
|
||||
$unit->target['state'] = 'needs-translation';
|
||||
$unit->target['state'] = 'needs-translation'; // @phpstan-ignore assign.propertyType
|
||||
$foundEmpty = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.31.0';
|
||||
public const VERSION = '2.32.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 23100;
|
||||
public const VERSION_ID = 23200;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -164,6 +164,7 @@ final class InvoiceController extends AbstractController
|
||||
try {
|
||||
$query->setCustomers([$customer]);
|
||||
$model = $this->service->createModel($query);
|
||||
$model->setPreview(true);
|
||||
|
||||
return $this->service->renderInvoice($model, $this->dispatcher, true);
|
||||
} catch (Exception $ex) {
|
||||
|
||||
@@ -34,11 +34,20 @@ final class PluginController extends AbstractController
|
||||
$page = new PageSetup('menu.plugin');
|
||||
$page->setHelp('plugins.html');
|
||||
|
||||
$all = $this->getPluginInformation($client, $cache);
|
||||
$bundles = [];
|
||||
foreach ($all as $item) {
|
||||
if ($item['bundle'] !== null) {
|
||||
$bundles[$item['bundle']] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'plugins' => $plugins,
|
||||
'installed' => $installed,
|
||||
'extensions' => $this->getPluginInformation($client, $cache)
|
||||
'extensions' => $all,
|
||||
'bundles' => $bundles,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class InvoiceCreatedEvent extends Event
|
||||
{
|
||||
public function __construct(private Invoice $invoice, private InvoiceModel $model)
|
||||
public function __construct(private readonly Invoice $invoice, private readonly InvoiceModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ final class InvoiceDocumentsEvent extends Event
|
||||
$this->documents = $documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setMaximumAllowedDocuments(int $max): void
|
||||
{
|
||||
$this->maximum = $max;
|
||||
|
||||
@@ -16,7 +16,11 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class InvoicePreRenderEvent extends Event
|
||||
{
|
||||
public function __construct(private InvoiceModel $model, private InvoiceDocument $document, private RendererInterface $renderer)
|
||||
public function __construct(
|
||||
private readonly InvoiceModel $model,
|
||||
private readonly InvoiceDocument $document,
|
||||
private readonly RendererInterface $renderer
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace App\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.32 - use TranslatorInterface directly
|
||||
*/
|
||||
final class ThemeJavascriptTranslationsEvent extends Event
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -15,13 +15,19 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeApproveMonthEvent extends Event
|
||||
{
|
||||
public function __construct(private User $user, private Month $month, private \DateTimeInterface $approvalDate, private User $approver)
|
||||
public function __construct(
|
||||
private readonly Month $month,
|
||||
private readonly User $approvedBy
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getMonth()->getUser() instead)
|
||||
*/
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
return $this->getMonth()->getUser();
|
||||
}
|
||||
|
||||
public function getMonth(): Month
|
||||
@@ -29,13 +35,8 @@ final class WorkingTimeApproveMonthEvent extends Event
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getApprovalDate(): \DateTimeInterface
|
||||
public function getApprovedBy(): User
|
||||
{
|
||||
return $this->approvalDate;
|
||||
}
|
||||
|
||||
public function getApprover(): User
|
||||
{
|
||||
return $this->approver;
|
||||
return $this->approvedBy;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Event/WorkingTimeUnlockMonthEvent.php
Normal file
34
src/Event/WorkingTimeUnlockMonthEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeUnlockMonthEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Month $month,
|
||||
private readonly User $unlockedBy
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMonth(): Month
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getUnlockedBy(): User
|
||||
{
|
||||
return $this->unlockedBy;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,8 @@ final class SpreadsheetRenderer
|
||||
$columns[] = (new Column('fixedRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getFixedRate());
|
||||
}
|
||||
|
||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('alias', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getUserIdentifier())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber());
|
||||
$columns[] = (new Column('customer', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('project', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
|
||||
@@ -360,16 +360,13 @@ class TimesheetEditForm extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
$moneyOptions = ['currency' => $currency];
|
||||
|
||||
$builder
|
||||
->add('fixedRate', FixedRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', HourlyRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('internalRate', InternalRateType::class, [
|
||||
'currency' => $currency,
|
||||
]);
|
||||
->add('fixedRate', FixedRateType::class, $moneyOptions)
|
||||
->add('hourlyRate', HourlyRateType::class, $moneyOptions)
|
||||
//->add('internalRate', InternalRateType::class, $moneyOptions)
|
||||
;
|
||||
}
|
||||
|
||||
protected function addUser(FormBuilderInterface $builder, array $options): void
|
||||
|
||||
@@ -52,6 +52,11 @@ final class InvoiceModel
|
||||
private array $itemHydrator = [];
|
||||
private ?string $invoiceNumber = null;
|
||||
private bool $hideZeroTax = false;
|
||||
private bool $isPreview = false;
|
||||
/**
|
||||
* @var array<string, string|array<string|int, mixed>|null|bool|int|float>
|
||||
*/
|
||||
private array $options = [];
|
||||
|
||||
/**
|
||||
* @internal use InvoiceModelFactory
|
||||
@@ -68,6 +73,22 @@ final class InvoiceModel
|
||||
$this->addItemHydrator(new InvoiceItemDefaultHydrator());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<string|int, mixed>|null|bool|int|float $value
|
||||
*/
|
||||
public function setOption(string $key, string|array|null|bool|int|float $value): void
|
||||
{
|
||||
$this->options[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|array<string|int, mixed>|null|bool|int|float>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getQuery(): ?InvoiceQuery
|
||||
{
|
||||
return $this->query;
|
||||
@@ -265,4 +286,14 @@ final class InvoiceModel
|
||||
{
|
||||
$this->hideZeroTax = $hideZeroTax;
|
||||
}
|
||||
|
||||
public function isPreview(): bool
|
||||
{
|
||||
return $this->isPreview;
|
||||
}
|
||||
|
||||
public function setPreview(bool $preview): void
|
||||
{
|
||||
$this->isPreview = $preview;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
|
||||
{
|
||||
use PDFRendererTrait;
|
||||
|
||||
public function __construct(Environment $twig, private HtmlToPdfConverter $converter)
|
||||
public function __construct(Environment $twig, private readonly HtmlToPdfConverter $converter)
|
||||
{
|
||||
parent::__construct($twig);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
|
||||
$context->setOption('margin_bottom', '8');
|
||||
|
||||
$content = $this->renderTwigTemplate($document, $model, ['pdfContext' => $context]);
|
||||
$content = $this->converter->convertToPdf($content, $context->getOptions());
|
||||
$content = $this->converter->convertToPdf($content, array_merge($model->getOptions(), $context->getOptions()));
|
||||
|
||||
return $this->createPdfResponse($content, $context);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ interface HtmlToPdfConverter
|
||||
* Returns the binary content of the PDF, which can be saved as file.
|
||||
* Throws an exception if conversion fails.
|
||||
*
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string;
|
||||
|
||||
@@ -18,22 +18,29 @@ use Mpdf\Output\Destination;
|
||||
|
||||
final class MPdfConverter implements HtmlToPdfConverter
|
||||
{
|
||||
public function __construct(private FileHelper $fileHelper, private string $cacheDirectory)
|
||||
public function __construct(
|
||||
private readonly FileHelper $fileHelper,
|
||||
private readonly string $cacheDirectory
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @return array<string, mixed|array<string, mixed>>
|
||||
*/
|
||||
private function sanitizeOptions(array $options): array
|
||||
{
|
||||
$configs = new ConfigVariables();
|
||||
$fonts = new FontVariables();
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts',
|
||||
];
|
||||
|
||||
$filtered = array_filter($options, function ($key) use ($allowed, $configs, $fonts): bool {
|
||||
$filtered = array_filter($options, function ($key): bool {
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', 'associated_files'
|
||||
];
|
||||
if (!\in_array($key, $allowed)) {
|
||||
$configs = new ConfigVariables();
|
||||
if (!\array_key_exists($key, $configs->getDefaults())) {
|
||||
$fonts = new FontVariables();
|
||||
|
||||
return \array_key_exists($key, $fonts->getDefaults());
|
||||
}
|
||||
}
|
||||
@@ -49,10 +56,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string
|
||||
{
|
||||
@@ -93,17 +97,26 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return Mpdf
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
*/
|
||||
private function initMpdf(array $options): Mpdf
|
||||
{
|
||||
$options['fontDir'] = $this->getFontDirectories();
|
||||
$options['fontdata'] = $this->mergeFontData($options);
|
||||
|
||||
$associatedFiles = [];
|
||||
if (\array_key_exists('associated_files', $options) && \is_array($options['associated_files'])) {
|
||||
$associatedFiles = $options['associated_files'];
|
||||
unset($options['associated_files']);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf($options);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
if (\count($associatedFiles) > 0) {
|
||||
$mpdf->SetAssociatedFiles($associatedFiles);
|
||||
}
|
||||
|
||||
return $mpdf;
|
||||
}
|
||||
|
||||
@@ -120,8 +133,8 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return array<string, array<mixed>>
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @return array<string, mixed|array<string, mixed>>
|
||||
*/
|
||||
private function mergeFontData(array $options): array
|
||||
{
|
||||
|
||||
@@ -15,12 +15,9 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
*/
|
||||
class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
public function __construct(private UserRepository $userRepository)
|
||||
public function __construct(private readonly UserRepository $userRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ trait RepositorySearchTrait
|
||||
$alias = 'meta' . $a++;
|
||||
$paramName = 'metaName' . $i++;
|
||||
$paramValue = 'metaValue' . $c++;
|
||||
$subqueryName = 'metaNotExists' . $metaName;
|
||||
|
||||
if ($metaValue === '*') {
|
||||
$qb->leftJoin($rootAlias . '.meta', $alias);
|
||||
@@ -76,7 +77,7 @@ trait RepositorySearchTrait
|
||||
$and->add($qb->expr()->isNotNull($alias . '.value'));
|
||||
} elseif ($metaValue === '~') {
|
||||
$and->add(
|
||||
\sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias)
|
||||
);
|
||||
} elseif ($metaValue === '' || $metaValue === null) {
|
||||
$qb->leftJoin($rootAlias . '.meta', $alias);
|
||||
@@ -86,7 +87,7 @@ trait RepositorySearchTrait
|
||||
$qb->expr()->eq($alias . '.name', ':' . $paramName),
|
||||
$qb->expr()->isNull($alias . '.value')
|
||||
),
|
||||
\sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias)
|
||||
)
|
||||
);
|
||||
$qb->setParameter($paramName, $metaName);
|
||||
|
||||
@@ -35,7 +35,6 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @extends EntityRepository<User>
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
|
||||
|
||||
@@ -21,18 +21,10 @@ class WorkingTimeRepository extends EntityRepository
|
||||
{
|
||||
private bool $pendingUpdate = false;
|
||||
|
||||
public function deleteWorkingTime(WorkingTime $workingTime): void
|
||||
public function scheduleWorkingTimeDelete(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function saveWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($workingTime);
|
||||
$entityManager->flush();
|
||||
$this->pendingUpdate = true;
|
||||
$this->getEntityManager()->remove($workingTime);
|
||||
}
|
||||
|
||||
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
|
||||
|
||||
@@ -19,7 +19,6 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
@@ -29,7 +28,7 @@ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgrader
|
||||
/**
|
||||
* @param iterable<UserProviderInterface<User>> $providers
|
||||
*/
|
||||
public function __construct(private iterable $providers, private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly iterable $providers, private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -166,12 +166,7 @@ final class DateTimeFactory
|
||||
return $date->modify('23:59:59');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format
|
||||
* @param null|string $datetime
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): bool|DateTime
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): false|DateTime
|
||||
{
|
||||
return DateTime::createFromFormat($format, $datetime ?? 'now', $this->getTimezone());
|
||||
}
|
||||
|
||||
@@ -24,15 +24,17 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ThemeExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration, private Security $security)
|
||||
public function __construct(
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly Security $security
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Environment $environment
|
||||
* @param string $eventName
|
||||
* @param array<string, mixed> $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function trigger(Environment $environment, string $eventName, array $payload = []): ThemeEvent
|
||||
{
|
||||
@@ -61,7 +63,7 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function getJavascriptTranslations(): array
|
||||
{
|
||||
$event = new ThemeJavascriptTranslationsEvent();
|
||||
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
@@ -97,7 +99,7 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function generateTitle(?string $prefix = null, string $delimiter = ' – '): string
|
||||
{
|
||||
return ($prefix ?? '') . Constants::SOFTWARE . $delimiter . $this->translator->trans('time_tracking', [], 'messages');
|
||||
return ($prefix ?? '') . Constants::SOFTWARE;
|
||||
}
|
||||
|
||||
public function colorize(?string $color, ?string $identifier = null): string
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Event\WorkingTimeApproveMonthEvent;
|
||||
use App\Event\WorkingTimeQueryStatsEvent;
|
||||
use App\Event\WorkingTimeUnlockMonthEvent;
|
||||
use App\Event\WorkingTimeYearEvent;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -155,12 +156,14 @@ final class WorkingTimeService
|
||||
$dayDate = $day->getDay();
|
||||
$result = new WorkingTime($user, $dayDate);
|
||||
|
||||
if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) {
|
||||
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
|
||||
}
|
||||
if ($dayDate <= $until) {
|
||||
if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) {
|
||||
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
|
||||
}
|
||||
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$day->setWorkingTime($result);
|
||||
@@ -181,6 +184,7 @@ final class WorkingTimeService
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
// deprecated 3.0 remove $user, fetch from $month->getUser() instead
|
||||
public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approvedBy): void
|
||||
{
|
||||
foreach ($month->getDays() as $day) {
|
||||
@@ -189,10 +193,6 @@ final class WorkingTimeService
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($workingTime->getId() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($month->isLocked() || $workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
@@ -205,15 +205,38 @@ final class WorkingTimeService
|
||||
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
|
||||
// $user = $month->getUser();
|
||||
$user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT));
|
||||
$this->userRepository->saveUser($user);
|
||||
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approvedBy));
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($month, $approvedBy));
|
||||
}
|
||||
|
||||
public function unlockMonth(Month $month, User $unlockedBy): void
|
||||
{
|
||||
foreach ($month->getDays() as $day) {
|
||||
$workingTime = $day->getWorkingTime();
|
||||
if ($workingTime === null || $workingTime->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->workingTimeRepository->scheduleWorkingTimeDelete($workingTime);
|
||||
}
|
||||
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
|
||||
$user = $month->getUser();
|
||||
$user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT));
|
||||
$this->userRepository->saveUser($user);
|
||||
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeUnlockMonthEvent($month, $unlockedBy));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $year
|
||||
* @param User $user
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function getYearStatistics(\DateTimeInterface $year, User $user): array
|
||||
|
||||
@@ -98,10 +98,7 @@
|
||||
{% for month in year.months %}
|
||||
{% set monthShould = month.expectedTime(now) %}
|
||||
{% set monthIs = month.actualTime %}
|
||||
{% set showStats = now > month.month or monthIs > 0 %}
|
||||
{% if showStats %}
|
||||
{% set yearShould = yearShould + monthShould %}
|
||||
{% endif %}
|
||||
{% set yearShould = yearShould + monthShould %}
|
||||
{% set yearIs = yearIs + monthIs %}
|
||||
{% if month.locked %}
|
||||
{% set approved = approved + 1 %}
|
||||
@@ -137,7 +134,6 @@
|
||||
{% for month in year.months %}
|
||||
{% set monthShould = month.expectedTime(now) %}
|
||||
{% set monthIs = month.actualTime %}
|
||||
{% set showStats = now > month.month or monthIs > 0 %}
|
||||
<tr>
|
||||
{% if withWorkHourConfiguration %}
|
||||
<td class="w-min">
|
||||
@@ -152,9 +148,7 @@
|
||||
{{ month.month|month_name }}
|
||||
</td>
|
||||
<td class="text-end total w-min">
|
||||
{% if showStats %}
|
||||
{{ work_times_result(monthShould, monthIs, decimal) }}
|
||||
{% endif %}
|
||||
{{ work_times_result(monthShould, monthIs, decimal) }}
|
||||
</td>
|
||||
<td class="text-end w-min">
|
||||
{{ monthShould|duration(decimal) }}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
<img alt="{{ get_title() }}" style="max-width: 150px;"
|
||||
src="{% if '://' in tabler_bundle.getLogoUrl() %}{{ tabler_bundle.getLogoUrl() }}{% else %}{{ asset(tabler_bundle.getLogoUrl()) }}{% endif %}">
|
||||
{% else %}
|
||||
<p>{{ get_title('', '<br>')|raw }}</p>
|
||||
<p>{{ get_title()|raw }}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
|
||||
{{ plugin.metadata.description }}
|
||||
{% if bundles[plugin.id] is defined and bundles[plugin.id]['latest_release'] is defined and bundles[plugin.id]['latest_release'] != plugin.metadata.version %}
|
||||
{{ widgets.alert('warning', 'update_available'|trans({'%version%': bundles[plugin.id]['latest_release']}, 'plugins')) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'version') }}">
|
||||
{{ widgets.label(plugin.metadata.version, 'primary', plugin.id) }}
|
||||
|
||||
@@ -51,7 +51,7 @@ class PasswordResetControllerTest extends AbstractControllerBaseTestCase
|
||||
|
||||
$content = $response->getContent();
|
||||
self::assertNotFalse($content);
|
||||
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
|
||||
self::assertStringContainsString('<title>Kimai</title>', $content);
|
||||
self::assertStringContainsString('Reset your password', $content);
|
||||
self::assertStringContainsString('<form class="card-body security-password-reset" action="/en/resetting/send-email" method="post" autocomplete="off">', $content);
|
||||
self::assertStringContainsString('<input autocomplete="username" type="text"', $content);
|
||||
|
||||
@@ -45,7 +45,7 @@ class SecurityControllerTest extends AbstractControllerBaseTestCase
|
||||
self::assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$content = $response->getContent();
|
||||
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
|
||||
self::assertStringContainsString('<title>Kimai</title>', $content);
|
||||
self::assertStringContainsString('<form action="/en/login_check" method="post"', $content);
|
||||
self::assertStringContainsString('<input autocomplete="username" type="text" id="username" name="_username"', $content);
|
||||
self::assertStringContainsString('<input autocomplete="new-password" id="password" name="_password" type="password"', $content);
|
||||
|
||||
@@ -56,7 +56,7 @@ class SelfRegistrationControllerTest extends AbstractControllerBaseTestCase
|
||||
self::assertTrue($response->isSuccessful());
|
||||
|
||||
$content = $response->getContent();
|
||||
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
|
||||
self::assertStringContainsString('<title>Kimai</title>', $content);
|
||||
self::assertStringContainsString('Register a new account', $content);
|
||||
self::assertStringContainsString('<form name="user_registration_form" method="post" action="/en/register/"', $content);
|
||||
self::assertStringContainsString('<input type="email"', $content);
|
||||
@@ -115,7 +115,7 @@ class SelfRegistrationControllerTest extends AbstractControllerBaseTestCase
|
||||
$this->createUser($client, 'example', 'register@example.com', 'test1234');
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
|
||||
self::assertStringContainsString('<title>Kimai</title>', $content);
|
||||
self::assertStringContainsString('An e-mail has been sent to register@example.com. It contains a link you must click to activate your account.', $content);
|
||||
self::assertStringContainsString('<a href="/en/login">', $content);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
|
||||
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$sut = new ThemeJavascriptTranslationsEvent();
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
|
||||
self::assertCount(self::COUNTER, $sut->getTranslations());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter(): void
|
||||
{
|
||||
$sut = new ThemeJavascriptTranslationsEvent();
|
||||
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
$sut->setTranslation('foo', 'bar');
|
||||
$sut->setTranslation('hello', 'world', 'testing');
|
||||
|
||||
|
||||
34
tests/Event/WorkingTimeApproveMonthEventTest.php
Normal file
34
tests/Event/WorkingTimeApproveMonthEventTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkingTimeApproveMonthEvent;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\WorkingTimeApproveMonthEvent
|
||||
*/
|
||||
class WorkingTimeApproveMonthEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$user = new User();
|
||||
$approver = new User();
|
||||
$approvalMonth = new Month(new \DateTimeImmutable('2023-02-10'), $user);
|
||||
|
||||
$sut = new WorkingTimeApproveMonthEvent($approvalMonth, $approver);
|
||||
|
||||
self::assertSame($user, $sut->getUser()); // @phpstan-ignore method.deprecated
|
||||
self::assertSame($approvalMonth, $sut->getMonth());
|
||||
self::assertSame($approver, $sut->getApprovedBy());
|
||||
}
|
||||
}
|
||||
33
tests/Event/WorkingTimeUnlockMonthEventTest.php
Normal file
33
tests/Event/WorkingTimeUnlockMonthEventTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkingTimeUnlockMonthEvent;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\WorkingTimeUnlockMonthEvent
|
||||
*/
|
||||
class WorkingTimeUnlockMonthEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$user = new User();
|
||||
$approver = new User();
|
||||
$approvalMonth = new Month(new \DateTimeImmutable('2023-02-10'), $user);
|
||||
|
||||
$sut = new WorkingTimeUnlockMonthEvent($approvalMonth, $approver);
|
||||
|
||||
self::assertSame($approvalMonth, $sut->getMonth());
|
||||
self::assertSame($approver, $sut->getUnlockedBy());
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ abstract class AbstractActionsSubscriberTestCase extends TestCase
|
||||
protected function createSubscriber(string $className, ...$grants): AbstractActionsSubscriber
|
||||
{
|
||||
$auth = $this->createMock(AuthorizationCheckerInterface::class);
|
||||
$auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants);
|
||||
$auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants); // @phpstan-ignore argument.named
|
||||
$router = $this->createMock(UrlGeneratorInterface::class);
|
||||
$router->method('generate')->willReturnArgument(0);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class EmailSubscriberTest extends TestCase
|
||||
$events = EmailSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(EmailEvent::class, $events);
|
||||
$methodName = $events[EmailEvent::class][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(EmailSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ class LastLoginSubscriberTest extends TestCase
|
||||
|
||||
self::assertArrayHasKey(UserInteractiveLoginEvent::class, $events);
|
||||
$methodName = $events[UserInteractiveLoginEvent::class];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
|
||||
|
||||
self::assertArrayHasKey(LoginSuccessEvent::class, $events);
|
||||
$methodName = $events[LoginSuccessEvent::class];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class MenuSubscriberTest extends TestCase
|
||||
$events = MenuSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(ConfigureMainMenuEvent::class, $events);
|
||||
$methodName = $events[ConfigureMainMenuEvent::class][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(MenuSubscriber::class, $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class PagerfantaExceptionSubscriberTest extends TestCase
|
||||
$events = PagerfantaExceptionSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(KernelEvents::EXCEPTION, $events);
|
||||
$methodName = $events[KernelEvents::EXCEPTION][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(PagerfantaExceptionSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class ProfileSubscriberTest extends TestCase
|
||||
|
||||
self::assertArrayHasKey(LoginSuccessEvent::class, $events);
|
||||
$methodName = $events[LoginSuccessEvent::class];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class UserDetailsSubscriberTest extends TestCase
|
||||
$events = UserDetailsSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(UserDetailsEvent::class, $events);
|
||||
$methodName = $events[UserDetailsEvent::class][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(UserDetailsSubscriber::class, $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class UserPreferenceSubscriberTest extends TestCase
|
||||
$events = UserPreferenceSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(PrepareUserEvent::class, $events);
|
||||
$methodName = $events[PrepareUserEvent::class][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(UserPreferenceSubscriber::class, $methodName));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class WizardSubscriberTest extends TestCase
|
||||
$events = WizardSubscriber::getSubscribedEvents();
|
||||
self::assertArrayHasKey(KernelEvents::REQUEST, $events);
|
||||
$methodName = $events[KernelEvents::REQUEST][0];
|
||||
self::assertIsString($methodName);
|
||||
self::assertTrue(method_exists(WizardSubscriber::class, $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
'0',
|
||||
'0',
|
||||
'84',
|
||||
'Kevin',
|
||||
'kevin',
|
||||
'',
|
||||
'Customer Name',
|
||||
@@ -142,6 +143,7 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
'0',
|
||||
'0',
|
||||
'-100.92',
|
||||
'niveK',
|
||||
'nivek',
|
||||
'',
|
||||
'Customer Name',
|
||||
@@ -168,6 +170,6 @@ class CsvRendererTest extends AbstractRendererTestCase
|
||||
self::assertEquals($expected, $all[5]);
|
||||
self::assertEquals($expected2, $all[6]);
|
||||
self::assertEquals(\count($expected), \count($all[0]));
|
||||
self::assertEquals('foo', $all[4][16]);
|
||||
self::assertEquals('foo', $all[4][17]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,19 @@ abstract class AbstractRendererTestCase extends KernelTestCase
|
||||
$activity->setProject($project);
|
||||
$activity->setMetaField((new ActivityMeta())->setName('activity-foo')->setValue('activity-bar')->setIsVisible(true));
|
||||
|
||||
$userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier'];
|
||||
$userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier', 'getAlias'];
|
||||
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||
$user1->method('getId')->willReturn(1);
|
||||
$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
$user1->method('getAlias')->willReturn('Foo Bar');
|
||||
$user1->method('getUserIdentifier')->willReturn('foo-bar');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
|
||||
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||
$user2->method('getId')->willReturn(2);
|
||||
$user2->method('getUsername')->willReturn('hello-world');
|
||||
$user2->method('getAlias')->willReturn('Hello World');
|
||||
$user2->method('getUserIdentifier')->willReturn('hello-world');
|
||||
$user2->method('getUsername')->willReturn('hello-world');
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
@@ -111,6 +113,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase
|
||||
;
|
||||
|
||||
$userKevin = new User();
|
||||
$userKevin->setAlias('Kevin');
|
||||
$userKevin->setUserIdentifier('kevin');
|
||||
|
||||
$timesheet5 = new Timesheet();
|
||||
@@ -129,6 +132,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase
|
||||
;
|
||||
|
||||
$userNivek = new User();
|
||||
$userNivek->setAlias('niveK');
|
||||
$userNivek->setUserIdentifier('nivek');
|
||||
|
||||
$timesheet6 = new Timesheet();
|
||||
|
||||
@@ -32,6 +32,8 @@ class InvoiceModelTest extends TestCase
|
||||
self::assertNotNull($sut->getCustomer());
|
||||
self::assertNotNull($sut->getTemplate());
|
||||
|
||||
self::assertFalse($sut->isPreview());
|
||||
self::assertFalse($sut->isHideZeroTax());
|
||||
self::assertNull($sut->getCalculator());
|
||||
self::assertEmpty($sut->getEntries());
|
||||
self::assertIsArray($sut->getEntries());
|
||||
@@ -43,6 +45,12 @@ class InvoiceModelTest extends TestCase
|
||||
$sut->setFormatter($newFormatter);
|
||||
self::assertNotSame($formatter, $sut->getFormatter());
|
||||
self::assertSame($newFormatter, $sut->getFormatter());
|
||||
self::assertEquals([], $sut->getOptions());
|
||||
|
||||
$sut->setPreview(true);
|
||||
$sut->setHideZeroTax(true);
|
||||
self::assertTrue($sut->isPreview());
|
||||
self::assertTrue($sut->isHideZeroTax());
|
||||
}
|
||||
|
||||
public function testEmptyObjectThrowsExceptionOnNumberGenerator(): void
|
||||
@@ -75,6 +83,23 @@ class InvoiceModelTest extends TestCase
|
||||
|
||||
self::assertSame($template, $sut->getTemplate());
|
||||
self::assertInstanceOf(\DateTimeInterface::class, $sut->getDueDate());
|
||||
|
||||
$sut->setOption('foo-int', 123);
|
||||
$sut->setOption('foo-float', 123.45);
|
||||
$sut->setOption('foo-string', '12345');
|
||||
$sut->setOption('foo-null', null);
|
||||
$sut->setOption('foo-bool', true);
|
||||
$sut->setOption('foo-array', ['foo' => 'bar']);
|
||||
$sut->setOption('foo-array2', ['foo', 'bar']);
|
||||
self::assertEquals([
|
||||
'foo-int' => 123,
|
||||
'foo-float' => 123.45,
|
||||
'foo-string' => '12345',
|
||||
'foo-null' => null,
|
||||
'foo-bool' => true,
|
||||
'foo-array' => ['foo' => 'bar'],
|
||||
'foo-array2' => ['foo', 'bar']
|
||||
], $sut->getOptions());
|
||||
}
|
||||
|
||||
public function testDueDate(): void
|
||||
|
||||
@@ -50,7 +50,7 @@ class ThemeEventExtensionTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSut(bool $hasListener = true, string $title = null): ThemeExtension
|
||||
protected function getSut(bool $hasListener = true): ThemeExtension
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->method('hasListeners')->willReturn($hasListener);
|
||||
@@ -155,18 +155,6 @@ class ThemeEventExtensionTest extends TestCase
|
||||
public function testGetTitle(): void
|
||||
{
|
||||
$sut = $this->getSut(false);
|
||||
self::assertEquals('Kimai – foo', $sut->generateTitle());
|
||||
self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | '));
|
||||
self::assertEquals('<b>Kimai</b> ... foo', $sut->generateTitle('<b>', '</b> ... '));
|
||||
self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | '));
|
||||
}
|
||||
|
||||
public function testGetBrandedTitle(): void
|
||||
{
|
||||
$sut = $this->getSut(false, 'MyCompany');
|
||||
self::assertEquals('Kimai – foo', $sut->generateTitle());
|
||||
self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | '));
|
||||
self::assertEquals('<b>Kimai</b> ... foo', $sut->generateTitle('<b>', '</b> ... '));
|
||||
self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | '));
|
||||
self::assertEquals('Kimai', $sut->generateTitle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1311,51 +1311,11 @@ parameters:
|
||||
count: 1
|
||||
path: EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/EmailSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, array\\<int\\|string, int\\|string\\>\\|int\\|string\\>\\|string given\\.$#"
|
||||
count: 2
|
||||
path: EventSubscriber/LastLoginSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/MenuSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/PagerfantaExceptionSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\ProfileSubscriberTest\\:\\:getInvalidCookies\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/ProfileSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, array\\<int\\|string, int\\|string\\>\\|int\\|string\\>\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/ProfileSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/UserDetailsSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/UserPreferenceSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/WizardSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
|
||||
count: 6
|
||||
|
||||
@@ -614,6 +614,14 @@
|
||||
<source>complete_month.help</source>
|
||||
<target>Dies wird alle Tage des Jahres vor dem gewählten Datum abschliessen. Der Benutzer wird für den abgeschlossenen Zeitraum keine Zeiten mehr anlegen oder bearbeiten können.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4WlKIfD" resname="unlock_month" xml:space="preserve" approved="yes">
|
||||
<source>Unlock month</source>
|
||||
<target state="final">Monat entsperren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mhOe3Iy" resname="unlock_month.help">
|
||||
<source>unlock_month.help</source>
|
||||
<target>Dadurch werden alle Tage des Jahres nach dem ausgewählten Datum entsperrt.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eWmotjb" resname="completed_month_pdf">
|
||||
<source>completed_month_pdf</source>
|
||||
<target>Monats-Abschluss PDF</target>
|
||||
|
||||
@@ -614,6 +614,14 @@
|
||||
<source>complete_month.help</source>
|
||||
<target>This will lock all days of the year before the chosen date. The user will no longer be able to create or edit times for the locked period.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4WlKIfD" resname="unlock_month" xml:space="preserve" approved="yes">
|
||||
<source>Unlock month</source>
|
||||
<target state="final">Unlock month</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mhOe3Iy" resname="unlock_month.help">
|
||||
<source>unlock_month.help</source>
|
||||
<target>This unlocks all days of the year after the selected date.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eWmotjb" resname="completed_month_pdf">
|
||||
<source>completed_month_pdf</source>
|
||||
<target>Completed month PDF</target>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<source>Plugin Shop</source>
|
||||
<target>Mehr Erweiterungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="aNvGEaj" resname="update_available" xml:space="preserve">
|
||||
<source>Update available</source>
|
||||
<target>Update verfügbar: %version%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<source>Plugin Shop</source>
|
||||
<target>More extensions</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="aNvGEaj" resname="update_available" xml:space="preserve">
|
||||
<source>Update available</source>
|
||||
<target>Update available: %version%</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -158,6 +158,10 @@
|
||||
<source>An absence cannot be booked after the last working day.</source>
|
||||
<target>Eine Abwesenheit kann nicht nach dem letzten Arbeitstag gebucht werden.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mveJ4vW" resname="Selected period cannot be locked: unconfirmed absence requests are pending.">
|
||||
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
||||
<target>Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -158,6 +158,10 @@
|
||||
<source>An absence cannot be booked after the last working day.</source>
|
||||
<target>An absence cannot be booked after the last working day.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mveJ4vW" resname="Selected period cannot be locked: unconfirmed absence requests are pending.">
|
||||
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
||||
<target>Selected period cannot be locked: unconfirmed absence requests are pending.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user