set rates via batch update (#1326)

This commit is contained in:
Kevin Papst
2019-12-29 16:52:43 +01:00
committed by GitHub
parent 8aa8f452d5
commit baac4eb698
12 changed files with 258 additions and 49 deletions

View File

@@ -343,6 +343,15 @@ abstract class TimesheetAbstractController extends AbstractController
$timesheet->setExported($dto->isExported());
$execute = true;
}
if (null !== $dto->getHourlyRate()) {
$timesheet->setFixedRate(null);
$timesheet->setHourlyRate($dto->getHourlyRate());
$execute = true;
} elseif (null !== $dto->getFixedRate()) {
$timesheet->setFixedRate($dto->getFixedRate());
$timesheet->setHourlyRate(null);
$execute = true;
}
}
if ($execute) {

View File

@@ -11,6 +11,8 @@ namespace App\Form\MultiUpdate;
use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType;
use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType;
use App\Form\Type\ProjectType;
use App\Form\Type\TagsInputType;
use App\Form\Type\UserType;
@@ -57,6 +59,7 @@ class TimesheetMultiUpdate extends AbstractType
$activity = null;
$project = null;
$customer = null;
$currency = null;
$customerCount = $this->customers->countCustomer(true);
if (isset($options['data'])) {
@@ -70,6 +73,10 @@ class TimesheetMultiUpdate extends AbstractType
if (null === $project && null !== $activity) {
$project = $activity->getProject();
}
if (null !== $customer) {
$currency = $customer->getCurrency();
}
}
$builder
@@ -189,6 +196,14 @@ class TimesheetMultiUpdate extends AbstractType
]);
}
$builder
->add('fixedRate', FixedRateType::class, [
'currency' => $currency,
])
->add('hourlyRate', HourlyRateType::class, [
'currency' => $currency,
]);
$builder->add('entities', HiddenType::class, [
'required' => false,
]);

View File

@@ -49,6 +49,14 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO
* @var bool|null
*/
private $exported = null;
/**
* @var float
*/
private $fixedRate;
/**
* @var float
*/
private $hourlyRate;
public function getCustomer(): ?Customer
{
@@ -136,4 +144,28 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO
return $this;
}
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
public function setFixedRate(?float $fixedRate): TimesheetMultiUpdateDTO
{
$this->fixedRate = $fixedRate;
return $this;
}
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
public function setHourlyRate(?float $hourlyRate): TimesheetMultiUpdateDTO
{
$this->hourlyRate = $hourlyRate;
return $this;
}
}

View File

