mirror of
https://github.com/kevinpapst/kimai2.git
synced 2025-03-16 14:03:30 +00:00
Release 2.27 (#5212)
This commit is contained in:
parent
4fdfb6f478
commit
4332ef95a2
56 changed files with 354 additions and 256 deletions
phpstan.neon
src
Configuration
Constants.phpCustomer
Event
Form/Toolbar
Invoice
Hydrator
Renderer
Mail
Project
Repository
Twig
Utils
templates/default
tests
translations
email.de.xlfemail.en.xlfemail.es.xlfemail.he.xlfemail.hr.xlfemail.id.xlfemail.it.xlfemail.pt_BR.xlfemail.uk.xlfmessages.cs.xlfmessages.da.xlfmessages.de.xlfmessages.en.xlfmessages.es.xlfmessages.fi.xlfmessages.fr.xlfmessages.he.xlfmessages.hr.xlfmessages.id.xlfmessages.it.xlfmessages.nl.xlfmessages.pl.xlfmessages.pt.xlfmessages.pt_BR.xlfmessages.ro.xlfmessages.ru.xlfmessages.sk.xlfmessages.sv.xlfmessages.tr.xlfmessages.uk.xlfmessages.vi.xlfmessages.zh_CN.xlfmessages.zh_Hant.xlf
|
@ -3032,11 +3032,6 @@ parameters:
|
|||
count: 1
|
||||
path: src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getEnd\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydrator\\:\\:getBudgetValues\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -15,9 +15,15 @@
|
|||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ form_errors(form) }}
|
||||
{% block form_body %}
|
||||
{{ form_rest(form) }}
|
||||
{% block form_body_outer %}
|
||||
{% block form_body_pre %}
|
||||
{{ form_errors(form) }}
|
||||
{% endblock %}
|
||||
{% block form_body %}
|
||||
{{ form_rest(form) }}
|
||||
{% endblock %}
|
||||
{% block form_body_post %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
|
|
|
@ -16,9 +16,15 @@
|
|||
</div>
|
||||
<div class="modal-body">
|
||||
{% block modal_body %}
|
||||
{{ form_errors(form) }}
|
||||
{% block form_body %}
|
||||
{{ form_rest(form) }}
|
||||
{% block form_body_outer %}
|
||||
{% block form_body_pre %}
|
||||
{{ form_errors(form) }}
|
||||
{% endblock %}
|
||||
{% block form_body %}
|
||||
{{ form_rest(form) }}
|
||||
{% endblock %}
|
||||
{% block form_body_post %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
32
tests/Ldap/LdapBadgeTest.php
Normal file
32
tests/Ldap/LdapBadgeTest.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?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\Ldap;
|
||||
|
||||
use App\Ldap\LdapBadge;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Ldap\LdapBadge
|
||||
*/
|
||||
class LdapBadgeTest extends TestCase
|
||||
{
|
||||
public function testMarkResolvedSetsResolvedToTrue(): void
|
||||
{
|
||||
$badge = new LdapBadge();
|
||||
$badge->markResolved();
|
||||
self::assertTrue($badge->isResolved());
|
||||
}
|
||||
|
||||
public function testIsResolvedReturnsFalseInitially(): void
|
||||
{
|
||||
$badge = new LdapBadge();
|
||||
self::assertFalse($badge->isResolved());
|
||||
}
|
||||
}
|
|
@ -22,16 +22,19 @@ use Symfony\Component\Mime\Email;
|
|||
*/
|
||||
class KimaiMailerTest extends TestCase
|
||||
{
|
||||
public function getSut(): KimaiMailer
|
||||
public function getSut(?MailerInterface $mailer = null): KimaiMailer
|
||||
{
|
||||
$config = new MailConfiguration('zippel@example.com');
|
||||
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
if ($mailer === null) {
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
$mailer->expects(self::once())->method('send');
|
||||
}
|
||||
|
||||
return new KimaiMailer($config, $mailer);
|
||||
}
|
||||
|
||||
public function testSendSetsFrom(): void
|
||||
public function testSendSetsFromHeaderFromFallback(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('Testing');
|
||||
|
@ -47,4 +50,83 @@ class KimaiMailerTest extends TestCase
|
|||
|
||||
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $message->getFrom());
|
||||
}
|
||||
|
||||
public function testSendToUserSetsFromHeaderFromFallback(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('Testing');
|
||||
$user->setEmail('foo@example.com');
|
||||
$user->setAlias('Super User');
|
||||
$user->setEnabled(true);
|
||||
|
||||
$mailer = $this->getSut();
|
||||
$message = new Email();
|
||||
|
||||
$mailer->sendToUser($user, $message);
|
||||
|
||||
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $message->getFrom());
|
||||
}
|
||||
|
||||
public function testSendToUserSendsEmailWhenUserIsEnabledAndHasEmail(): void
|
||||
{
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('isEnabled')->willReturn(true);
|
||||
$user->method('getEmail')->willReturn('foo-bar@example.com');
|
||||
|
||||
$email = new Email();
|
||||
self::assertEquals([], $email->getTo());
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
$mailer->expects(self::once())->method('send')->with($email);
|
||||
|
||||
$sut = $this->getSut($mailer);
|
||||
|
||||
$sut->sendToUser($user, $email);
|
||||
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $email->getFrom());
|
||||
self::assertEquals([new Address('foo-bar@example.com')], $email->getTo());
|
||||
}
|
||||
|
||||
public function testSendToUserDoesNotSendEmailWhenUserIsDisabled(): void
|
||||
{
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('isEnabled')->willReturn(false);
|
||||
$user->method('getEmail')->willReturn('user@example.com');
|
||||
|
||||
$email = new Email();
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
$mailer->expects(self::never())->method('send');
|
||||
$sut = $this->getSut($mailer);
|
||||
|
||||
$sut->sendToUser($user, $email);
|
||||
}
|
||||
|
||||
public function testSendToUserDoesNotSendEmailWhenUserHasNoEmail(): void
|
||||
{
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('isEnabled')->willReturn(true);
|
||||
$user->method('getEmail')->willReturn(null);
|
||||
|
||||
$email = new Email();
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
$mailer->expects(self::never())->method('send');
|
||||
$sut = $this->getSut($mailer);
|
||||
|
||||
$sut->sendToUser($user, $email);
|
||||
}
|
||||
|
||||
public function testSThrowsOnEmptyFromAddress(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Missing email "from" address');
|
||||
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('isEnabled')->willReturn(true);
|
||||
$user->method('getEmail')->willReturn('test@example.com');
|
||||
|
||||
$email = new Email();
|
||||
$config = new MailConfiguration('');
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
$sut = new KimaiMailer($config, $mailer);
|
||||
|
||||
$sut->sendToUser($user, $email);
|
||||
}
|
||||
}
|
||||
|
|
38
tests/Plugin/PackageTest.php
Normal file
38
tests/Plugin/PackageTest.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?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\Plugin;
|
||||
|
||||
use App\Plugin\Package;
|
||||
use App\Plugin\PluginMetadata;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Plugin\Package
|
||||
*/
|
||||
class PackageTest extends TestCase
|
||||
{
|
||||
public function testGetPackageFileReturnsCorrectFile(): void
|
||||
{
|
||||
$fileInfo = new \SplFileInfo('path/to/package.zip');
|
||||
$metadata = $this->createMock(PluginMetadata::class);
|
||||
$package = new Package($fileInfo, $metadata);
|
||||
|
||||
self::assertSame($fileInfo, $package->getPackageFile());
|
||||
}
|
||||
|
||||
public function testGetMetadataReturnsCorrectMetadata(): void
|
||||
{
|
||||
$fileInfo = new \SplFileInfo('path/to/package.zip');
|
||||
$metadata = $this->createMock(PluginMetadata::class);
|
||||
$package = new Package($fileInfo, $metadata);
|
||||
|
||||
self::assertSame($metadata, $package->getMetadata());
|
||||
}
|
||||
}
|
|
@ -65,6 +65,7 @@ class PaginationExtensionTest extends TestCase
|
|||
|
||||
$values = array_fill(0, 151, 'blub');
|
||||
$pagerfanta = new Pagination(new ArrayAdapter($values));
|
||||
$pagerfanta->setMaxPerPage(10);
|
||||
$result = $sut->renderPagination($pagerfanta, [
|
||||
'css_container_class' => 'pagination pagination-sm inline',
|
||||
'routeName' => 'project_activities',
|
||||
|
@ -98,6 +99,7 @@ class PaginationExtensionTest extends TestCase
|
|||
|
||||
$values = array_fill(0, 151, 'blub');
|
||||
$pagerfanta = new Pagination(new ArrayAdapter($values));
|
||||
$pagerfanta->setMaxPerPage(10);
|
||||
$result = $sut->renderPagination($pagerfanta, [
|
||||
'css_container_class' => 'pagination pagination-sm inline',
|
||||
'routeName' => 'project_activities',
|
||||
|
@ -106,6 +108,26 @@ class PaginationExtensionTest extends TestCase
|
|||
$this->assertPaginationHtml($result);
|
||||
}
|
||||
|
||||
public function testRenderPaginationWithoutPageSize(): void
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
|
||||
$values = array_fill(0, 151, 'blub');
|
||||
$pagerfanta = new Pagination(new ArrayAdapter($values));
|
||||
$result = $sut->renderPagination($pagerfanta, [
|
||||
'css_container_class' => 'pagination pagination-sm inline',
|
||||
'routeName' => 'project_activities',
|
||||
'routeParams' => ['id' => 137]
|
||||
]);
|
||||
|
||||
$expected =
|
||||
'<ul class="pagination pagination-sm inline"><li class="page-item disabled"><span class="page-link pagination-link"><i class="fas fa-chevron-left"></i></span></li>' .
|
||||
'<li class="page-item active"><a class="page-link pagination-link" href="project_activities?id=137&page=1">1</a></li>' .
|
||||
'<li class="page-item disabled"><span class="page-link pagination-link"><i class="fas fa-chevron-right"></i></span></li></ul>';
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function testRenderPaginationWithoutRouteName(): void
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
|
|
|
@ -72,7 +72,7 @@ Bitte überprüfen Sie diese unter: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Ihre Genehmigungsanfrage wurde von %user% abgelehnt.
|
||||
<target state="translated">Ihre Genehmigungsanfrage wurde von %created_by% abgelehnt.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Bitte überprüfen Sie ihre Anfragen unter: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Ihre Anfrage wurde von %user% genehmigt.
|
||||
<target state="translated">Ihre Anfrage wurde von %created_by% genehmigt.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ Please review them at: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Your authorisation request has been rejected by %user%.
|
||||
<target state="translated">Your authorisation request has been rejected by %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Please review your requests at: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Your request has been approved by %user%.
|
||||
<target state="translated">Your request has been approved by %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ Por favor revísalos en: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="no">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Su solicitud de autorización ha sido rechazada por %user%.
|
||||
<target state="translated">Su solicitud de autorización ha sido rechazada por %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -80,7 +80,7 @@ Por favor revise sus solicitudes en: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="no">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Su solicitud ha sido aprobada por %user%.
|
||||
<target state="translated">Su solicitud ha sido aprobada por %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ Please review them at: %url%</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="yes">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="final">בקשת האימות נדחתה על ידי המשתמש %user%.
|
||||
<target state="final">בקשת האימות נדחתה על ידי המשתמש %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Please review them at: %url%</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="yes">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="final">הבקשה שלך אושרה על ידי %user%.
|
||||
<target state="final">הבקשה שלך אושרה על ידי %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ Pregledaj ih na: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">%user% je odbio/la tvoj zahtjev za autorizaciju.
|
||||
<target state="translated">%created_by% je odbio/la tvoj zahtjev za autorizaciju.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Pregledaj tvoje zahtjeve na: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">%user% je odobrio/la tvoj zahtjev.
|
||||
<target state="translated">%created_by% je odobrio/la tvoj zahtjev.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ Silakan tinjau di: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Permintaan otorisasi Anda telah ditolak oleh %user%.
|
||||
<target state="translated">Permintaan otorisasi Anda telah ditolak oleh %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Silakan tinjau permintaan Anda di: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Permintaan Anda telah disetujui oleh %user%.
|
||||
<target state="translated">Permintaan Anda telah disetujui oleh %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ Controllali qui: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="yes">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="final">La tua richiesta è stata approvata da %user%.
|
||||
<target state="final">La tua richiesta è stata approvata da %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -72,7 +72,7 @@ Puoi trovare tutte le tue richieste qui: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="yes">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="final">La tua richiesta di autorizzazione è stata rifiutata da %user%.
|
||||
<target state="final">La tua richiesta di autorizzazione è stata rifiutata da %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ Favor revisá-los em: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="no">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">Sua solicitação foi aprovada pelo %user%.
|
||||
<target state="translated">Sua solicitação foi aprovada pelo %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Você pode encontrar todas as suas solicitações em: %url%</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="no">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Sua solicitação de autorização foi recusada pelo %user%.
|
||||
<target state="translated">Sua solicitação de autorização foi recusada pelo %created_by%.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ Please review them at: %url%</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
|
||||
<source>approval_rejected_message</source>
|
||||
<target state="translated">Ваш запит на авторизацію %user% скасовує.
|
||||
<target state="translated">Ваш запит на авторизацію %created_by% скасовує.
|
||||
|
||||
%list%
|
||||
|
||||
|
@ -84,7 +84,7 @@ Please review them at: %url%</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
|
||||
<source>approval_approved_message</source>
|
||||
<target state="translated">%user% схвалює Ваш запит.
|
||||
<target state="translated">%created_by% схвалює Ваш запит.
|
||||
|
||||
%list%
|
||||
|
||||
|
|
|
@ -1462,10 +1462,6 @@
|
|||
<source>complete_month.help</source>
|
||||
<target state="translated">Toto uzamkne všechny dny roku předcházející vybrané datum. Uživatel dál nebude moci vytvářet ani upravovat časy pro uzamčené období.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Odpracované hodiny</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
|
||||
<source>work_times_should.none_configured</source>
|
||||
<target state="translated">Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny.</target>
|
||||
|
|
|
@ -1026,10 +1026,6 @@
|
|||
<source>Review</source>
|
||||
<target state="translated">Gennemse</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Timer arbejdet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0ZWvn12" resname="notifications.welcome" xml:space="preserve">
|
||||
<source>notifications.welcome</source>
|
||||
<target state="translated">Tak for at du har tilmedt dig notifikationer!</target>
|
||||
|
|
|
@ -650,10 +650,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target>Geleistete Stunden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target>Geleistete Stunden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target>Bestätigt von %user% am %date%</target>
|
||||
|
@ -1938,6 +1934,14 @@
|
|||
<source>Log out of all devices</source>
|
||||
<target>Von allen Geräten abmelden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9qIU96X" resname="result">
|
||||
<source>result</source>
|
||||
<target>Ergebnis</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wvt4jH3" resname="training">
|
||||
<source>training</source>
|
||||
<target>Fortbildung</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -650,10 +650,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target>Hours worked</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target>Hours worked</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target>Confirmed by %user% at %date%</target>
|
||||
|
@ -1938,6 +1934,14 @@
|
|||
<source>Log out of all devices</source>
|
||||
<target>Log out of all devices</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9qIU96X" resname="result">
|
||||
<source>result</source>
|
||||
<target>Result</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wvt4jH3" resname="training">
|
||||
<source>training</source>
|
||||
<target>Training</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1534,10 +1534,6 @@
|
|||
<source>receive_from</source>
|
||||
<target state="translated">Recibir de %name%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Horas trabajadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yJg6P3m" resname="holiday.intro" xml:space="preserve">
|
||||
<source>You have submitted %used% of your available %days% vacation days so far.</source>
|
||||
<target state="translated">Has utilizado %used% de tus %days% días disponibles de vacaciones.</target>
|
||||
|
|
|
@ -1406,10 +1406,6 @@
|
|||
<source>favorite_routes</source>
|
||||
<target state="translated">Suosikit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Työskennellyt tunnit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yJg6P3m" resname="holiday.intro" xml:space="preserve">
|
||||
<source>You have submitted %used% of your available %days% vacation days so far.</source>
|
||||
<target state="needs-translation">Olet käyttänyt %used% vapaata saatavilla olevista %days% päivästä.</target>
|
||||
|
|
|
@ -1498,10 +1498,6 @@
|
|||
<source>manual_bookings.work_contract_intro</source>
|
||||
<target state="translated">Les réservations manuelles ne peuvent être ni modifiées ni supprimées !</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Heures travaillées</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="t1r372x" resname="invisible">
|
||||
<source>invisible</source>
|
||||
<target state="translated">Invisible</target>
|
||||
|
|
|
@ -1446,10 +1446,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target state="final">שעות עבודה בפועל</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve" approved="yes">
|
||||
<source>work_times_result</source>
|
||||
<target state="final">שעות עבודה</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="translated">אושר על ידי %user% ב־%date%</target>
|
||||
|
|
|
@ -1442,10 +1442,6 @@
|
|||
<source>manual_bookings</source>
|
||||
<target state="translated">Ručne rezervacije</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Odrađeni sati</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="31dFXhR" resname="work_times_is" xml:space="preserve">
|
||||
<source>work_times_is</source>
|
||||
<target state="translated">Odrađeni sati</target>
|
||||
|
|
|
@ -650,10 +650,6 @@
|
|||
<source>work_times_should.none_configured</source>
|
||||
<target state="translated">Tidak ada target jam yang ditetapkan pada pengguna ini dalam setelan kontrak pekerjaan.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Jam bekerja</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1c0EQaz" resname="profile.registration_date" xml:space="preserve">
|
||||
<source>profile.registration_date</source>
|
||||
<target state="translated">Teregistrasi di</target>
|
||||
|
|
|
@ -1482,10 +1482,6 @@
|
|||
<source>confirmed_by_at</source>
|
||||
<target state="final">Confermato da %user% il %date%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" approved="yes">
|
||||
<source>work_times_result</source>
|
||||
<target state="final">Ore lavorate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wZRRqg0" resname="manual_bookings" approved="yes">
|
||||
<source>manual_bookings</source>
|
||||
<target state="final">Prenotazioni manuali</target>
|
||||
|
|
|
@ -1430,10 +1430,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target state="translated">Gewerkte uren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Gewerkte uren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wZRRqg0" resname="manual_bookings">
|
||||
<source>manual_bookings</source>
|
||||
<target state="translated">Handmatige boekingen</target>
|
||||
|
|
|
@ -1574,10 +1574,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target state="translated">Przepracowane godziny</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Przepracowane godziny</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at" xml:space="preserve">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="translated">Potwierdzone przez %user% dnia %date%</target>
|
||||
|
|
|
@ -1630,10 +1630,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target state="translated">Horas realizadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Horas trabalhadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="translated">Confirmado por %user% em %date%</target>
|
||||
|
|
|
@ -1474,10 +1474,6 @@
|
|||
<source>completed_month_pdf</source>
|
||||
<target state="translated">PDF do mês concluído</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Horas trabalhadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
|
||||
<source>work_times_should.none_configured</source>
|
||||
<target state="translated">Nenhuma hora-alvo foi configurada para esse usuário nas configurações do contrato de trabalho.</target>
|
||||
|
|
|
@ -1406,10 +1406,6 @@
|
|||
<source>work_times_is</source>
|
||||
<target state="needs-translation">Timpul de lucru este</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Ore lucrate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at" xml:space="preserve">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="translated">Confirmat de %user% la %date%</target>
|
||||
|
|
|
@ -1354,10 +1354,6 @@
|
|||
<source>Expected number of hours</source>
|
||||
<target state="translated">Предполагаемые часы</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Отработанные часы</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="needs-translation">Подтверждено %user% в %date%</target>
|
||||
|
|
|
@ -1170,10 +1170,6 @@
|
|||
<source>export.warn_result_amount</source>
|
||||
<target state="translated">Vaše vyhľadávanie vedie k %count% výsledkom. Pokiaľ sa export nepodarí, musíte zúžiť vaše vyhľadávanie.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Odpracované hodiny</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
|
||||
<source>work_times_should.none_configured</source>
|
||||
<target state="translated">Pro tohoto uživateľa neboli v nastaveniach pracovnej zmluvy nakonfigurované žiadne cielové hodiny.</target>
|
||||
|
|
|
@ -1434,10 +1434,6 @@
|
|||
<source>confirmed_by_at</source>
|
||||
<target state="translated">Bekräftad %date% av %user%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Timmar arbetade</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="opDmYAa" resname="status.approved">
|
||||
<source>status.approved</source>
|
||||
<target state="translated">Godkänd</target>
|
||||
|
|
|
@ -1470,10 +1470,6 @@
|
|||
<source>work_times_should.none_configured</source>
|
||||
<target state="translated">İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Çalışılan saatler</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help">
|
||||
<source>manual_bookings.duration_help</source>
|
||||
<target state="translated">Süre hem pozitif (is-time artması) hem de negatif (is-time azalması) olabilir.</target>
|
||||
|
|
|
@ -1438,10 +1438,6 @@
|
|||
<source>Expected number of hours</source>
|
||||
<target state="translated">Очікувані години</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Відпрацьовані години</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wZRRqg0" resname="manual_bookings">
|
||||
<source>manual_bookings</source>
|
||||
<target state="translated">Ручне бронювання</target>
|
||||
|
|
|
@ -1310,10 +1310,6 @@
|
|||
<source>evaluation</source>
|
||||
<target state="translated">Đánh giá</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">Số giờ làm việc</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="31dFXhR" resname="work_times_is" xml:space="preserve">
|
||||
<source>work_times_is</source>
|
||||
<target state="needs-translation">Giờ làm việc thực tế</target>
|
||||
|
|
|
@ -1478,10 +1478,6 @@
|
|||
<source>manual_bookings.duration_help</source>
|
||||
<target state="translated">持续时间既可以是正的(增加的是时间)也可以是负的(减少的是时间)。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target state="translated">工作时间</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target state="translated">由 %user% 在 %date% 确认</target>
|
||||
|
|
|
@ -1410,10 +1410,6 @@
|
|||
<source>unit_price</source>
|
||||
<target state="final">單價</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve" approved="yes">
|
||||
<source>work_times_result</source>
|
||||
<target state="final">工作時數</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="XE01Ar1" resname="parental" xml:space="preserve" approved="yes">
|
||||
<source>parental</source>
|
||||
<target state="final">育嬰假</target>
|
||||
|
|
Loading…
Reference in a new issue