Release 2.9.0 (#4526)

* added fix to work around bc break in new phpword version
* fix phpoffice deprecations
* mark unused option as deprecated
* support for DateTimeInterface and DateTimeImmutable where possible
* use TRUSTED_PROXIES setting - fixes #4533
* re-enable Kimai test in docker test build script (#4541)
* bump dependencies
This commit is contained in:
Kevin Papst
2024-01-10 12:43:07 +01:00
committed by GitHub
parent c6a651456e
commit 6531e7fe52
46 changed files with 487 additions and 485 deletions

View File

@@ -16,7 +16,8 @@ use App\Model\ActivityBudgetStatisticModel;
use App\Model\ActivityStatistic;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -26,19 +27,15 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class ActivityStatisticService
{
public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher)
public function __construct(private readonly TimesheetRepository $timesheetRepository, private readonly EventDispatcherInterface $dispatcher)
{
}
/**
* WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range".
*
* @param Activity $activity
* @param DateTime|null $begin
* @param DateTime|null $end
* @return ActivityStatistic
* 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 getActivityStatistics(Activity $activity, ?DateTime $begin = null, ?DateTime $end = null): ActivityStatistic
public function getActivityStatistics(Activity $activity, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): ActivityStatistic
{
$statistics = $this->getBudgetStatistic([$activity], $begin, $end);
$event = new ActivityStatisticEvent($activity, array_pop($statistics), $begin, $end);
@@ -47,7 +44,7 @@ class ActivityStatisticService
return $event->getStatistic();
}
public function getBudgetStatisticModel(Activity $activity, DateTime $today): ActivityBudgetStatisticModel
public function getBudgetStatisticModel(Activity $activity, DateTimeInterface $today): ActivityBudgetStatisticModel
{
$stats = new ActivityBudgetStatisticModel($activity);
$stats->setStatisticTotal($this->getActivityStatistics($activity));
@@ -68,10 +65,9 @@ class ActivityStatisticService
/**
* @param Activity[] $activities
* @param DateTime $today
* @return ActivityBudgetStatisticModel[]
*/
public function getBudgetStatisticModelForActivities(array $activities, DateTime $today): array
public function getBudgetStatisticModelForActivities(array $activities, DateTimeInterface $today): array
{
$models = [];
$monthly = [];
@@ -121,11 +117,9 @@ class ActivityStatisticService
/**
* @param Activity[] $activities
* @param DateTime|null $begin
* @param DateTime|null $end
* @return array<int, ActivityStatistic>
*/
private function getBudgetStatistic(array $activities, ?DateTime $begin = null, ?DateTime $end = null): array
private function getBudgetStatistic(array $activities, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array
{
$statistics = [];
foreach ($activities as $activity) {
@@ -139,25 +133,25 @@ class ActivityStatisticService
if (null !== $result) {
foreach ($result as $resultRow) {
$statistic = $statistics[$resultRow['id']];
$statistic->setDuration($statistic->getDuration() + $resultRow['duration']);
$statistic->setRate($statistic->getRate() + $resultRow['rate']);
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
$statistic->addDuration((int) $resultRow['duration']);
$statistic->addRate((float) $resultRow['rate']);
$statistic->addInternalRate((float) $resultRow['internalRate']);
$statistic->addCounter((int) $resultRow['counter']);
if ($resultRow['billable']) {
$statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
$statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
$statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
$statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']);
$statistic->addDurationBillable((int) $resultRow['duration']);
$statistic->addRateBillable((float) $resultRow['rate']);
$statistic->addInternalRateBillable((float) $resultRow['internalRate']);
$statistic->addCounterBillable((int) $resultRow['counter']);
if ($resultRow['exported']) {
$statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']);
$statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']);
$statistic->addDurationBillableExported((int) $resultRow['duration']);
$statistic->addRateBillableExported((float) $resultRow['rate']);
}
}
if ($resultRow['exported']) {
$statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']);
$statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']);
$statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']);
$statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']);
$statistic->addDurationExported((int) $resultRow['duration']);
$statistic->addRateExported((float) $resultRow['rate']);
$statistic->addInternalRateExported((float) $resultRow['internalRate']);
$statistic->addCounterExported((int) $resultRow['counter']);
}
}
}
@@ -165,7 +159,7 @@ class ActivityStatisticService
return $statistics;
}
private function createStatisticQueryBuilder(array $activities, DateTime $begin = null, ?DateTime $end = null): QueryBuilder
private function createStatisticQueryBuilder(array $activities, \DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder
{
$qb = $this->timesheetRepository->createQueryBuilder('t');
$qb
@@ -187,14 +181,14 @@ class ActivityStatisticService
if ($begin !== null) {
$qb
->andWhere($qb->expr()->gte('t.begin', ':begin'))
->setParameter('begin', $begin, Types::DATETIME_MUTABLE)
->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE)
;
}
if ($end !== null) {
$qb
->andWhere($qb->expr()->lte('t.begin', ':end'))
->setParameter('end', $end, Types::DATETIME_MUTABLE)
->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE)
;
}

View File

@@ -158,10 +158,12 @@ final class ExportCreateCommand extends Command
return Command::FAILURE;
}
}
if (!$start instanceof \DateTime) {
if (!$start instanceof \DateTimeInterface) {
$start = $dateFactory->getStartOfMonth();
}
$start->setTime(0, 0, 0);
$start = \DateTimeImmutable::createFromInterface($start);
$start = $start->setTime(0, 0, 0);
$end = $input->getOption('end');
if (!empty($end)) {
@@ -174,11 +176,12 @@ final class ExportCreateCommand extends Command
}
}
if (empty($end)) {
if (!$end instanceof \DateTimeInterface) {
$end = $dateFactory->getEndOfMonth($start);
}
$end->setTime(23, 59, 59);
$end = \DateTimeImmutable::createFromInterface($end);
$end = $end->setTime(23, 59, 59);
$directory = rtrim(sys_get_temp_dir(), '/') . '/';
if ($input->getOption('directory') !== null) {

View File

@@ -64,7 +64,7 @@ final class InvoiceCreateCommand extends Command
->addOption('project', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of project IDs', null)
->addOption('by-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created')
->addOption('by-project', null, InputOption::VALUE_NONE, 'If set, one invoice for each active project in the given timerange is created')
->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the invoice items should be marked as exported')
->addOption('set-exported', null, InputOption::VALUE_NONE, '[DEPRECATED] this flag has no meaning any more: invoiced items are always exported')
->addOption('template', null, InputOption::VALUE_OPTIONAL, 'Invoice template', null)
->addOption('search', null, InputOption::VALUE_OPTIONAL, 'Search term to filter invoice entries', null)
->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for invoice entries (possible values: exported, all), by default only "not exported" items are fetched', null)
@@ -156,10 +156,12 @@ final class InvoiceCreateCommand extends Command
return Command::FAILURE;
}
}
if (!$start instanceof \DateTime) {
if (!$start instanceof \DateTimeInterface) {
$start = $dateFactory->getStartOfMonth();
}
$start->setTime(0, 0, 0);
$start = \DateTimeImmutable::createFromInterface($start);
$start = $start->setTime(0, 0, 0);
$end = $input->getOption('end');
if (!empty($end)) {
@@ -171,17 +173,18 @@ final class InvoiceCreateCommand extends Command
return Command::FAILURE;
}
}
if (!$end instanceof \DateTime) {
if (!$end instanceof \DateTimeInterface) {
$end = $dateFactory->getEndOfMonth();
}
$end->setTime(23, 59, 59);
$end = \DateTimeImmutable::createFromInterface($end);
$end = $end->setTime(23, 59, 59);
$searchTerm = null;
if (null !== $input->getOption('search')) {
$searchTerm = new SearchTerm($input->getOption('search'));
}
$markAsExported = false;
if ($input->getOption('preview') !== null) {
$this->previewUniqueFile = (bool) $input->getOption('preview-unique');
$this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/';
@@ -191,7 +194,7 @@ final class InvoiceCreateCommand extends Command
return Command::FAILURE;
}
} elseif ($input->getOption('set-exported')) {
$markAsExported = true;
@trigger_error('The "set-exported" option of kimai:invoice:create command has no meaning anymore, it will be removed soon', E_USER_DEPRECATED);
}
// =============== VALIDATION END ===============

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.8.0';
public const VERSION = '2.9.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20800;
public const VERSION_ID = 20900;
/**
* The software name
*/

View File

@@ -10,6 +10,7 @@
namespace App\Controller\Reporting;
use App\Controller\AbstractController;
use App\Entity\Customer;
use App\Form\Model\DateRange;
use App\Project\ProjectStatisticService;
use App\Reporting\ProjectDateRange\ProjectDateRangeForm;
@@ -37,15 +38,20 @@ final class ProjectDateRangeController extends AbstractController
]);
$form->submit($request->query->all(), false);
$begin = $query->getMonth() ?? $defaultStart;
$dateRange = new DateRange(true);
$dateRange->setBegin($query->getMonth() ?? $defaultStart);
$dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin()));
$dateRange->setBegin($begin);
$end = $dateFactory->getEndOfMonth($dateRange->getBegin()); // this resets the time
$dateRange->setEnd($end);
$projects = $service->findProjectsForDateRange($query, $dateRange);
$entries = $service->getBudgetStatisticModelForProjectsByDateRange($projects, $dateRange->getBegin(), $dateRange->getEnd(), $dateRange->getEnd());
$entries = $service->getBudgetStatisticModelForProjectsByDateRange($projects, $begin, $end, $end);
$byCustomer = [];
foreach ($entries as $entry) {
/** @var Customer $customer */
$customer = $entry->getProject()->getCustomer();
if (!isset($byCustomer[$customer->getId()])) {
$byCustomer[$customer->getId()] = ['customer' => $customer, 'projects' => []];

View File

@@ -17,6 +17,8 @@ use App\Model\CustomerStatistic;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
@@ -27,17 +29,12 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class CustomerStatisticService
{
public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher)
public function __construct(private readonly TimesheetRepository $timesheetRepository, private readonly EventDispatcherInterface $dispatcher)
{
}
/**
* WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range".
*
* @param Customer $customer
* @param DateTime|null $begin
* @param DateTime|null $end
* @return CustomerStatistic
* 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
{
@@ -48,13 +45,13 @@ class CustomerStatisticService
return $event->getStatistic();
}
public function getBudgetStatisticModel(Customer $customer, DateTime $today): CustomerBudgetStatisticModel
public function getBudgetStatisticModel(Customer $customer, DateTimeInterface $today): CustomerBudgetStatisticModel
{
$stats = new CustomerBudgetStatisticModel($customer);
$stats->setStatisticTotal($this->getCustomerStatistics($customer));
$begin = null;
$end = $today;
$end = DateTime::createFromInterface($today);
if ($customer->isMonthlyBudget()) {
$dateFactory = new DateTimeFactory($today->getTimezone());
@@ -69,11 +66,9 @@ class CustomerStatisticService
/**
* @param Customer[] $customers
* @param DateTime|null $begin
* @param DateTime|null $end
* @return array<int, CustomerStatistic>
*/
private function getBudgetStatistic(array $customers, ?DateTime $begin = null, ?DateTime $end = null): array
private function getBudgetStatistic(array $customers, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array
{
$statistics = [];
foreach ($customers as $customer) {
@@ -87,25 +82,25 @@ class CustomerStatisticService
if (null !== $result) {
foreach ($result as $resultRow) {
$statistic = $statistics[$resultRow['id']];
$statistic->setDuration($statistic->getDuration() + $resultRow['duration']);
$statistic->setRate($statistic->getRate() + $resultRow['rate']);
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
$statistic->addDuration((int) $resultRow['duration']);
$statistic->addRate((float) $resultRow['rate']);
$statistic->addInternalRate((float) $resultRow['internalRate']);
$statistic->addCounter((int) $resultRow['counter']);
if ($resultRow['billable']) {
$statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
$statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
$statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
$statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']);
$statistic->addDurationBillable((int) $resultRow['duration']);
$statistic->addRateBillable((float) $resultRow['rate']);
$statistic->addInternalRateBillable((float) $resultRow['internalRate']);
$statistic->addCounterBillable((int) $resultRow['counter']);
if ($resultRow['exported']) {
$statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']);
$statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']);
$statistic->addDurationBillableExported((int) $resultRow['duration']);
$statistic->addRateBillableExported((float) $resultRow['rate']);
}
}
if ($resultRow['exported']) {
$statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']);
$statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']);
$statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']);
$statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']);
$statistic->addDurationExported((int) $resultRow['duration']);
$statistic->addRateExported((float) $resultRow['rate']);
$statistic->addInternalRateExported((float) $resultRow['internalRate']);
$statistic->addCounterExported((int) $resultRow['counter']);
}
}
}
@@ -113,7 +108,7 @@ class CustomerStatisticService
return $statistics;
}
private function createStatisticQueryBuilder(array $customers, DateTime $begin = null, ?DateTime $end = null): QueryBuilder
private function createStatisticQueryBuilder(array $customers, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): QueryBuilder
{
$qb = $this->timesheetRepository->createQueryBuilder('t');
$qb
@@ -136,14 +131,14 @@ class CustomerStatisticService
if ($begin !== null) {
$qb
->andWhere($qb->expr()->gte('t.begin', ':begin'))
->setParameter('begin', $begin, Types::DATETIME_MUTABLE)
->setParameter('begin', DateTimeImmutable::createFromInterface($begin), Types::DATETIME_IMMUTABLE)
;
}
if ($end !== null) {
$qb
->andWhere($qb->expr()->lte('t.begin', ':end'))
->setParameter('end', $end, Types::DATETIME_MUTABLE)
->setParameter('end', DateTimeImmutable::createFromInterface($end), Types::DATETIME_IMMUTABLE)
;
}

View File

@@ -109,8 +109,7 @@ final class AppExtension extends Extension
$locales = explode('|', $container->getParameter('app_locales'));
$directory = $container->getParameter('kernel.project_dir');
$config = $directory . DIRECTORY_SEPARATOR . 'config/locales.php';
$settings = include $config;
$settings = include $directory . DIRECTORY_SEPARATOR . 'config/locales.php';
$appLocales = [];
$defaults = [

View File

@@ -13,13 +13,27 @@ use App\Model\ActivityBudgetStatisticModel;
final class ActivityBudgetStatisticEvent
{
private readonly ?\DateTime $begin;
private readonly ?\DateTime $end;
/**
* @param ActivityBudgetStatisticModel[] $models
* @param \DateTime|null $begin
* @param \DateTime|null $end
*/
public function __construct(private array $models, private ?\DateTime $begin = null, private ?\DateTime $end = null)
public function __construct(
private readonly array $models,
?\DateTimeInterface $begin = null,
?\DateTimeInterface $end = null
)
{
if ($begin !== null) {
$begin = \DateTime::createFromInterface($begin);
}
$this->begin = $begin;
if ($end !== null) {
$end = \DateTime::createFromInterface($end);
}
$this->end = $end;
}
public function getModel(int $activityId): ?ActivityBudgetStatisticModel

View File

@@ -11,12 +11,32 @@ namespace App\Event;
use App\Entity\Activity;
use App\Model\ActivityStatistic;
use DateTime;
use DateTimeInterface;
final class ActivityStatisticEvent extends AbstractActivityEvent
{
public function __construct(Activity $activity, private ActivityStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
private readonly ?DateTime $begin;
private readonly ?DateTime $end;
public function __construct(
Activity $activity,
private readonly ActivityStatistic $statistic,
?DateTimeInterface $begin = null,
?DateTimeInterface $end = null
)
{
parent::__construct($activity);
if ($begin !== null) {
$begin = DateTime::createFromInterface($begin);
}
$this->begin = $begin;
if ($end !== null) {
$end = DateTime::createFromInterface($end);
}
$this->end = $end;
}
public function getStatistic(): ActivityStatistic
@@ -24,12 +44,12 @@ final class ActivityStatisticEvent extends AbstractActivityEvent
return $this->statistic;
}
public function getBegin(): ?\DateTime
public function getBegin(): ?DateTime
{
return $this->begin;
}
public function getEnd(): ?\DateTime
public function getEnd(): ?DateTime
{
return $this->end;
}

View File

@@ -10,16 +10,32 @@
namespace App\Event;
use App\Model\ProjectBudgetStatisticModel;
use DateTime;
use DateTimeInterface;
final class ProjectBudgetStatisticEvent
{
private readonly ?DateTime $begin;
private readonly ?DateTime $end;
/**
* @param ProjectBudgetStatisticModel[] $models
* @param \DateTime|null $begin
* @param \DateTime|null $end
*/
public function __construct(private array $models, private ?\DateTime $begin = null, private ?\DateTime $end = null)
public function __construct(
private readonly array $models,
?DateTimeInterface $begin = null,
?DateTimeInterface $end = null
)
{
if ($begin !== null) {
$begin = \DateTime::createFromInterface($begin);
}
$this->begin = $begin;
if ($end !== null) {
$end = \DateTime::createFromInterface($end);
}
$this->end = $end;
}
public function getModel(int $projectId): ?ProjectBudgetStatisticModel
@@ -45,12 +61,12 @@ final class ProjectBudgetStatisticEvent
return $this->models;
}
public function getBegin(): ?\DateTime
public function getBegin(): ?DateTime
{
return $this->begin;
}
public function getEnd(): ?\DateTime
public function getEnd(): ?DateTime
{
return $this->end;
}

View File

@@ -14,7 +14,12 @@ use App\Model\ProjectStatistic;
final class ProjectStatisticEvent extends AbstractProjectEvent
{
public function __construct(Project $project, private ProjectStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
public function __construct(
Project $project,
private readonly ProjectStatistic $statistic,
private readonly ?\DateTimeInterface $begin = null,
private readonly ?\DateTimeInterface $end = null
)
{
parent::__construct($project);
}
@@ -24,12 +29,12 @@ final class ProjectStatisticEvent extends AbstractProjectEvent
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;
}

View File

@@ -186,7 +186,7 @@ abstract class AbstractSpreadsheetRenderer
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
// TODO why is that format hardcoded and does not depend on the users locale?
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD);
}
protected function setDurationTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void

View File

@@ -24,11 +24,11 @@ final class DateFormatter implements CellFormatterInterface
return;
}
if (!$value instanceof \DateTime) {
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
if (!$value instanceof \DateTimeInterface) {
throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported');
}
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD);
}
}

View File

@@ -25,11 +25,11 @@ final class DateTimeFormatter implements CellFormatterInterface
return;
}
if (!$value instanceof \DateTime) {
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
if (!$value instanceof \DateTimeInterface) {
throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported');
}
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
}
}

View File

@@ -25,8 +25,8 @@ final class TimeFormatter implements CellFormatterInterface
return;
}
if (!$value instanceof \DateTime) {
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
if (!$value instanceof \DateTimeInterface) {
throw new \InvalidArgumentException('Unsupported value given, only DateTimeInterface is supported');
}
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));