@@ -18,20 +18,22 @@ use Symfony\Component\Validator\Constraint;
*/
class TimesheetMultiUpdate extends Constraint
{
public const MISSING_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d84';
public const MISSING_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d85';
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'xy5hffg-dsfef3-426a-83d7-1f2d33hs5d86';
public const DISABLED_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d87';
public const DISABLED_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d88';
public const DISABLED_CUSTOMER_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d89';
public const MISSING_ACTIVITY_ERROR = 'ts-multi-update-84';
public const MISSING_PROJECT_ERROR = 'ts-multi-update-85';
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'ts-multi-update-86';
public const DISABLED_ACTIVITY_ERROR = 'ts-multi-update-87';
public const DISABLED_PROJECT_ERROR = 'ts-multi-update-88';
public const DISABLED_CUSTOMER_ERROR = 'ts-multi-update-89';
public const HOURLY_RATE_FIXED_RATE = 'ts-multi-update-90';
protected static $errorNames = [
self::MISSING_ACTIVITY_ERROR => 'A timesheet must have an activity.',
self::MISSING_ACTIVITY_ERROR => 'You need to choose an activity, if the project should be changed.',
self::MISSING_PROJECT_ERROR => 'A timesheet must have a project.',
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch: chosen project does not match the activity project.',
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
self::HOURLY_RATE_FIXED_RATE => 'Cannot set hourly rate and fixed rate at the same time.',
];
public $message = 'This form has invalid settings.';

View File

@@ -9,43 +9,22 @@
namespace App\Validator\Constraints;
use App\Configuration\TimesheetConfiguration;
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
use App\Validator\Constraints\TimesheetMultiUpdate as TimesheetConstraint;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use App\Validator\Constraints\TimesheetMultiUpdate as TimesheetMultiUpdateConstraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TimesheetMultiUpdateValidator extends ConstraintValidator
final class TimesheetMultiUpdateValidator extends ConstraintValidator
{
/**
* @var AuthorizationCheckerInterface
*/
protected $auth;
/**
* @var TimesheetConfiguration
*/
protected $configuration;
/**
* @param AuthorizationCheckerInterface $auth
* @param TimesheetConfiguration $configuration
*/
public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration)
{
$this->auth = $auth;
$this->configuration = $configuration;
}
/**
* @param TimesheetMultiUpdateDTO|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
if (!($constraint instanceof TimesheetConstraint)) {
if (!($constraint instanceof TimesheetMultiUpdateConstraint)) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimesheetMultiUpdate');
}
@@ -54,6 +33,20 @@ class TimesheetMultiUpdateValidator extends ConstraintValidator
}
$this->validateActivityAndProject($value, $this->context);
if (null !== $value->getFixedRate() && null !== $value->getHourlyRate()) {
$this->context->buildViolation('Cannot set hourly rate and fixed rate at the same time.')
->atPath('fixedRate')
->setTranslationDomain('validators')
->setCode(TimesheetMultiUpdateConstraint::HOURLY_RATE_FIXED_RATE)
->addViolation();
$this->context->buildViolation('Cannot set hourly rate and fixed rate at the same time.')
->atPath('hourlyRate')
->setTranslationDomain('validators')
->setCode(TimesheetMultiUpdateConstraint::HOURLY_RATE_FIXED_RATE)
->addViolation();
}
}
/**
@@ -67,20 +60,24 @@ class TimesheetMultiUpdateValidator extends ConstraintValidator
// non global activity without project
if (null !== $activity && null !== $activity->getProject() && null === $project) {
$context->buildViolation('Missing project')
$context->buildViolation('Missing project.')
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
->setCode(TimesheetMultiUpdateConstraint::MISSING_PROJECT_ERROR)
->addViolation();
return;
}
// only project was chosen
if (null === $activity && null !== $project) {
$context->buildViolation('Missing activity')
->atPath('project')
$context->buildViolation('You need to choose an activity, if the project should be changed.')
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
->setCode(TimesheetMultiUpdateConstraint::MISSING_ACTIVITY_ERROR)
->addViolation();
return;
}
if (null !== $activity) {
@@ -88,15 +85,17 @@ class TimesheetMultiUpdateValidator extends ConstraintValidator
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
->setCode(TimesheetMultiUpdateConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
->addViolation();
return;
}
if (!$activity->isVisible()) {
$context->buildViolation('Cannot assign a disabled activity.')
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::DISABLED_ACTIVITY_ERROR)
->setCode(TimesheetMultiUpdateConstraint::DISABLED_ACTIVITY_ERROR)
->addViolation();
}
}
@@ -106,7 +105,7 @@ class TimesheetMultiUpdateValidator extends ConstraintValidator
$context->buildViolation('Cannot assign a disabled project.')
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::DISABLED_PROJECT_ERROR)
->setCode(TimesheetMultiUpdateConstraint::DISABLED_PROJECT_ERROR)
->addViolation();
}
@@ -114,7 +113,7 @@ class TimesheetMultiUpdateValidator extends ConstraintValidator
$context->buildViolation('Cannot assign a disabled customer.')
->atPath('customer')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::DISABLED_CUSTOMER_ERROR)
->setCode(TimesheetMultiUpdateConstraint::DISABLED_CUSTOMER_ERROR)
->addViolation();
}
}

View File

@@ -141,7 +141,7 @@ class PermissionControllerTest extends ControllerBaseTest
self::assertEquals(1, $permission->getRole()->getId());
// flush the cache to prevent wrong results
$em->clear(RolePermission::class);
$em->clear();
// update the permission
$this->request($client, '/admin/permissions/roles/1/view_user/0');

View File

@@ -124,7 +124,7 @@ class TagControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/admin/tags/'));
$client->followRedirect();
$em->clear(Tag::class);
$em->clear();
self::assertEquals(0, $em->getRepository(Tag::class)->count([]));
}
}

View File

@@ -360,7 +360,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
$client->followRedirect();
$em->clear(Timesheet::class);
$em->clear();
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
}
@@ -404,11 +404,12 @@ class TimesheetControllerTest extends ControllerBaseTest
$client->submit($form, [
'timesheet_multi_update' => [
'exported' => true,
'tags' => 'test, foo-bar'
'tags' => 'test, foo-bar',
'fixedRate' => 13,
]
]);
$em->clear(Timesheet::class);
$em->clear();
/** @var Timesheet[] $timesheets */
$timesheets = $em->getRepository(Timesheet::class)->findAll();
@@ -416,6 +417,7 @@ class TimesheetControllerTest extends ControllerBaseTest
foreach ($timesheets as $timesheet) {
self::assertCount(2, $timesheet->getTags());
self::assertTrue($timesheet->isExported());
self::assertEquals(13, $timesheet->getFixedRate());
}
}
}

View File

@@ -14,6 +14,7 @@ use App\Entity\TimesheetMeta;
use App\Entity\User;
use App\Form\Type\DateRangeType;
use App\Tests\DataFixtures\TimesheetFixtures;
use App\Timesheet\Util;
/**
* @group integration
@@ -276,7 +277,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/'));
$client->followRedirect();
$em->clear(Timesheet::class);
$em->clear();
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
}
@@ -323,11 +324,12 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
'user' => $newUser->getId(),
'exported' => true,
'replaceTags' => true,
'tags' => 'test, foo-bar, tralalala'
'tags' => 'test, foo-bar, tralalala',
'hourlyRate' => 13.78,
]
]);
$em->clear(Timesheet::class);
$em->clear();
/** @var Timesheet[] $timesheets */
$timesheets = $em->getRepository(Timesheet::class)->findAll();
@@ -336,6 +338,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
self::assertCount(3, $timesheet->getTags());
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
self::assertTrue($timesheet->isExported());
self::assertEquals(Util::calculateRate(13.78, $timesheet->getDuration()), $timesheet->getRate());
}
}
}

