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()); $timesheet->setExported($dto->isExported());
$execute = true; $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) { if ($execute) {

View File

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

View File

@@ -49,6 +49,14 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO
* @var bool|null * @var bool|null
*/ */
private $exported = null; private $exported = null;
/**
* @var float
*/
private $fixedRate;
/**
* @var float
*/
private $hourlyRate;
public function getCustomer(): ?Customer public function getCustomer(): ?Customer
{ {
@@ -136,4 +144,28 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO
return $this; 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 class TimesheetMultiUpdate extends Constraint
{ {
public const MISSING_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d84'; public const MISSING_ACTIVITY_ERROR = 'ts-multi-update-84';
public const MISSING_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d85'; public const MISSING_PROJECT_ERROR = 'ts-multi-update-85';
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'xy5hffg-dsfef3-426a-83d7-1f2d33hs5d86'; public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'ts-multi-update-86';
public const DISABLED_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d87'; public const DISABLED_ACTIVITY_ERROR = 'ts-multi-update-87';
public const DISABLED_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d88'; public const DISABLED_PROJECT_ERROR = 'ts-multi-update-88';
public const DISABLED_CUSTOMER_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d89'; public const DISABLED_CUSTOMER_ERROR = 'ts-multi-update-89';
public const HOURLY_RATE_FIXED_RATE = 'ts-multi-update-90';
protected static $errorNames = [ 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::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::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_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.', self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.', 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.'; public $message = 'This form has invalid settings.';

View File

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

View File

@@ -141,7 +141,7 @@ class PermissionControllerTest extends ControllerBaseTest
self::assertEquals(1, $permission->getRole()->getId()); self::assertEquals(1, $permission->getRole()->getId());
// flush the cache to prevent wrong results // flush the cache to prevent wrong results
$em->clear(RolePermission::class); $em->clear();
// update the permission // update the permission
$this->request($client, '/admin/permissions/roles/1/view_user/0'); $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/')); $this->assertIsRedirect($client, $this->createUrl('/admin/tags/'));
$client->followRedirect(); $client->followRedirect();
$em->clear(Tag::class); $em->clear();
self::assertEquals(0, $em->getRepository(Tag::class)->count([])); self::assertEquals(0, $em->getRepository(Tag::class)->count([]));
} }
} }

View File

@@ -360,7 +360,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/timesheet/')); $this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
$client->followRedirect(); $client->followRedirect();
$em->clear(Timesheet::class); $em->clear();
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([])); self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
} }
@@ -404,11 +404,12 @@ class TimesheetControllerTest extends ControllerBaseTest
$client->submit($form, [ $client->submit($form, [
'timesheet_multi_update' => [ 'timesheet_multi_update' => [
'exported' => true, 'exported' => true,
'tags' => 'test, foo-bar' 'tags' => 'test, foo-bar',
'fixedRate' => 13,
] ]
]); ]);
$em->clear(Timesheet::class); $em->clear();
/** @var Timesheet[] $timesheets */ /** @var Timesheet[] $timesheets */
$timesheets = $em->getRepository(Timesheet::class)->findAll(); $timesheets = $em->getRepository(Timesheet::class)->findAll();
@@ -416,6 +417,7 @@ class TimesheetControllerTest extends ControllerBaseTest
foreach ($timesheets as $timesheet) { foreach ($timesheets as $timesheet) {
self::assertCount(2, $timesheet->getTags()); self::assertCount(2, $timesheet->getTags());
self::assertTrue($timesheet->isExported()); 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\Entity\User;
use App\Form\Type\DateRangeType; use App\Form\Type\DateRangeType;
use App\Tests\DataFixtures\TimesheetFixtures; use App\Tests\DataFixtures\TimesheetFixtures;
use App\Timesheet\Util;
/** /**
* @group integration * @group integration
@@ -276,7 +277,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/')); $this->assertIsRedirect($client, $this->createUrl('/team/timesheet/'));
$client->followRedirect(); $client->followRedirect();
$em->clear(Timesheet::class); $em->clear();
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([])); self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
} }
@@ -323,11 +324,12 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
'user' => $newUser->getId(), 'user' => $newUser->getId(),
'exported' => true, 'exported' => true,
'replaceTags' => 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 */ /** @var Timesheet[] $timesheets */
$timesheets = $em->getRepository(Timesheet::class)->findAll(); $timesheets = $em->getRepository(Timesheet::class)->findAll();
@@ -336,6 +338,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
self::assertCount(3, $timesheet->getTags()); self::assertCount(3, $timesheet->getTags());
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId()); self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
self::assertTrue($timesheet->isExported()); 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); $this->assertHasFlashDeleteSuccess($client);
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion // 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([]); // $timesheets = $em->getRepository(Timesheet::class)->count([]);
// $this->assertEquals(0, $timesheets); // $this->assertEquals(0, $timesheets);

View File

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