View File

@@ -26,9 +26,9 @@ final class DateRange implements EquatableInterface
return $this->begin;
}
public function setBegin(DateTime $begin): DateRange
public function setBegin(\DateTimeInterface $begin): DateRange
{
$this->begin = $begin;
$this->begin = DateTime::createFromInterface($begin);
if ($this->resetTimes) {
$this->begin->setTime(0, 0, 0);
}
@@ -41,9 +41,9 @@ final class DateRange implements EquatableInterface
return $this->end;
}
public function setEnd(DateTime $end): DateRange
public function setEnd(\DateTimeInterface $end): DateRange
{
$this->end = $end;
$this->end = DateTime::createFromInterface($end);
if ($this->resetTimes) {
$this->end->setTime(23, 59, 59);
}

View File

@@ -39,10 +39,12 @@ class DatePickerType extends AbstractType
return null;
}
if ($reverseTransform instanceof \DateTime && $options['force_time']) {
if ($reverseTransform instanceof \DateTimeInterface && $options['force_time']) {
if ($options['force_time'] === 'start') {
$reverseTransform = \DateTime::createFromInterface($reverseTransform);
$reverseTransform->setTime(0, 0, 0);
} elseif ($options['force_time'] === 'end') {
$reverseTransform = \DateTime::createFromInterface($reverseTransform);
$reverseTransform->setTime(23, 59, 59);
}
}

View File

@@ -48,7 +48,9 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
'customer.invoice_text' => $customer->getInvoiceText() ?? '',
];
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $model->getQuery()->getEnd());
/** @var \DateTime $end */
$end = $model->getQuery()->getEnd();
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end);
$values = array_merge($values, $this->getBudgetValues('customer.', $statistic, $model));

