diff --git a/src/Controller/TimesheetAbstractController.php b/src/Controller/TimesheetAbstractController.php index d6eb064d..18607a7b 100644 --- a/src/Controller/TimesheetAbstractController.php +++ b/src/Controller/TimesheetAbstractController.php @@ -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) { diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdate.php b/src/Form/MultiUpdate/TimesheetMultiUpdate.php index 6dda99ab..64ac684c 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdate.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdate.php @@ -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, ]); diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php index 3d19ff91..33c6a7f0 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php @@ -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; + } } diff --git a/src/Validator/Constraints/TimesheetMultiUpdate.php b/src/Validator/Constraints/TimesheetMultiUpdate.php index a33d85a9..efa472b3 100644 --- a/src/Validator/Constraints/TimesheetMultiUpdate.php +++ b/src/Validator/Constraints/TimesheetMultiUpdate.php @@ -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.'; diff --git a/src/Validator/Constraints/TimesheetMultiUpdateValidator.php b/src/Validator/Constraints/TimesheetMultiUpdateValidator.php index 7be847d6..f21a1a0f 100644 --- a/src/Validator/Constraints/TimesheetMultiUpdateValidator.php +++ b/src/Validator/Constraints/TimesheetMultiUpdateValidator.php @@ -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(); } } diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index f2d2a743..71c60196 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -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'); diff --git a/tests/Controller/TagControllerTest.php b/tests/Controller/TagControllerTest.php index bade42a9..0ed1bce9 100644 --- a/tests/Controller/TagControllerTest.php +++ b/tests/Controller/TagControllerTest.php @@ -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([])); } } diff --git a/tests/Controller/TimesheetControllerTest.php b/tests/Controller/TimesheetControllerTest.php index e40c7e16..935d4543 100644 --- a/tests/Controller/TimesheetControllerTest.php +++ b/tests/Controller/TimesheetControllerTest.php @@ -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()); } } } diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php index 9bdb5df3..d3efb52f 100644 --- a/tests/Controller/TimesheetTeamControllerTest.php +++ b/tests/Controller/TimesheetTeamControllerTest.php @@ -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()); } } } diff --git a/tests/Controller/UserControllerTest.php b/tests/Controller/UserControllerTest.php index d8b6cfa9..fa872981 100644 --- a/tests/Controller/UserControllerTest.php +++ b/tests/Controller/UserControllerTest.php @@ -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); diff --git a/tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php b/tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php index b45228cb..3735e08a 100644 --- a/tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php +++ b/tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php @@ -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()); } } diff --git a/tests/Validator/Constraints/TimesheetMultiUpdateValidatorTest.php b/tests/Validator/Constraints/TimesheetMultiUpdateValidatorTest.php new file mode 100644 index 00000000..2c38580a --- /dev/null +++ b/tests/Validator/Constraints/TimesheetMultiUpdateValidatorTest.php @@ -0,0 +1,139 @@ +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(); + } +}