rounding rules via admin screen, round begin when starting record (#1229)
This commit is contained in:
@@ -21,11 +21,13 @@ kimai:
|
|||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# TIME-TRACKING
|
# TIME-TRACKING
|
||||||
# All configs related to time-tracking, timesheets and record management
|
# All configs related to time-tracking, timesheets and record management
|
||||||
|
# Most settings can be configured in the System configuration screen
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
timesheet:
|
timesheet:
|
||||||
|
|
||||||
# renders timesheet descriptions with markdown
|
# Allows to render timesheet descriptions with markdown
|
||||||
markdown_content: false
|
# This setting can be changed through the Administration screen
|
||||||
|
# markdown_content: false
|
||||||
|
|
||||||
# The time-tracking mode that should be used.
|
# The time-tracking mode that should be used.
|
||||||
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
|
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
|
||||||
@@ -42,36 +44,36 @@ kimai:
|
|||||||
# Rounding rules are used to round the begin & end dates and the duration for timesheet records.
|
# Rounding rules are used to round the begin & end dates and the duration for timesheet records.
|
||||||
# The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded.
|
# The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded.
|
||||||
# Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html
|
# Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html
|
||||||
rounding:
|
# rounding:
|
||||||
default:
|
# default:
|
||||||
days: ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']
|
# days: ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']
|
||||||
begin: 1
|
# begin: 1
|
||||||
end: 1
|
# end: 1
|
||||||
duration: 0
|
# duration: 0
|
||||||
mode: default
|
# mode: default
|
||||||
|
|
||||||
# # If you want to apply different hourly rates for specific weekdays, you can uncomment the "rates" configuration.
|
# If you want to apply different hourly rates for specific weekdays, you can uncomment the "rates" configuration.
|
||||||
# # The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays".
|
# The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays".
|
||||||
# # See documentation at: https://www.kimai.org/documentation/timesheet.html#rate-calculation
|
# See documentation at: https://www.kimai.org/documentation/timesheet.html#rate-calculation
|
||||||
# rates:
|
# rates:
|
||||||
# weekend:
|
# weekend:
|
||||||
# days: ['saturday','sunday']
|
# days: ['saturday','sunday']
|
||||||
# factor: 1.5
|
# factor: 1.5
|
||||||
#
|
|
||||||
# # If you want to limit the max. active entries per user, you can do it here.
|
# If you want to limit the max. active entries per user, you can do it here.
|
||||||
# # The soft_limit is used as theme setting and displays a warning color if the user has reached X active recordings
|
# The soft_limit is used as theme setting and displays a warning color if the user has reached X active recordings
|
||||||
# # The hard_limit is used to detect how many active records are allowed per user:
|
# The hard_limit is used to detect how many active records are allowed per user:
|
||||||
# # - by default a user can only have one active time-record: it is automatically stopped when a new one is started
|
# - by default a user can only have one active time-record: it is automatically stopped when a new one is started
|
||||||
# # - when hard_limit is > 1 and the user is trying to start a new entry after reaching the limit, a warning is shown
|
# - when hard_limit is > 1 and the user is trying to start a new entry after reaching the limit, a warning is shown
|
||||||
# # and the user has to stop an active entry first
|
# and the user has to stop an active entry first
|
||||||
# active_entries:
|
# active_entries:
|
||||||
# soft_limit: 1
|
# soft_limit: 1
|
||||||
# hard_limit: 3
|
# hard_limit: 3
|
||||||
#
|
|
||||||
# # Rules that define timesheet validation and behaviour
|
# Rules that define timesheet validation and behaviour
|
||||||
# rules:
|
# allow_future_times: whether records in the future can be created
|
||||||
# # whether records in the future can be created
|
# rules:
|
||||||
# allow_future_times: true
|
# allow_future_times: true
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,12 +118,18 @@ services:
|
|||||||
# TIMESHEET RECORD CALCULATOR
|
# TIMESHEET RECORD CALCULATOR
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|
||||||
App\Timesheet\Calculator\DurationCalculator:
|
App\Timesheet\RoundingService:
|
||||||
arguments: ['%kimai.timesheet.rounding%']
|
arguments:
|
||||||
|
$roundingModes: !tagged timesheet.rounding_mode
|
||||||
|
$rules: '%kimai.timesheet.rounding%'
|
||||||
|
|
||||||
App\Timesheet\Calculator\RateCalculator:
|
App\Timesheet\Calculator\RateCalculator:
|
||||||
arguments: ['%kimai.timesheet.rates%']
|
arguments: ['%kimai.timesheet.rates%']
|
||||||
|
|
||||||
|
App\Timesheet\TrackingModeService:
|
||||||
|
arguments:
|
||||||
|
$modes: !tagged timesheet.tracking_mode
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# SECURITY & VOTER
|
# SECURITY & VOTER
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ parameters:
|
|||||||
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
||||||
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
||||||
- '#Access to an undefined property Faker\\Generator::\$bs.#'
|
- '#Access to an undefined property Faker\\Generator::\$bs.#'
|
||||||
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
|
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
|
||||||
|
- '#Method App\\Controller\\AbstractController::getUser\(\) should return App\\Entity\\User|null but returns object|null. #'
|
||||||
excludes_analyse:
|
excludes_analyse:
|
||||||
- %rootDir%/../../../src/Command/KimaiImporterCommand.php
|
- %rootDir%/../../../src/Command/KimaiImporterCommand.php
|
||||||
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||||
|
|
||||||
@@ -18,4 +19,12 @@ abstract class BaseApiController extends AbstractController
|
|||||||
{
|
{
|
||||||
public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
|
public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
|
||||||
public const DATE_FORMAT_PHP = 'Y-m-d\TH:m:s';
|
public const DATE_FORMAT_PHP = 'Y-m-d\TH:m:s';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
protected function getUser()
|
||||||
|
{
|
||||||
|
return parent::getUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Form\API\TimesheetApiEditForm;
|
|||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TagRepository;
|
use App\Repository\TagRepository;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
|
use App\Timesheet\RoundingService;
|
||||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
use App\Timesheet\TrackingModeService;
|
use App\Timesheet\TrackingModeService;
|
||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
@@ -75,6 +76,10 @@ class TimesheetController extends BaseApiController
|
|||||||
* @var EventDispatcherInterface
|
* @var EventDispatcherInterface
|
||||||
*/
|
*/
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
/**
|
||||||
|
* @var RoundingService
|
||||||
|
*/
|
||||||
|
private $roundingService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ViewHandlerInterface $viewHandler,
|
ViewHandlerInterface $viewHandler,
|
||||||
@@ -83,7 +88,8 @@ class TimesheetController extends BaseApiController
|
|||||||
TimesheetConfiguration $configuration,
|
TimesheetConfiguration $configuration,
|
||||||
TagRepository $tagRepository,
|
TagRepository $tagRepository,
|
||||||
TrackingModeService $trackingModeService,
|
TrackingModeService $trackingModeService,
|
||||||
EventDispatcherInterface $dispatcher
|
EventDispatcherInterface $dispatcher,
|
||||||
|
RoundingService $roundingService
|
||||||
) {
|
) {
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
@@ -92,6 +98,7 @@ class TimesheetController extends BaseApiController
|
|||||||
$this->tagRepository = $tagRepository;
|
$this->tagRepository = $tagRepository;
|
||||||
$this->trackingModeService = $trackingModeService;
|
$this->trackingModeService = $trackingModeService;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->roundingService = $roundingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTrackingMode(): TrackingModeInterface
|
protected function getTrackingMode(): TrackingModeInterface
|
||||||
@@ -293,12 +300,12 @@ class TimesheetController extends BaseApiController
|
|||||||
{
|
{
|
||||||
$timesheet = new Timesheet();
|
$timesheet = new Timesheet();
|
||||||
$timesheet->setUser($this->getUser());
|
$timesheet->setUser($this->getUser());
|
||||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
|
||||||
|
|
||||||
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
$mode = $this->getTrackingMode();
|
$mode = $this->getTrackingMode();
|
||||||
|
$mode->create($timesheet, $request);
|
||||||
|
|
||||||
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
@@ -608,6 +615,7 @@ class TimesheetController extends BaseApiController
|
|||||||
->setActivity($timesheet->getActivity())
|
->setActivity($timesheet->getActivity())
|
||||||
->setProject($timesheet->getProject())
|
->setProject($timesheet->getProject())
|
||||||
;
|
;
|
||||||
|
$this->roundingService->roundBegin($copyTimesheet);
|
||||||
|
|
||||||
if (null !== ($copy = $paramFetcher->get('copy'))) {
|
if (null !== ($copy = $paramFetcher->get('copy'))) {
|
||||||
if (in_array($copy, ['rates', 'all'])) {
|
if (in_array($copy, ['rates', 'all'])) {
|
||||||
|
|||||||
@@ -47,4 +47,29 @@ class TimesheetConfiguration implements SystemBundleConfiguration
|
|||||||
{
|
{
|
||||||
return (int) $this->find('active_entries.soft_limit');
|
return (int) $this->find('active_entries.soft_limit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDefaultRoundingDays(): string
|
||||||
|
{
|
||||||
|
return (string) $this->find('rounding.default.days');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultRoundingMode(): string
|
||||||
|
{
|
||||||
|
return (string) $this->find('rounding.default.mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultRoundingBegin(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('rounding.default.begin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultRoundingEnd(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('rounding.default.end');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultRoundingDuration(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('rounding.default.duration');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||||
use Symfony\Component\Translation\DataCollectorTranslator;
|
use Symfony\Component\Translation\DataCollectorTranslator;
|
||||||
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
||||||
@@ -36,6 +37,14 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
return $this->container->get('translator');
|
return $this->container->get('translator');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
protected function getUser()
|
||||||
|
{
|
||||||
|
return parent::getUser();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a "successful" flash message to the stack.
|
* Adds a "successful" flash message to the stack.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
|
|||||||
use App\Form\SystemConfigurationForm;
|
use App\Form\SystemConfigurationForm;
|
||||||
use App\Form\Type\EnhancedSelectboxType;
|
use App\Form\Type\EnhancedSelectboxType;
|
||||||
use App\Form\Type\LanguageType;
|
use App\Form\Type\LanguageType;
|
||||||
|
use App\Form\Type\RoundingModeType;
|
||||||
use App\Form\Type\SkinType;
|
use App\Form\Type\SkinType;
|
||||||
use App\Form\Type\TrackingModeType;
|
use App\Form\Type\TrackingModeType;
|
||||||
|
use App\Form\Type\WeekDaysType;
|
||||||
use App\Repository\ConfigurationRepository;
|
use App\Repository\ConfigurationRepository;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
@@ -215,6 +217,39 @@ class SystemConfigurationController extends AbstractController
|
|||||||
new GreaterThanOrEqual(['value' => 1])
|
new GreaterThanOrEqual(['value' => 1])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
(new SystemConfigurationModel())
|
||||||
|
->setSection(SystemConfigurationModel::SECTION_ROUNDING)
|
||||||
|
->setConfiguration([
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rounding.default.mode')
|
||||||
|
->setType(RoundingModeType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rounding.default.begin')
|
||||||
|
->setType(IntegerType::class)
|
||||||
|
->setTranslationDomain('system-configuration')
|
||||||
|
->setConstraints([
|
||||||
|
new GreaterThanOrEqual(['value' => 0])
|
||||||
|
]),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rounding.default.end')
|
||||||
|
->setType(IntegerType::class)
|
||||||
|
->setTranslationDomain('system-configuration')
|
||||||
|
->setConstraints([
|
||||||
|
new GreaterThanOrEqual(['value' => 0])
|
||||||
|
]),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rounding.default.duration')
|
||||||
|
->setType(IntegerType::class)
|
||||||
|
->setTranslationDomain('system-configuration')
|
||||||
|
->setConstraints([
|
||||||
|
new GreaterThanOrEqual(['value' => 0])
|
||||||
|
]),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rounding.default.days')
|
||||||
|
->setType(WeekDaysType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
|
]),
|
||||||
(new SystemConfigurationModel())
|
(new SystemConfigurationModel())
|
||||||
->setSection(SystemConfigurationModel::SECTION_FORM_CUSTOMER)
|
->setSection(SystemConfigurationModel::SECTION_FORM_CUSTOMER)
|
||||||
->setConfiguration([
|
->setConfiguration([
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
{
|
{
|
||||||
$entry = new Timesheet();
|
$entry = new Timesheet();
|
||||||
$entry->setUser($this->getUser());
|
$entry->setUser($this->getUser());
|
||||||
$entry->setBegin($this->dateTime->createDateTime());
|
|
||||||
|
|
||||||
if ($request->query->get('project')) {
|
if ($request->query->get('project')) {
|
||||||
$project = $projectRepository->find($request->query->get('project'));
|
$project = $projectRepository->find($request->query->get('project'));
|
||||||
|
|||||||
@@ -36,10 +36,15 @@ class AppExtension extends Extension
|
|||||||
if (isset($config['timesheet']['duration_only'])) {
|
if (isset($config['timesheet']['duration_only'])) {
|
||||||
@trigger_error('Configuration "kimai.timesheet.duration_only" is deprecated, please remove it', E_USER_DEPRECATED);
|
@trigger_error('Configuration "kimai.timesheet.duration_only" is deprecated, please remove it', E_USER_DEPRECATED);
|
||||||
if (true === $config['timesheet']['duration_only'] && 'duration_only' !== $config['timesheet']['mode']) {
|
if (true === $config['timesheet']['duration_only'] && 'duration_only' !== $config['timesheet']['mode']) {
|
||||||
trigger_error('Found ambiguous configuration. Please remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
trigger_error('Found ambiguous configuration: remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we use a comma sepearated string internally, to be able to use it in combination with the database configuration system
|
||||||
|
foreach ($config['timesheet']['rounding'] as $name => $settings) {
|
||||||
|
$config['timesheet']['rounding'][$name]['days'] = implode(',', $settings['days']);
|
||||||
|
}
|
||||||
|
|
||||||
// safe alternatives to %kernel.project_dir%
|
// safe alternatives to %kernel.project_dir%
|
||||||
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
||||||
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
||||||
|
|||||||
@@ -98,17 +98,15 @@ class Configuration implements ConfigurationInterface
|
|||||||
->arrayPrototype()
|
->arrayPrototype()
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('days')
|
->arrayNode('days')
|
||||||
->requiresAtLeastOneElement()
|
|
||||||
->useAttributeAsKey('key')
|
->useAttributeAsKey('key')
|
||||||
->isRequired()
|
|
||||||
->scalarPrototype()->end()
|
->scalarPrototype()->end()
|
||||||
->defaultValue([])
|
->defaultValue(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'])
|
||||||
->end()
|
->end()
|
||||||
->integerNode('begin')
|
->integerNode('begin')
|
||||||
->defaultValue(0)
|
->defaultValue(1)
|
||||||
->end()
|
->end()
|
||||||
->integerNode('end')
|
->integerNode('end')
|
||||||
->defaultValue(0)
|
->defaultValue(1)
|
||||||
->end()
|
->end()
|
||||||
->integerNode('duration')
|
->integerNode('duration')
|
||||||
->defaultValue(0)
|
->defaultValue(0)
|
||||||
@@ -131,7 +129,15 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->defaultValue([])
|
->defaultValue([
|
||||||
|
'default' => [
|
||||||
|
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default'
|
||||||
|
]
|
||||||
|
])
|
||||||
->end()
|
->end()
|
||||||
->arrayNode('rates')
|
->arrayNode('rates')
|
||||||
->requiresAtLeastOneElement()
|
->requiresAtLeastOneElement()
|
||||||
|
|||||||
@@ -26,18 +26,10 @@ class TimesheetSubscriber implements EventSubscriber
|
|||||||
protected $calculator;
|
protected $calculator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable $calculators
|
* @param CalculatorInterface[] $calculators
|
||||||
*/
|
*/
|
||||||
public function __construct(iterable $calculators)
|
public function __construct(iterable $calculators)
|
||||||
{
|
{
|
||||||
foreach ($calculators as $calculator) {
|
|
||||||
if (!($calculator instanceof CalculatorInterface)) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'Invalid TimesheetCalculator implementation given. Expected CalculatorInterface but received ' .
|
|
||||||
get_class($calculator)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->calculator = $calculators;
|
$this->calculator = $calculators;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Form\Model;
|
|||||||
|
|
||||||
class SystemConfiguration
|
class SystemConfiguration
|
||||||
{
|
{
|
||||||
|
public const SECTION_ROUNDING = 'rounding';
|
||||||
public const SECTION_TIMESHEET = 'timesheet';
|
public const SECTION_TIMESHEET = 'timesheet';
|
||||||
public const SECTION_FORM_CUSTOMER = 'form_customer';
|
public const SECTION_FORM_CUSTOMER = 'form_customer';
|
||||||
public const SECTION_FORM_USER = 'form_user';
|
public const SECTION_FORM_USER = 'form_user';
|
||||||
|
|||||||
56
src/Form/Type/RoundingModeType.php
Normal file
56
src/Form/Type/RoundingModeType.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use App\Timesheet\RoundingService;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to select the timesheet mode.
|
||||||
|
*/
|
||||||
|
class RoundingModeType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RoundingService
|
||||||
|
*/
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
public function __construct(RoundingService $service)
|
||||||
|
{
|
||||||
|
$this->service = $service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$choices = [];
|
||||||
|
|
||||||
|
foreach ($this->service->getRoundingModes() as $mode) {
|
||||||
|
$id = $mode->getId();
|
||||||
|
$choices[ucfirst($id)] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'choices' => $choices,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return ChoiceType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Form/Type/WeekDaysType.php
Normal file
63
src/Form/Type/WeekDaysType.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\CallbackTransformer;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to select weekdays.
|
||||||
|
*/
|
||||||
|
class WeekDaysType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->addModelTransformer(new CallbackTransformer(
|
||||||
|
function ($weekdays) {
|
||||||
|
return explode(',', $weekdays);
|
||||||
|
},
|
||||||
|
function ($weekdays) {
|
||||||
|
return implode(',', $weekdays);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$choices = [
|
||||||
|
'Monday' => 'monday',
|
||||||
|
'Tuesday' => 'tuesday',
|
||||||
|
'Wednesday' => 'wednesday',
|
||||||
|
'Thursday' => 'thursday',
|
||||||
|
'Friday' => 'friday',
|
||||||
|
'Saturday' => 'saturday',
|
||||||
|
'Sunday' => 'sunday'
|
||||||
|
];
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'multiple' => true,
|
||||||
|
'choices' => $choices,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return ChoiceType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
|||||||
use App\Ldap\FormLoginLdapFactory;
|
use App\Ldap\FormLoginLdapFactory;
|
||||||
use App\Plugin\PluginInterface;
|
use App\Plugin\PluginInterface;
|
||||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||||
|
use App\Timesheet\Rounding\RoundingInterface;
|
||||||
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
use App\Widget\WidgetInterface;
|
use App\Widget\WidgetInterface;
|
||||||
use App\Widget\WidgetRendererInterface;
|
use App\Widget\WidgetRendererInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
@@ -52,6 +54,8 @@ class Kernel extends BaseKernel
|
|||||||
public const TAG_INVOICE_REPOSITORY = 'invoice.repository';
|
public const TAG_INVOICE_REPOSITORY = 'invoice.repository';
|
||||||
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
|
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
|
||||||
public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter';
|
public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter';
|
||||||
|
public const TAG_TIMESHEET_TRACKING_MODE = 'timesheet.tracking_mode';
|
||||||
|
public const TAG_TIMESHEET_ROUNDING_MODE = 'timesheet.rounding_mode';
|
||||||
|
|
||||||
public function getCacheDir()
|
public function getCacheDir()
|
||||||
{
|
{
|
||||||
@@ -75,6 +79,8 @@ class Kernel extends BaseKernel
|
|||||||
$container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER);
|
$container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER);
|
||||||
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);
|
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);
|
||||||
$container->registerForAutoconfiguration(TimesheetExportInterface::class)->addTag(self::TAG_TIMESHEET_EXPORTER);
|
$container->registerForAutoconfiguration(TimesheetExportInterface::class)->addTag(self::TAG_TIMESHEET_EXPORTER);
|
||||||
|
$container->registerForAutoconfiguration(TrackingModeInterface::class)->addTag(self::TAG_TIMESHEET_TRACKING_MODE);
|
||||||
|
$container->registerForAutoconfiguration(RoundingInterface::class)->addTag(self::TAG_TIMESHEET_ROUNDING_MODE);
|
||||||
|
|
||||||
/** @var SecurityExtension $extension */
|
/** @var SecurityExtension $extension */
|
||||||
$extension = $container->getExtension('security');
|
$extension = $container->getExtension('security');
|
||||||
|
|||||||
@@ -11,26 +11,19 @@ namespace App\Timesheet\Calculator;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Timesheet\CalculatorInterface;
|
use App\Timesheet\CalculatorInterface;
|
||||||
use App\Timesheet\Rounding\RoundingInterface;
|
use App\Timesheet\RoundingService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation to calculate the durations for a timesheet record.
|
* Implementation to calculate the durations for a timesheet record.
|
||||||
*
|
|
||||||
* This calculator takes the configuration %kimai.timesheet.rounding% as argument,
|
|
||||||
* so its rounding behaviour can be customized.
|
|
||||||
*/
|
*/
|
||||||
class DurationCalculator implements CalculatorInterface
|
final class DurationCalculator implements CalculatorInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var RoundingService
|
||||||
*/
|
*/
|
||||||
protected $roundings;
|
private $roundings;
|
||||||
|
|
||||||
/**
|
public function __construct(RoundingService $roundings)
|
||||||
* DurationCalculator constructor.
|
|
||||||
* @param array $roundings
|
|
||||||
*/
|
|
||||||
public function __construct(array $roundings)
|
|
||||||
{
|
{
|
||||||
$this->roundings = $roundings;
|
$this->roundings = $roundings;
|
||||||
}
|
}
|
||||||
@@ -44,37 +37,9 @@ class DurationCalculator implements CalculatorInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->applyDuration($record);
|
|
||||||
$this->applyRoundings($record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Timesheet $record
|
|
||||||
*/
|
|
||||||
protected function applyDuration(Timesheet $record)
|
|
||||||
{
|
|
||||||
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
||||||
$record->setDuration($duration);
|
$record->setDuration($duration);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->roundings->applyRoundings($record);
|
||||||
* @param Timesheet $record
|
|
||||||
*/
|
|
||||||
protected function applyRoundings(Timesheet $record)
|
|
||||||
{
|
|
||||||
foreach ($this->roundings as $rounding) {
|
|
||||||
$weekday = $record->getEnd()->format('l');
|
|
||||||
$days = array_map('strtolower', $rounding['days']);
|
|
||||||
|
|
||||||
if (in_array(strtolower($weekday), $days)) {
|
|
||||||
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($rounding['mode']) . 'Rounding';
|
|
||||||
/* @var $rounder RoundingInterface */
|
|
||||||
$rounder = new $class();
|
|
||||||
$rounder->roundBegin($record, $rounding['begin']);
|
|
||||||
$rounder->roundEnd($record, $rounding['end']);
|
|
||||||
$this->applyDuration($record);
|
|
||||||
$rounder->roundDuration($record, $rounding['duration']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ namespace App\Timesheet\Rounding;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
class CeilRounding implements RoundingInterface
|
final class CeilRounding implements RoundingInterface
|
||||||
{
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'ceil';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet $record
|
* @param Timesheet $record
|
||||||
* @param int $minutes
|
* @param int $minutes
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ namespace App\Timesheet\Rounding;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
class ClosestRounding implements RoundingInterface
|
final class ClosestRounding implements RoundingInterface
|
||||||
{
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'closest';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet $record
|
* @param Timesheet $record
|
||||||
* @param int $minutes
|
* @param int $minutes
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ namespace App\Timesheet\Rounding;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
class DefaultRounding implements RoundingInterface
|
final class DefaultRounding implements RoundingInterface
|
||||||
{
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet $record
|
* @param Timesheet $record
|
||||||
* @param int $minutes
|
* @param int $minutes
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ namespace App\Timesheet\Rounding;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
class FloorRounding implements RoundingInterface
|
final class FloorRounding implements RoundingInterface
|
||||||
{
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'floor';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet $record
|
* @param Timesheet $record
|
||||||
* @param int $minutes
|
* @param int $minutes
|
||||||
|
|||||||
@@ -33,4 +33,9 @@ interface RoundingInterface
|
|||||||
* @param int $minutes
|
* @param int $minutes
|
||||||
*/
|
*/
|
||||||
public function roundDuration(Timesheet $record, $minutes);
|
public function roundDuration(Timesheet $record, $minutes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string;
|
||||||
}
|
}
|
||||||
|
|||||||
147
src/Timesheet/RoundingService.php
Normal file
147
src/Timesheet/RoundingService.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?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\Timesheet;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Timesheet\Rounding\RoundingInterface;
|
||||||
|
|
||||||
|
final class RoundingService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $rules;
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $rulesCache;
|
||||||
|
/**
|
||||||
|
* @var TimesheetConfiguration
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
|
/**
|
||||||
|
* @var RoundingInterface[]
|
||||||
|
*/
|
||||||
|
private $roundingModes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TimesheetConfiguration $configuration
|
||||||
|
* @param RoundingInterface[] $roundingModes
|
||||||
|
* @param array $rules
|
||||||
|
*/
|
||||||
|
public function __construct(TimesheetConfiguration $configuration, iterable $roundingModes, array $rules)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
$this->roundingModes = $roundingModes;
|
||||||
|
$this->rules = $rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRoundingRules(): array
|
||||||
|
{
|
||||||
|
if (empty($this->rulesCache)) {
|
||||||
|
$this->rulesCache = $this->rules;
|
||||||
|
if (empty($this->rulesCache) || array_key_exists('default', $this->rulesCache)) {
|
||||||
|
$this->rulesCache['default']['days'] = $this->configuration->getDefaultRoundingDays();
|
||||||
|
$this->rulesCache['default']['begin'] = $this->configuration->getDefaultRoundingBegin();
|
||||||
|
$this->rulesCache['default']['end'] = $this->configuration->getDefaultRoundingEnd();
|
||||||
|
$this->rulesCache['default']['duration'] = $this->configuration->getDefaultRoundingDuration();
|
||||||
|
$this->rulesCache['default']['mode'] = $this->configuration->getDefaultRoundingMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// see AppExtension, conversion from string to array due to system configuration ont allowing to store arrays
|
||||||
|
foreach ($this->rulesCache as $key => $settings) {
|
||||||
|
$days = explode(',', $settings['days']);
|
||||||
|
$days = array_map('trim', $days);
|
||||||
|
$days = array_map('strtolower', $days);
|
||||||
|
$this->rulesCache[$key]['days'] = $days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->rulesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function roundBegin(Timesheet $record): void
|
||||||
|
{
|
||||||
|
foreach ($this->getRoundingRules() as $rounding) {
|
||||||
|
$weekday = $record->getBegin()->format('l');
|
||||||
|
|
||||||
|
if (in_array(strtolower($weekday), $rounding['days'])) {
|
||||||
|
$rounder = $this->getRoundingMode($rounding['mode']);
|
||||||
|
$rounder->roundBegin($record, $rounding['begin']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function roundEnd(Timesheet $record): void
|
||||||
|
{
|
||||||
|
foreach ($this->getRoundingRules() as $rounding) {
|
||||||
|
$weekday = $record->getEnd()->format('l');
|
||||||
|
|
||||||
|
if (in_array(strtolower($weekday), $rounding['days'])) {
|
||||||
|
$rounder = $this->getRoundingMode($rounding['mode']);
|
||||||
|
$rounder->roundEnd($record, $rounding['end']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function roundDuration(Timesheet $record): void
|
||||||
|
{
|
||||||
|
foreach ($this->getRoundingRules() as $rounding) {
|
||||||
|
$weekday = $record->getEnd()->format('l');
|
||||||
|
|
||||||
|
if (in_array(strtolower($weekday), $rounding['days'])) {
|
||||||
|
$rounder = $this->getRoundingMode($rounding['mode']);
|
||||||
|
$rounder->roundDuration($record, $rounding['duration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyRoundings(Timesheet $record): void
|
||||||
|
{
|
||||||
|
if (null === $record->getEnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->getRoundingRules() as $rounding) {
|
||||||
|
$weekday = $record->getEnd()->format('l');
|
||||||
|
|
||||||
|
if (in_array(strtolower($weekday), $rounding['days'])) {
|
||||||
|
$rounder = $this->getRoundingMode($rounding['mode']);
|
||||||
|
$rounder->roundBegin($record, $rounding['begin']);
|
||||||
|
$rounder->roundEnd($record, $rounding['end']);
|
||||||
|
|
||||||
|
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
||||||
|
$record->setDuration($duration);
|
||||||
|
|
||||||
|
$rounder->roundDuration($record, $rounding['duration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return RoundingInterface[]
|
||||||
|
*/
|
||||||
|
public function getRoundingModes(): iterable
|
||||||
|
{
|
||||||
|
return $this->roundingModes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoundingMode(string $id): RoundingInterface
|
||||||
|
{
|
||||||
|
foreach ($this->roundingModes as $mode) {
|
||||||
|
if ($mode->getId() === $id) {
|
||||||
|
return $mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException('Unknown rounding mode: ' . $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,25 @@
|
|||||||
|
|
||||||
namespace App\Timesheet\TrackingMode;
|
namespace App\Timesheet\TrackingMode;
|
||||||
|
|
||||||
class DefaultMode extends AbstractTrackingMode
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Timesheet\RoundingService;
|
||||||
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
final class DefaultMode extends AbstractTrackingMode
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var RoundingService
|
||||||
|
*/
|
||||||
|
private $rounding;
|
||||||
|
|
||||||
|
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, RoundingService $rounding)
|
||||||
|
{
|
||||||
|
parent::__construct($dateTime, $configuration);
|
||||||
|
$this->rounding = $rounding;
|
||||||
|
}
|
||||||
|
|
||||||
public function canEditBegin(): bool
|
public function canEditBegin(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -40,4 +57,23 @@ class DefaultMode extends AbstractTrackingMode
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function create(Timesheet $timesheet, Request $request): void
|
||||||
|
{
|
||||||
|
parent::create($timesheet, $request);
|
||||||
|
|
||||||
|
if (null === $timesheet->getBegin()) {
|
||||||
|
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rounding->roundBegin($timesheet);
|
||||||
|
|
||||||
|
if (null !== $timesheet->getEnd()) {
|
||||||
|
$this->rounding->roundEnd($timesheet);
|
||||||
|
|
||||||
|
if (null !== $timesheet->getDuration()) {
|
||||||
|
$this->rounding->roundDuration($timesheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ use App\Entity\Timesheet;
|
|||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class DurationFixedBeginMode implements TrackingModeInterface
|
final class DurationFixedBeginMode implements TrackingModeInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
*/
|
*/
|
||||||
protected $dateTime;
|
private $dateTime;
|
||||||
/**
|
/**
|
||||||
* @var TimesheetConfiguration
|
* @var TimesheetConfiguration
|
||||||
*/
|
*/
|
||||||
protected $configuration;
|
private $configuration;
|
||||||
|
|
||||||
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace App\Timesheet\TrackingMode;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class DurationOnlyMode extends AbstractTrackingMode
|
final class DurationOnlyMode extends AbstractTrackingMode
|
||||||
{
|
{
|
||||||
public function canEditBegin(): bool
|
public function canEditBegin(): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,10 +10,21 @@
|
|||||||
namespace App\Timesheet\TrackingMode;
|
namespace App\Timesheet\TrackingMode;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class PunchInOutMode implements TrackingModeInterface
|
final class PunchInOutMode implements TrackingModeInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var UserDateTimeFactory
|
||||||
|
*/
|
||||||
|
private $dateTime;
|
||||||
|
|
||||||
|
public function __construct(UserDateTimeFactory $dateTime)
|
||||||
|
{
|
||||||
|
$this->dateTime = $dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
public function canEditBegin(): bool
|
public function canEditBegin(): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -36,6 +47,9 @@ class PunchInOutMode implements TrackingModeInterface
|
|||||||
|
|
||||||
public function create(Timesheet $timesheet, Request $request): void
|
public function create(Timesheet $timesheet, Request $request): void
|
||||||
{
|
{
|
||||||
|
if (null === $timesheet->getBegin()) {
|
||||||
|
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): string
|
public function getId(): string
|
||||||
|
|||||||
@@ -10,28 +10,28 @@
|
|||||||
namespace App\Timesheet;
|
namespace App\Timesheet;
|
||||||
|
|
||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Timesheet\TrackingMode\DefaultMode;
|
|
||||||
use App\Timesheet\TrackingMode\DurationFixedBeginMode;
|
|
||||||
use App\Timesheet\TrackingMode\DurationOnlyMode;
|
|
||||||
use App\Timesheet\TrackingMode\PunchInOutMode;
|
|
||||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||||
|
|
||||||
class TrackingModeService
|
final class TrackingModeService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var TrackingModeInterface[]
|
||||||
*/
|
*/
|
||||||
protected $dateTime;
|
private $modes = [];
|
||||||
/**
|
/**
|
||||||
* @var TimesheetConfiguration
|
* @var TimesheetConfiguration
|
||||||
*/
|
*/
|
||||||
protected $configuration;
|
private $configuration;
|
||||||
|
|
||||||
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
/**
|
||||||
|
* @param TimesheetConfiguration $configuration
|
||||||
|
* @param TrackingModeInterface[] $modes
|
||||||
|
*/
|
||||||
|
public function __construct(TimesheetConfiguration $configuration, iterable $modes)
|
||||||
{
|
{
|
||||||
$this->dateTime = $dateTime;
|
|
||||||
$this->configuration = $configuration;
|
$this->configuration = $configuration;
|
||||||
|
$this->modes = $modes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,12 +39,7 @@ class TrackingModeService
|
|||||||
*/
|
*/
|
||||||
public function getModes(): iterable
|
public function getModes(): iterable
|
||||||
{
|
{
|
||||||
return [
|
return $this->modes;
|
||||||
new DefaultMode($this->dateTime, $this->configuration),
|
|
||||||
new PunchInOutMode(),
|
|
||||||
new DurationOnlyMode($this->dateTime, $this->configuration),
|
|
||||||
new DurationFixedBeginMode($this->dateTime, $this->configuration),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getActiveMode(): TrackingModeInterface
|
public function getActiveMode(): TrackingModeInterface
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['form[name=system_configuration_form_timesheet]', $this->createUrl('/admin/system-config/update/timesheet')],
|
['form[name=system_configuration_form_timesheet]', $this->createUrl('/admin/system-config/update/timesheet')],
|
||||||
|
['form[name=system_configuration_form_rounding]', $this->createUrl('/admin/system-config/update/rounding')],
|
||||||
['form[name=system_configuration_form_form_customer]', $this->createUrl('/admin/system-config/update/form_customer')],
|
['form[name=system_configuration_form_form_customer]', $this->createUrl('/admin/system-config/update/form_customer')],
|
||||||
['form[name=system_configuration_form_form_user]', $this->createUrl('/admin/system-config/update/form_user')],
|
['form[name=system_configuration_form_form_user]', $this->createUrl('/admin/system-config/update/form_user')],
|
||||||
['form[name=system_configuration_form_theme]', $this->createUrl('/admin/system-config/update/theme')],
|
['form[name=system_configuration_form_theme]', $this->createUrl('/admin/system-config/update/theme')],
|
||||||
|
|||||||
@@ -129,7 +129,15 @@ class AppExtensionTest extends TestCase
|
|||||||
'kimai.timesheet' => [
|
'kimai.timesheet' => [
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
'markdown_content' => false,
|
'markdown_content' => false,
|
||||||
'rounding' => [],
|
'rounding' => [
|
||||||
|
'default' => [
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday'
|
||||||
|
]
|
||||||
|
],
|
||||||
'rates' => [],
|
'rates' => [],
|
||||||
'active_entries' => [
|
'active_entries' => [
|
||||||
'soft_limit' => 1,
|
'soft_limit' => 1,
|
||||||
@@ -141,7 +149,15 @@ class AppExtensionTest extends TestCase
|
|||||||
'default_begin' => 'now',
|
'default_begin' => 'now',
|
||||||
],
|
],
|
||||||
'kimai.timesheet.rates' => [],
|
'kimai.timesheet.rates' => [],
|
||||||
'kimai.timesheet.rounding' => [],
|
'kimai.timesheet.rounding' => [
|
||||||
|
'default' => [
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday'
|
||||||
|
]
|
||||||
|
],
|
||||||
'kimai.ldap' => [
|
'kimai.ldap' => [
|
||||||
'user' => [
|
'user' => [
|
||||||
'baseDn' => null,
|
'baseDn' => null,
|
||||||
@@ -240,7 +256,7 @@ class AppExtensionTest extends TestCase
|
|||||||
public function testDurationOnlyDeprecationIsTriggered()
|
public function testDurationOnlyDeprecationIsTriggered()
|
||||||
{
|
{
|
||||||
$this->expectException(Notice::class);
|
$this->expectException(Notice::class);
|
||||||
$this->expectExceptionMessage('Found ambiguous configuration. Please remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
$this->expectExceptionMessage('Found ambiguous configuration: remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
|
||||||
|
|
||||||
$minConfig = $this->getMinConfig();
|
$minConfig = $this->getMinConfig();
|
||||||
$minConfig['kimai']['timesheet']['duration_only'] = true;
|
$minConfig['kimai']['timesheet']['duration_only'] = true;
|
||||||
|
|||||||
@@ -220,7 +220,15 @@ class ConfigurationTest extends TestCase
|
|||||||
'default_begin' => 'now',
|
'default_begin' => 'now',
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
'markdown_content' => false,
|
'markdown_content' => false,
|
||||||
'rounding' => [],
|
'rounding' => [
|
||||||
|
'default' => [
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||||
|
]
|
||||||
|
],
|
||||||
'rates' => [],
|
'rates' => [],
|
||||||
'active_entries' => [
|
'active_entries' => [
|
||||||
'soft_limit' => 1,
|
'soft_limit' => 1,
|
||||||
|
|||||||
@@ -24,12 +24,4 @@ class TimesheetSubscriberTest extends TestCase
|
|||||||
$events = $sut->getSubscribedEvents();
|
$events = $sut->getSubscribedEvents();
|
||||||
$this->assertTrue(in_array(Events::onFlush, $events));
|
$this->assertTrue(in_array(Events::onFlush, $events));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstructThrowsExceptionOnInvalidParam()
|
|
||||||
{
|
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
|
||||||
$this->expectExceptionMessage('Invalid TimesheetCalculator implementation given. Expected CalculatorInterface but received stdClass');
|
|
||||||
|
|
||||||
new TimesheetSubscriber([new \stdClass()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
51
tests/Mocks/RoundingServiceFactory.php
Normal file
51
tests/Mocks/RoundingServiceFactory.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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\Mocks;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Tests\Configuration\TestConfigLoader;
|
||||||
|
use App\Timesheet\Rounding\CeilRounding;
|
||||||
|
use App\Timesheet\Rounding\ClosestRounding;
|
||||||
|
use App\Timesheet\Rounding\DefaultRounding;
|
||||||
|
use App\Timesheet\Rounding\FloorRounding;
|
||||||
|
use App\Timesheet\RoundingService;
|
||||||
|
|
||||||
|
class RoundingServiceFactory extends AbstractMockFactory
|
||||||
|
{
|
||||||
|
public function create(?array $rules = null): RoundingService
|
||||||
|
{
|
||||||
|
$loader = new TestConfigLoader([]);
|
||||||
|
|
||||||
|
if (null === $rules) {
|
||||||
|
$rules = [
|
||||||
|
'default' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration = new TimesheetConfiguration($loader, [
|
||||||
|
'rounding' => $rules
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modes = [
|
||||||
|
new CeilRounding(),
|
||||||
|
new ClosestRounding(),
|
||||||
|
new DefaultRounding(),
|
||||||
|
new FloorRounding(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new RoundingService($configuration, $modes, $rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
tests/Mocks/TrackingModeServiceFactory.php
Normal file
45
tests/Mocks/TrackingModeServiceFactory.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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\Mocks;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Tests\Configuration\TestConfigLoader;
|
||||||
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
|
use App\Timesheet\TrackingMode\DefaultMode;
|
||||||
|
use App\Timesheet\TrackingMode\DurationFixedBeginMode;
|
||||||
|
use App\Timesheet\TrackingMode\DurationOnlyMode;
|
||||||
|
use App\Timesheet\TrackingMode\PunchInOutMode;
|
||||||
|
use App\Timesheet\TrackingModeService;
|
||||||
|
|
||||||
|
class TrackingModeServiceFactory extends AbstractMockFactory
|
||||||
|
{
|
||||||
|
public function create(?string $mode = null, ?array $modes = null): TrackingModeService
|
||||||
|
{
|
||||||
|
if (null === $mode) {
|
||||||
|
$mode = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateTime = (new UserDateTimeFactoryFactory($this->getTestCase()))->create();
|
||||||
|
$loader = new TestConfigLoader([]);
|
||||||
|
|
||||||
|
$configuration = new TimesheetConfiguration($loader, ['mode' => $mode]);
|
||||||
|
|
||||||
|
if (null === $modes) {
|
||||||
|
$modes = [
|
||||||
|
new DefaultMode($dateTime, $configuration, (new RoundingServiceFactory($this->getTestCase()))->create()),
|
||||||
|
new PunchInOutMode($dateTime),
|
||||||
|
new DurationOnlyMode($dateTime, $configuration),
|
||||||
|
new DurationFixedBeginMode($dateTime, $configuration),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TrackingModeService($configuration, $modes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,13 @@
|
|||||||
namespace App\Tests\Timesheet\Calculator;
|
namespace App\Tests\Timesheet\Calculator;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Tests\Mocks\RoundingServiceFactory;
|
||||||
use App\Timesheet\Calculator\DurationCalculator;
|
use App\Timesheet\Calculator\DurationCalculator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Timesheet\Calculator\DurationCalculator
|
* @covers \App\Timesheet\Calculator\DurationCalculator
|
||||||
|
* @covers \App\Timesheet\RoundingService
|
||||||
*/
|
*/
|
||||||
class DurationCalculatorTest extends TestCase
|
class DurationCalculatorTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -24,7 +26,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
$record->setBegin(new \DateTime());
|
$record->setBegin(new \DateTime());
|
||||||
$this->assertEquals(0, $record->getDuration());
|
$this->assertEquals(0, $record->getDuration());
|
||||||
|
|
||||||
$sut = new DurationCalculator([]);
|
$sut = new DurationCalculator((new RoundingServiceFactory($this))->create());
|
||||||
$sut->calculate($record);
|
$sut->calculate($record);
|
||||||
$this->assertEquals(0, $record->getDuration());
|
$this->assertEquals(0, $record->getDuration());
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
$record->setEnd($end);
|
$record->setEnd($end);
|
||||||
$this->assertEquals(0, $record->getDuration());
|
$this->assertEquals(0, $record->getDuration());
|
||||||
|
|
||||||
$sut = new DurationCalculator($rules);
|
$sut = new DurationCalculator((new RoundingServiceFactory($this))->create($rules));
|
||||||
$sut->calculate($record);
|
$sut->calculate($record);
|
||||||
$this->assertEquals($expectedDuration, $record->getDuration());
|
$this->assertEquals($expectedDuration, $record->getDuration());
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[],
|
null,
|
||||||
$start,
|
$start,
|
||||||
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
|
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
|
||||||
1837
|
1837
|
||||||
@@ -60,7 +62,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 15,
|
'begin' => 15,
|
||||||
'end' => 15,
|
'end' => 15,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
@@ -74,7 +76,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
@@ -88,7 +90,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 1,
|
'begin' => 1,
|
||||||
'end' => 1,
|
'end' => 1,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
@@ -102,7 +104,7 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 30,
|
'duration' => 30,
|
||||||
@@ -116,14 +118,14 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 15,
|
'begin' => 15,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 1,
|
'end' => 1,
|
||||||
'duration' => 30,
|
'duration' => 30,
|
||||||
@@ -137,14 +139,14 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 15,
|
'begin' => 15,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 30,
|
'duration' => 30,
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 1,
|
'end' => 1,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
@@ -158,14 +160,14 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 1,
|
'duration' => 1,
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 1,
|
'duration' => 1,
|
||||||
@@ -179,14 +181,14 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 1,
|
'begin' => 1,
|
||||||
'end' => 1,
|
'end' => 1,
|
||||||
'duration' => 1,
|
'duration' => 1,
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
'begin' => 1,
|
'begin' => 1,
|
||||||
'end' => 1,
|
'end' => 1,
|
||||||
'duration' => 1,
|
'duration' => 1,
|
||||||
@@ -200,14 +202,14 @@ class DurationCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
'default' => [
|
'default' => [
|
||||||
'days' => [$day],
|
'days' => $day,
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
'mode' => 'default',
|
'mode' => 'default',
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
'begin' => 0,
|
'begin' => 0,
|
||||||
'end' => 0,
|
'end' => 0,
|
||||||
'duration' => 0,
|
'duration' => 0,
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
'days' => [$day],
|
'days' => [$day],
|
||||||
'factor' => 2.0
|
'factor' => 2.0
|
||||||
],
|
],
|
||||||
'weekdays' => [
|
'foo' => [
|
||||||
'days' => ['MonDay', 'tUEsdAy', 'WEdnesday', 'THursday', 'friDay', 'SATURday', 'sunDAY'],
|
'days' => ['MonDay', 'tUEsdAy', 'WEdnesday', 'THursday', 'friDay', 'SATURday', 'sunDAY'],
|
||||||
'factor' => 1.5
|
'factor' => 1.5
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class CeilRoundingTest extends TestCase
|
|||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|
||||||
$sut = new CeilRounding();
|
$sut = new CeilRounding();
|
||||||
|
self::assertEquals('ceil', $sut->getId());
|
||||||
$sut->roundBegin($record, $roundBegin);
|
$sut->roundBegin($record, $roundBegin);
|
||||||
$sut->roundEnd($record, $roundEnd);
|
$sut->roundEnd($record, $roundEnd);
|
||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class ClosestRoundingTest extends TestCase
|
|||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|
||||||
$sut = new ClosestRounding();
|
$sut = new ClosestRounding();
|
||||||
|
self::assertEquals('closest', $sut->getId());
|
||||||
$sut->roundBegin($record, $roundBegin);
|
$sut->roundBegin($record, $roundBegin);
|
||||||
$sut->roundEnd($record, $roundEnd);
|
$sut->roundEnd($record, $roundEnd);
|
||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class DefaultRoundingTest extends TestCase
|
|||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|
||||||
$sut = new DefaultRounding();
|
$sut = new DefaultRounding();
|
||||||
|
self::assertEquals('default', $sut->getId());
|
||||||
$sut->roundBegin($record, $roundBegin);
|
$sut->roundBegin($record, $roundBegin);
|
||||||
$sut->roundEnd($record, $roundEnd);
|
$sut->roundEnd($record, $roundEnd);
|
||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class FloorRoundingTest extends TestCase
|
|||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|
||||||
$sut = new FloorRounding();
|
$sut = new FloorRounding();
|
||||||
|
self::assertEquals('floor', $sut->getId());
|
||||||
$sut->roundBegin($record, $roundBegin);
|
$sut->roundBegin($record, $roundBegin);
|
||||||
$sut->roundEnd($record, $roundEnd);
|
$sut->roundEnd($record, $roundEnd);
|
||||||
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|||||||
251
tests/Timesheet/RoundingServiceTest.php
Normal file
251
tests/Timesheet/RoundingServiceTest.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<?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\Timesheet;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Tests\Mocks\RoundingServiceFactory;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Timesheet\RoundingService
|
||||||
|
*/
|
||||||
|
class RoundingServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCalculateWithEmptyEnd()
|
||||||
|
{
|
||||||
|
$record = new Timesheet();
|
||||||
|
$record->setBegin(new \DateTime());
|
||||||
|
$this->assertEquals(0, $record->getDuration());
|
||||||
|
|
||||||
|
$sut = (new RoundingServiceFactory($this))->create();
|
||||||
|
$sut->applyRoundings($record);
|
||||||
|
$this->assertEquals(0, $record->getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestData
|
||||||
|
*/
|
||||||
|
public function testCalculate($rules, $start, $end, $expectedStart, $expectedEnd, $expectedDuration)
|
||||||
|
{
|
||||||
|
$record = new Timesheet();
|
||||||
|
$record->setBegin($start);
|
||||||
|
$record->setEnd($end);
|
||||||
|
$this->assertEquals(0, $record->getDuration());
|
||||||
|
|
||||||
|
$sut = (new RoundingServiceFactory($this))->create($rules);
|
||||||
|
$sut->roundBegin($record);
|
||||||
|
$this->assertEquals($expectedStart, $record->getBegin());
|
||||||
|
$sut->roundEnd($record);
|
||||||
|
$this->assertEquals($expectedEnd, $record->getEnd());
|
||||||
|
|
||||||
|
// set the proper duration
|
||||||
|
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
|
||||||
|
|
||||||
|
$sut->roundDuration($record);
|
||||||
|
$this->assertEquals($expectedDuration, $record->getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTestData()
|
||||||
|
{
|
||||||
|
$start = new \DateTime();
|
||||||
|
$start->setTime(12, 0, 0);
|
||||||
|
$day = $start->format('l');
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
$start,
|
||||||
|
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
|
||||||
|
$start,
|
||||||
|
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
|
||||||
|
1837
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 15,
|
||||||
|
'end' => 15,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 17, 35),
|
||||||
|
(clone $start)->setTime(13, 32, 52),
|
||||||
|
(clone $start)->setTime(12, 15, 00),
|
||||||
|
(clone $start)->setTime(13, 45, 00),
|
||||||
|
5400
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 17, 35),
|
||||||
|
(clone $start)->setTime(13, 32, 52),
|
||||||
|
(clone $start)->setTime(12, 17, 35),
|
||||||
|
(clone $start)->setTime(13, 32, 52),
|
||||||
|
4517
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 17, 35),
|
||||||
|
(clone $start)->setTime(13, 32, 52),
|
||||||
|
(clone $start)->setTime(12, 17, 00),
|
||||||
|
(clone $start)->setTime(13, 33, 00),
|
||||||
|
4560
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 30,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 10, 51),
|
||||||
|
(clone $start)->setTime(14, 40, 52),
|
||||||
|
(clone $start)->setTime(12, 10, 51),
|
||||||
|
(clone $start)->setTime(14, 40, 52),
|
||||||
|
10800
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 15,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
'foo' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 30,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 27, 35), // 12:15
|
||||||
|
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 => 2:30
|
||||||
|
(clone $start)->setTime(12, 15, 00), // 12:15
|
||||||
|
(clone $start)->setTime(14, 33, 00), // 14:33 => 2:18 => 2:30
|
||||||
|
9000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 15,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 30,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
'foo' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 27, 35), // 12:15
|
||||||
|
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
|
||||||
|
(clone $start)->setTime(12, 15, 00), // 12:15
|
||||||
|
(clone $start)->setTime(14, 33, 00), // 14:33 => 2:18 (second duration will not be rounded)
|
||||||
|
9000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 1,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
'foo' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 1,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||||
|
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 1,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
'foo' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 1,
|
||||||
|
'end' => 1,
|
||||||
|
'duration' => 1,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
|
||||||
|
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'default' => [
|
||||||
|
'days' => $day,
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
'foo' => [
|
||||||
|
'days' => 'monday,tuesday,wednesday,thursday,friday,saturday,sunday',
|
||||||
|
'begin' => 0,
|
||||||
|
'end' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
'mode' => 'default',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||||
|
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
|
||||||
|
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
|
||||||
|
0
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
namespace App\Tests\Timesheet\TrackingMode;
|
namespace App\Tests\Timesheet\TrackingMode;
|
||||||
|
|
||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
use App\Tests\Configuration\TestConfigLoader;
|
use App\Tests\Configuration\TestConfigLoader;
|
||||||
|
use App\Tests\Mocks\RoundingServiceFactory;
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
use App\Timesheet\TrackingMode\DefaultMode;
|
use App\Timesheet\TrackingMode\DefaultMode;
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@ use App\Timesheet\TrackingMode\DefaultMode;
|
|||||||
*/
|
*/
|
||||||
class DefaultModeTest extends AbstractTrackingModeTest
|
class DefaultModeTest extends AbstractTrackingModeTest
|
||||||
{
|
{
|
||||||
|
protected function assertDefaultBegin(Timesheet $timesheet)
|
||||||
|
{
|
||||||
|
self::assertNotNull($timesheet->getBegin());
|
||||||
|
self::assertInstanceOf(\DateTime::class, $timesheet->getBegin());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return DefaultMode
|
* @return DefaultMode
|
||||||
*/
|
*/
|
||||||
@@ -28,7 +36,7 @@ class DefaultModeTest extends AbstractTrackingModeTest
|
|||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
||||||
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
|
$configuration = new TimesheetConfiguration($loader, ['default_begin' => '13:47']);
|
||||||
|
|
||||||
return new DefaultMode($dateTime, $configuration);
|
return new DefaultMode($dateTime, $configuration, (new RoundingServiceFactory($this))->create());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDefaultValues()
|
public function testDefaultValues()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Tests\Timesheet\TrackingMode;
|
namespace App\Tests\Timesheet\TrackingMode;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
use App\Timesheet\TrackingMode\PunchInOutMode;
|
use App\Timesheet\TrackingMode\PunchInOutMode;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -21,7 +22,8 @@ class PunchInOutModeTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function testDefaultValues()
|
public function testDefaultValues()
|
||||||
{
|
{
|
||||||
$sut = new PunchInOutMode();
|
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
||||||
|
$sut = new PunchInOutMode($dateTime);
|
||||||
|
|
||||||
self::assertFalse($sut->canEditBegin());
|
self::assertFalse($sut->canEditBegin());
|
||||||
self::assertFalse($sut->canEditEnd());
|
self::assertFalse($sut->canEditEnd());
|
||||||
@@ -33,13 +35,25 @@ class PunchInOutModeTest extends TestCase
|
|||||||
|
|
||||||
public function testCreate()
|
public function testCreate()
|
||||||
{
|
{
|
||||||
|
$startingTime = new \DateTime('22:54');
|
||||||
$timesheet = new Timesheet();
|
$timesheet = new Timesheet();
|
||||||
$timesheet->setBegin(new \DateTime('22:54'));
|
$timesheet->setBegin($startingTime);
|
||||||
$request = new Request();
|
$request = new Request();
|
||||||
$timesheetNew = clone $timesheet;
|
|
||||||
|
|
||||||
$sut = new PunchInOutMode();
|
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
||||||
|
$sut = new PunchInOutMode($dateTime);
|
||||||
$sut->create($timesheet, $request);
|
$sut->create($timesheet, $request);
|
||||||
self::assertEquals($timesheet, $timesheetNew);
|
self::assertEquals($timesheet->getBegin(), $startingTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateWithoutBegin()
|
||||||
|
{
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
||||||
|
$sut = new PunchInOutMode($dateTime);
|
||||||
|
$sut->create($timesheet, $request);
|
||||||
|
self::assertInstanceOf(\DateTime::class, $timesheet->getBegin());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,8 @@
|
|||||||
|
|
||||||
namespace App\Tests\Timesheet;
|
namespace App\Tests\Timesheet;
|
||||||
|
|
||||||
use App\Configuration\TimesheetConfiguration;
|
use App\Tests\Mocks\TrackingModeServiceFactory;
|
||||||
use App\Tests\Configuration\TestConfigLoader;
|
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
|
||||||
use App\Timesheet\TrackingMode\PunchInOutMode;
|
use App\Timesheet\TrackingMode\PunchInOutMode;
|
||||||
use App\Timesheet\TrackingModeService;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||||
|
|
||||||
@@ -24,11 +21,7 @@ class TrackingModeServiceTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function testDefaultTrackingModesAreRegistered()
|
public function testDefaultTrackingModesAreRegistered()
|
||||||
{
|
{
|
||||||
$loader = new TestConfigLoader([]);
|
$sut = (new TrackingModeServiceFactory($this))->create('punch');
|
||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
|
||||||
$configuration = new TimesheetConfiguration($loader, ['mode' => 'punch']);
|
|
||||||
|
|
||||||
$sut = new TrackingModeService($dateTime, $configuration);
|
|
||||||
|
|
||||||
$modes = $sut->getModes();
|
$modes = $sut->getModes();
|
||||||
self::assertGreaterThanOrEqual(4, $modes);
|
self::assertGreaterThanOrEqual(4, $modes);
|
||||||
@@ -46,11 +39,7 @@ class TrackingModeServiceTest extends TestCase
|
|||||||
|
|
||||||
public function testGetActiveMode()
|
public function testGetActiveMode()
|
||||||
{
|
{
|
||||||
$loader = new TestConfigLoader([]);
|
$sut = (new TrackingModeServiceFactory($this))->create('punch');
|
||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
|
||||||
$configuration = new TimesheetConfiguration($loader, ['mode' => 'punch']);
|
|
||||||
|
|
||||||
$sut = new TrackingModeService($dateTime, $configuration);
|
|
||||||
|
|
||||||
self::assertInstanceOf(PunchInOutMode::class, $sut->getActiveMode());
|
self::assertInstanceOf(PunchInOutMode::class, $sut->getActiveMode());
|
||||||
}
|
}
|
||||||
@@ -60,11 +49,7 @@ class TrackingModeServiceTest extends TestCase
|
|||||||
$this->expectException(ServiceNotFoundException::class);
|
$this->expectException(ServiceNotFoundException::class);
|
||||||
$this->expectExceptionMessage('You have requested a non-existent service "xxxxxx"');
|
$this->expectExceptionMessage('You have requested a non-existent service "xxxxxx"');
|
||||||
|
|
||||||
$loader = new TestConfigLoader([]);
|
$sut = (new TrackingModeServiceFactory($this))->create('xxxxxx');
|
||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
|
||||||
$configuration = new TimesheetConfiguration($loader, ['mode' => 'xxxxxx']);
|
|
||||||
|
|
||||||
$sut = new TrackingModeService($dateTime, $configuration);
|
|
||||||
|
|
||||||
$sut->getActiveMode();
|
$sut->getActiveMode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ use App\Entity\Activity;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
use App\Tests\Mocks\TrackingModeServiceFactory;
|
||||||
use App\Timesheet\TrackingModeService;
|
|
||||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||||
use App\Validator\Constraints\TimesheetValidator;
|
use App\Validator\Constraints\TimesheetValidator;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
@@ -39,10 +38,8 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
|||||||
'rules' => [
|
'rules' => [
|
||||||
'allow_future_times' => false,
|
'allow_future_times' => false,
|
||||||
],
|
],
|
||||||
'mode' => 'default',
|
|
||||||
]);
|
]);
|
||||||
$dateTime = (new UserDateTimeFactoryFactory($this))->create();
|
$service = (new TrackingModeServiceFactory($this))->create('default');
|
||||||
$service = new TrackingModeService($dateTime, $config);
|
|
||||||
|
|
||||||
return new TimesheetValidator($authMock, $config, $service);
|
return new TimesheetValidator($authMock, $config, $service);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,74 @@
|
|||||||
<source>label.theme.branding.title</source>
|
<source>label.theme.branding.title</source>
|
||||||
<target>Browser Titel</target>
|
<target>Browser Titel</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="rounding">
|
||||||
|
<source>rounding</source>
|
||||||
|
<target>Zeitenrundung</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.begin">
|
||||||
|
<source>label.timesheet.rounding.default.begin</source>
|
||||||
|
<target>Rundung des Startzeitpunkts in Minuten (0 = deaktiviert)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.end">
|
||||||
|
<source>label.timesheet.rounding.default.end</source>
|
||||||
|
<target>Rundung des Endzeitpunkts in Minuten (0 = deaktiviert)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.duration">
|
||||||
|
<source>label.timesheet.rounding.default.duration</source>
|
||||||
|
<target>Rundung der Dauer in Minuten (0 = deaktiviert)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.mode">
|
||||||
|
<source>label.timesheet.rounding.default.mode</source>
|
||||||
|
<target>Rundungsmodus</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.days">
|
||||||
|
<source>label.timesheet.rounding.default.days</source>
|
||||||
|
<target>Tage der Woche an denen gerundet wird</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Monday">
|
||||||
|
<source>Monday</source>
|
||||||
|
<target>Montag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Tuesday">
|
||||||
|
<source>Tuesday</source>
|
||||||
|
<target>Dienstag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Wednesday">
|
||||||
|
<source>Wednesday</source>
|
||||||
|
<target>Mittwoch</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Thursday">
|
||||||
|
<source>Thursday</source>
|
||||||
|
<target>Donnerstag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Friday">
|
||||||
|
<source>Friday</source>
|
||||||
|
<target>Freitag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Saturday">
|
||||||
|
<source>Saturday</source>
|
||||||
|
<target>Samstag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Sunday">
|
||||||
|
<source>Sunday</source>
|
||||||
|
<target>Sonntag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Ceil">
|
||||||
|
<source>Ceil</source>
|
||||||
|
<target>Ceil: Start, Ende und Dauer werden nach oben gerundet</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Closest">
|
||||||
|
<source>Closest</source>
|
||||||
|
<target>Closest: Mathematische Rundung zum nächsten Wert</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Default">
|
||||||
|
<source>Default</source>
|
||||||
|
<target>Standard: Start werden nach oben, Ende und Dauer nach oben gerundet</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Floor">
|
||||||
|
<source>Floor</source>
|
||||||
|
<target>Floor: Start, Ende und Dauer werden nach unten gerundet</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -118,6 +118,74 @@
|
|||||||
<source>label.theme.branding.title</source>
|
<source>label.theme.branding.title</source>
|
||||||
<target>Browser Title</target>
|
<target>Browser Title</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="rounding">
|
||||||
|
<source>rounding</source>
|
||||||
|
<target>Time rounding</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.begin">
|
||||||
|
<source>label.timesheet.rounding.default.begin</source>
|
||||||
|
<target>Rounding of the start time in minutes (0 = deactivated)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.end">
|
||||||
|
<source>label.timesheet.rounding.default.end</source>
|
||||||
|
<target>Rounding of the end time in minutes (0 = deactivated)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.duration">
|
||||||
|
<source>label.timesheet.rounding.default.duration</source>
|
||||||
|
<target>Rounding of the duration in minutes (0 = deactivated)</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.mode">
|
||||||
|
<source>label.timesheet.rounding.default.mode</source>
|
||||||
|
<target>Rounding mode</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="label.timesheet.rounding.default.days">
|
||||||
|
<source>label.timesheet.rounding.default.days</source>
|
||||||
|
<target>Days of the week when rounding will be applied</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Monday">
|
||||||
|
<source>Monday</source>
|
||||||
|
<target>Monday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Tuesday">
|
||||||
|
<source>Tuesday</source>
|
||||||
|
<target>Tuesday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Wednesday">
|
||||||
|
<source>Wednesday</source>
|
||||||
|
<target>Wednesday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Thursday">
|
||||||
|
<source>Thursday</source>
|
||||||
|
<target>Thursday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Friday">
|
||||||
|
<source>Friday</source>
|
||||||
|
<target>Friday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Saturday">
|
||||||
|
<source>Saturday</source>
|
||||||
|
<target>Saturday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Sunday">
|
||||||
|
<source>Sunday</source>
|
||||||
|
<target>Sunday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Ceil">
|
||||||
|
<source>Ceil</source>
|
||||||
|
<target>Ceil: begin, end and duration will be rounded up</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Closest">
|
||||||
|
<source>Closest</source>
|
||||||
|
<target>Closest: mathematical rounding to the nearest value</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Default">
|
||||||
|
<source>Default</source>
|
||||||
|
<target>Standard: begin will be rounded down, end and duration up</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Floor">
|
||||||
|
<source>Floor</source>
|
||||||
|
<target>Floor: begin, end and duration will be rounded down</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user