View File

@@ -57,7 +57,12 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface
$i++;
}
$cacheFile = $template->save();
$cacheFile = @tempnam(sys_get_temp_dir(), 'kimai-invoice-docx');
if (false === $cacheFile) {
throw new \Exception('Could not open temporary file');
}
$template->saveAs($cacheFile);
clearstatcache(true, $cacheFile);

View File

@@ -47,6 +47,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->counter = $counter;
}
public function addCounter(int $counter): void
{
$this->counter += $counter;
}
public function getCounterBillable(): int
{
return $this->counterBillable;
@@ -57,6 +62,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->counterBillable = $counter;
}
public function addCounterBillable(int $counter): void
{
$this->counterBillable += $counter;
}
public function getCounterExported(): int
{
return $this->counterExported;
@@ -67,6 +77,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->counterExported = $counter;
}
public function addCounterExported(int $counter): void
{
$this->counterExported += $counter;
}
/**
* For unified access, used in frontend.
*
@@ -82,6 +97,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordDuration = $duration;
}
public function addDuration(int $duration): void
{
$this->recordDuration += $duration;
}
/**
* For unified access, used in frontend.
*
@@ -107,6 +127,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordRate = $rate;
}
public function addRate(float $rate): void
{
$this->recordRate += $rate;
}
/**
* Returns the total internal rate of all included timesheet records.
*
@@ -127,6 +152,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->internalRateBillable = $internalRateBillable;
}
public function addInternalRateBillable(float $internalRateBillable): void
{
$this->internalRateBillable += $internalRateBillable;
}
public function getInternalRateExported(): float
{
return $this->internalRateExported;
@@ -137,11 +167,21 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->internalRateExported = $internalRateExported;
}
public function addInternalRateExported(float $internalRateExported): void
{
$this->internalRateExported += $internalRateExported;
}
public function setInternalRate(float $internalRate): void
{
$this->internalRate = $internalRate;
}
public function addInternalRate(float $internalRate): void
{
$this->internalRate += $internalRate;
}
public function getDurationBillable(): int
{
return $this->recordDurationBillable;
@@ -152,6 +192,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordDurationBillable = $recordDuration;
}
public function addDurationBillable(int $recordDuration): void
{
$this->recordDurationBillable += $recordDuration;
}
public function getDurationBillableExported(): int
{
return $this->recordDurationBillableExported;
@@ -162,6 +207,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordDurationBillableExported = $recordDuration;
}
public function addDurationBillableExported(int $recordDuration): void
{
$this->recordDurationBillableExported += $recordDuration;
}
public function getRateBillable(): float
{
return $this->recordRateBillable;
@@ -172,6 +222,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordRateBillable = $recordRate;
}
public function addRateBillable(float $recordRate): void
{
$this->recordRateBillable += $recordRate;
}
public function getRateBillableExported(): float
{
return $this->recordRateBillableExported;
@@ -182,6 +237,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordRateBillableExported = $recordRate;
}
public function addRateBillableExported(float $recordRate): void
{
$this->recordRateBillableExported += $recordRate;
}
public function getDurationExported(): int
{
return $this->recordDurationExported;
@@ -192,6 +252,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordDurationExported = $recordDuration;
}
public function addDurationExported(int $recordDuration): void
{
$this->recordDurationExported += $recordDuration;
}
public function getRateExported(): float
{
return $this->recordRateExported;
@@ -202,6 +267,11 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordRateExported = $recordRate;
}
public function addRateExported(float $recordRate): void
{
$this->recordRateExported += $recordRate;
}
public function jsonSerialize(): mixed
{
return [

View File

@@ -34,7 +34,8 @@ use App\Repository\ProjectRepository;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use App\Timesheet\DateTimeFactory;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Psr\EventDispatcher\EventDispatcherInterface;
@@ -54,14 +55,9 @@ class ProjectStatisticService
}
/**
* WARNING: this method does not respect the budget type. Your results will always be wither the "full lifetime data" or the "selected date-range".
*
* @param Project $project
* @param DateTime|null $begin
* @param DateTime|null $end
* @return ProjectStatistic
* 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 getProjectStatistics(Project $project, ?DateTime $begin = null, ?DateTime $end = null): ProjectStatistic
public function getProjectStatistics(Project $project, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): ProjectStatistic
{
$statistics = $this->getBudgetStatistic([$project], $begin, $end);
$event = new ProjectStatisticEvent($project, array_pop($statistics), $begin, $end);
@@ -71,14 +67,13 @@ class ProjectStatisticService
}
/**
* @param ProjectInactiveQuery $query
* @return Project[]
*/
public function findInactiveProjects(ProjectInactiveQuery $query): array
{
$user = $query->getUser();
$lastChange = clone $query->getLastChange();
$now = new DateTime('now', $lastChange->getTimezone());
$lastChange = DateTimeImmutable::createFromInterface($query->getLastChange());
$now = new DateTimeImmutable('now', $lastChange->getTimezone());
$qb2 = $this->projectRepository->createQueryBuilder('t1');
$qb2
@@ -101,15 +96,15 @@ class ProjectStatisticService
$qb->expr()->lte('p.start', ':project_start')
)
)
->setParameter('project_start', $now, Types::DATETIME_MUTABLE)
->setParameter('project_start', $now, Types::DATETIME_IMMUTABLE)
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('p.end'),
$qb->expr()->gte('p.end', ':project_end')
)
)
->setParameter('project_end', $now, Types::DATETIME_MUTABLE)
->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE)
->setParameter('project_end', $now, Types::DATETIME_IMMUTABLE)
->setParameter('begin', $lastChange, Types::DATETIME_IMMUTABLE)
;
$this->projectRepository->addPermissionCriteria($qb, $user);
@@ -125,7 +120,6 @@ class ProjectStatisticService
}
/**
* @param ProjectDateRangeQuery $query
* @return Project[]
*/
public function findProjectsForDateRange(ProjectDateRangeQuery $query, DateRange $dateRange): array
@@ -208,7 +202,7 @@ class ProjectStatisticService
return $projects;
}
public function getBudgetStatisticModel(Project $project, DateTime $today): ProjectBudgetStatisticModel
public function getBudgetStatisticModel(Project $project, DateTimeInterface $today): ProjectBudgetStatisticModel
{
$stats = new ProjectBudgetStatisticModel($project);
$stats->setStatisticTotal($this->getProjectStatistics($project));
@@ -229,10 +223,9 @@ class ProjectStatisticService
/**
* @param Project[] $projects
* @param DateTime $today
* @return ProjectBudgetStatisticModel[]
*/
public function getBudgetStatisticModelForProjects(array $projects, DateTime $today): array
public function getBudgetStatisticModelForProjects(array $projects, DateTimeInterface $today): array
{
$models = [];
$monthly = [];
@@ -282,12 +275,9 @@ class ProjectStatisticService
/**
* @param Project[] $projects
* @param DateTime $begin
* @param DateTime $end
* @param DateTime|null $totalsEnd
* @return ProjectBudgetStatisticModel[]
*/
public function getBudgetStatisticModelForProjectsByDateRange(array $projects, DateTime $begin, DateTime $end, ?DateTime $totalsEnd = null): array
public function getBudgetStatisticModelForProjectsByDateRange(array $projects, DateTimeInterface $begin, DateTimeInterface $end, ?DateTimeInterface $totalsEnd = null): array
{
$models = [];
@@ -313,11 +303,9 @@ class ProjectStatisticService
/**
* @param Project[] $projects
* @param DateTime|null $begin
* @param DateTime|null $end
* @return array<int, ProjectStatistic>
*/
public function getBudgetStatistic(array $projects, ?DateTime $begin = null, ?DateTime $end = null): array
public function getBudgetStatistic(array $projects, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array
{
$statistics = [];
foreach ($projects as $project) {
@@ -711,15 +699,13 @@ class ProjectStatisticService
}
/**
* @param User $user
* @param Project[] $projects
* @param DateTime $today
* @return ProjectViewModel[]
*/
public function getProjectView(User $user, array $projects, DateTime $today): array
public function getProjectView(User $user, array $projects, DateTimeInterface $today): array
{
$factory = DateTimeFactory::createByUser($user);
$today = clone $today;
$today = DateTimeImmutable::createFromInterface($today);
$startOfWeek = $factory->getStartOfWeek($today);
$endOfWeek = $factory->getEndOfWeek($today);

View File

@@ -0,0 +1,30 @@
<?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\Repository\Query;
use App\Form\Model\DateRange;
/**
* @internal
*/
interface DateRangeInterface
{
public function getBegin(): ?\DateTime;
public function setBegin(\DateTimeInterface $begin): void;
public function getEnd(): ?\DateTime;
public function setEnd(\DateTimeInterface $end): void;
public function getDateRange(): ?DateRange;
public function setDateRange(DateRange $dateRange): void;
}

View File

@@ -20,9 +20,9 @@ trait DateRangeTrait
return $this->dateRange?->getBegin();
}
public function setBegin(\DateTime $begin): void
public function setBegin(\DateTimeInterface $begin): void
{
$this->dateRange->setBegin($begin);
$this->dateRange->setBegin(\DateTime::createFromInterface($begin));
}
public function getEnd(): ?\DateTime
@@ -30,9 +30,9 @@ trait DateRangeTrait
return $this->dateRange?->getEnd();
}
public function setEnd(\DateTime $end): void
public function setEnd(\DateTimeInterface $end): void
{
$this->dateRange->setEnd($end);
$this->dateRange->setEnd(\DateTime::createFromInterface($end));
}
public function getDateRange(): ?DateRange

View File

@@ -16,7 +16,7 @@ use App\Form\Model\DateRange;
/**
* Query for created invoices.
*/
class InvoiceArchiveQuery extends BaseQuery
class InvoiceArchiveQuery extends BaseQuery implements DateRangeInterface
{
use DateRangeTrait;

View File

@@ -14,7 +14,7 @@ use App\Entity\Tag;
use App\Entity\User;
use App\Form\Model\DateRange;
class TimesheetQuery extends ActivityQuery implements BillableInterface
class TimesheetQuery extends ActivityQuery implements BillableInterface, DateRangeInterface
{
use BillableTrait;
use DateRangeTrait;

View File

@@ -62,10 +62,10 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
return [
new TwigTest('weekend', [$this, 'isWeekend']),
new TwigTest('today', function ($dateTime): bool {
if (!$dateTime instanceof \DateTime) {
if (!$dateTime instanceof \DateTimeInterface) {
return false;
}
$compare = new \DateTime('now', $dateTime->getTimezone());
$compare = new \DateTimeImmutable('now', $dateTime->getTimezone());
return $compare->format('Y-m-d') === $dateTime->format('Y-m-d');
}),

View File

@@ -58,8 +58,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
return;
}
$begin = $timesheet->getBegin();
// we can only work with stopped entries
if (null === $timesheet->getEnd() || null === $timesheet->getUser()) {
if ($begin === null || $timesheet->getEnd() === null || $timesheet->getUser() === null) {
return;
}
@@ -115,7 +117,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
if ($duration === $rawData['duration'] &&
$rate === $rawData['rate'] &&
$timesheet->isBillable() === $rawData['billable'] &&
$timesheet->getBegin()->format('Y.m.d') === $rawData['begin']->format('Y.m.d') &&
$begin->format('Y.m.d') === $rawData['begin']->format('Y.m.d') &&
$timesheet->getProject()->getId() === $projectId &&
($timesheet->getActivity() === null || $timesheet->getActivity()->getId() === $activityId)
) {
@@ -145,11 +147,11 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
}
}
$monthWasChanged = $timesheet->getBegin()->format('Y.m') !== $rawData['begin']->format('Y.m');
$monthWasChanged = $begin->format('Y.m') !== $rawData['begin']->format('Y.m');
}
$now = new DateTime('now', $timesheet->getBegin()->getTimezone());
$recordDate = $timesheet->getBegin();
$now = new DateTime('now', $begin->getTimezone());
$recordDate = $begin;
if (null !== ($activity = $timesheet->getActivity()) && $activity->hasBudgets()) {
$dateTime = $activity->isMonthlyBudget() ? $recordDate : $now;

View File

@@ -52,7 +52,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
$now = new \DateTime('now', $timesheetStart->getTimezone());
if (!empty($constraint->now)) {
if ($constraint->now instanceof \DateTime) {
if ($constraint->now instanceof \DateTimeInterface) {
$now = $constraint->now;
} elseif (\is_string($constraint->now)) {
try {