View File

@@ -164,7 +164,7 @@ class UserControllerTest extends ControllerBaseTest
$this->assertHasFlashDeleteSuccess($client);
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
// $em->clear(Timesheet::class);
// $em->clear();
// $timesheets = $em->getRepository(Timesheet::class)->count([]);
// $this->assertEquals(0, $timesheets);

View File

@@ -35,6 +35,8 @@ class TimesheetMultiUpdateDTOTest extends TestCase
self::assertEquals([], $sut->getTags());
self::assertNull($sut->getUser());
self::assertFalse($sut->isReplaceTags());
self::assertNull($sut->getFixedRate());
self::assertNull($sut->getHourlyRate());
}
public function testSetterAndGetter()
@@ -86,5 +88,11 @@ class TimesheetMultiUpdateDTOTest extends TestCase
$customer = (new Customer())->setName('sdfsdfsd');
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setCustomer($customer));
self::assertSame($customer, $sut->getCustomer());
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setFixedRate(12.78));
self::assertEquals(12.78, $sut->getFixedRate());
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setHourlyRate(123.45));
self::assertEquals(123.45, $sut->getHourlyRate());
}
}

View File

@@ -0,0 +1,139 @@
<?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\Validator\Constraints;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
use App\Validator\Constraints\TimesheetMultiUpdate as TimesheetMultiUpdateConstraint;
use App\Validator\Constraints\TimesheetMultiUpdateValidator;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @covers \App\Validator\Constraints\TimesheetMultiUpdateValidator
*/
class TimesheetMultiUpdateValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator($isGranted = true)
{
return new TimesheetMultiUpdateValidator();
}
public function testConstraintIsInvalid()
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate('foo', new NotBlank());
}
public function testProjectMismatch()
{
$activity = new Activity();
$project1 = new Project();
$project2 = new Project();
$activity->setProject($project1);
$timesheet = new TimesheetMultiUpdateDTO();
$timesheet
->setActivity($activity)
->setProject($project2)
;
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
$this->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
->atPath('property.path.project')
->setCode(TimesheetMultiUpdateConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
->assertRaised();
}
public function testProjectWithoutActivity()
{
$timesheet = new TimesheetMultiUpdateDTO();
$timesheet
->setProject(new Project())
;
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
$this->buildViolation('You need to choose an activity, if the project should be changed.')
->atPath('property.path.activity')
->setCode(TimesheetMultiUpdateConstraint::MISSING_ACTIVITY_ERROR)
->assertRaised();
}
public function testActivityWithoutProject()
{
$timesheet = new TimesheetMultiUpdateDTO();
$timesheet
->setActivity((new Activity())->setProject(new Project()))
;
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
$this->buildViolation('Missing project.')
->atPath('property.path.project')
->setCode(TimesheetMultiUpdateConstraint::MISSING_PROJECT_ERROR)
->assertRaised();
}
public function testHourlyRateAndFixedRateInParallelAreNotAllowed()
{
$timesheet = new TimesheetMultiUpdateDTO();
$timesheet
->setHourlyRate(10.12)
->setFixedRate(123.45)
;
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
$this->buildViolation('Cannot set hourly rate and fixed rate at the same time.')
->atPath('property.path.fixedRate')
->setCode(TimesheetMultiUpdateConstraint::HOURLY_RATE_FIXED_RATE)
->buildNextViolation('Cannot set hourly rate and fixed rate at the same time.')
->atPath('property.path.hourlyRate')
->setCode(TimesheetMultiUpdateConstraint::HOURLY_RATE_FIXED_RATE)
->assertRaised();
}
public function testDisabledValues()
{
$customer = new Customer();
$customer->setVisible(false);
$activity = new Activity();
$activity->setVisible(false);
$project = new Project();
$project->setVisible(false);
$project->setCustomer($customer);
$activity->setProject($project);
$timesheet = new TimesheetMultiUpdateDTO();
$timesheet
->setActivity($activity)
->setProject($project)
;
$this->validator->validate($timesheet, new TimesheetMultiUpdateConstraint(['message' => 'myMessage']));
$this->buildViolation('Cannot assign a disabled activity.')
->atPath('property.path.activity')
->setCode(TimesheetMultiUpdateConstraint::DISABLED_ACTIVITY_ERROR)
->buildNextViolation('Cannot assign a disabled project.')
->atPath('property.path.project')
->setCode(TimesheetMultiUpdateConstraint::DISABLED_PROJECT_ERROR)
->buildNextViolation('Cannot assign a disabled customer.')
->atPath('property.path.customer')
->setCode(TimesheetMultiUpdateConstraint::DISABLED_CUSTOMER_ERROR)
->assertRaised();
}
}