diff --git a/config/packages/doctrine/mysql.yaml b/config/packages/doctrine/mysql.yaml index cbe88e91..d152932d 100644 --- a/config/packages/doctrine/mysql.yaml +++ b/config/packages/doctrine/mysql.yaml @@ -3,7 +3,6 @@ doctrine: dql: datetime_functions: addtime: DoctrineExtensions\Query\Mysql\AddTime - convert_tz: DoctrineExtensions\Query\Mysql\ConvertTz date: DoctrineExtensions\Query\Mysql\Date date_format: DoctrineExtensions\Query\Mysql\DateFormat dateadd: DoctrineExtensions\Query\Mysql\DateAdd diff --git a/config/packages/doctrine/sqlite.yaml b/config/packages/doctrine/sqlite.yaml index 54349368..6388865e 100644 --- a/config/packages/doctrine/sqlite.yaml +++ b/config/packages/doctrine/sqlite.yaml @@ -2,7 +2,6 @@ doctrine: orm: dql: datetime_functions: - convert_tz: App\Doctrine\Query\Sqlite\ConvertTz date: DoctrineExtensions\Query\Sqlite\Date date_format: DoctrineExtensions\Query\Sqlite\DateFormat #dateadd: DoctrineExtensions\Query\Sqlite\DateAdd diff --git a/config/services.yaml b/config/services.yaml index 4e80ec3c..a8c82260 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -101,6 +101,14 @@ services: tags: - { name: doctrine.event_subscriber, priority: 50 } + # updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later) + Gedmo\Timestampable\TimestampableListener: + class: Gedmo\Timestampable\TimestampableListener + tags: + - { name: doctrine.event_subscriber, priority: 60 } + calls: + - [ setAnnotationReader, [ "@annotation_reader" ] ] + # make sure, that sqlite supports foreign keys and cascade deletes App\Doctrine\SqliteSessionInitSubscriber: class: App\Doctrine\SqliteSessionInitSubscriber diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 48eced1d..602ea307 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -145,6 +145,7 @@ class TimesheetController extends BaseApiController * @Rest\QueryParam(name="active", requirements="0|1", strict=true, nullable=true, description="Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)") * @Rest\QueryParam(name="full", requirements="true", strict=true, nullable=true, description="Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)") * @Rest\QueryParam(name="term", description="Free search term") + * @Rest\QueryParam(name="modified_after", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.") * * @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')") * @@ -255,6 +256,10 @@ class TimesheetController extends BaseApiController $query->setSearchTerm(new SearchTerm($term)); } + if (!empty($modifiedAfter = $paramFetcher->get('modified_after'))) { + $query->setModifiedAfter($this->dateTime->createDateTime($modifiedAfter)); + } + /** @var Pagerfanta $data */ $data = $this->repository->getPagerfantaForQuery($query); $data = (array) $data->getCurrentPageResults(); diff --git a/src/Doctrine/Query/Sqlite/ConvertTz.php b/src/Doctrine/Query/Sqlite/ConvertTz.php deleted file mode 100644 index a7a69637..00000000 --- a/src/Doctrine/Query/Sqlite/ConvertTz.php +++ /dev/null @@ -1,57 +0,0 @@ -walkArithmeticExpression($this->dateExpression); - $fromTz = $sqlWalker->walkStringPrimary($this->fromTz); - $toTz = $sqlWalker->walkStringPrimary($this->toTz); - - return sprintf('%s', $fieldName); - } - - /** - * {@inheritdoc} - */ - public function parse(Parser $parser) - { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); - - $this->dateExpression = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_COMMA); - - $this->fromTz = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); - - $this->toTz = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); - } -} diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 604b99c6..24fbd014 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -102,6 +102,7 @@ class Invoice * @var string * * @ORM\Column(name="currency", type="string", length=3, nullable=false) + * @Assert\NotNull() * @Assert\Length(max=3) */ private $currency; @@ -110,6 +111,7 @@ class Invoice * @var int * * @ORM\Column(name="due_days", type="integer", length=3, nullable=false) + * @Assert\NotNull() * @Assert\Range(min = 0, max = 999) */ private $dueDays = 30; @@ -118,6 +120,7 @@ class Invoice * @var float * * @ORM\Column(name="vat", type="float", nullable=false) + * @Assert\NotNull() * @Assert\Range(min = 0.0, max = 99.99) */ private $vat = 0.00; @@ -126,13 +129,16 @@ class Invoice * @var string * * @ORM\Column(name="status", type="string", length=20, nullable=false) + * @Assert\NotNull() */ private $status = self::STATUS_NEW; /** * @var string * - * @ORM\Column(name="invoice_filename", type="string", length=100, nullable=false) + * @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false) + * @Assert\NotNull() + * @Assert\Length(min=1, max=150) */ private $invoiceFilename; diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index cbece5b8..c31babe7 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -15,6 +15,7 @@ use DateTimeZone; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Component\Validator\Constraints as Assert; /** @@ -31,18 +32,29 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository") * @ORM\HasLifecycleCallbacks() * @App\Validator\Constraints\Timesheet - * - * columns={"user"} => IDX_4F60C6B18D93D649 => count results for user timesheets - * columns={"activity_id"} => IDX_4F60C6B181C06096 => ??? - * columns={"user","start_time"} => IDX_4F60C6B18D93D649502DF587 => recent activities, user timesheet with date filzer - * columns={"start_time"} => IDX_4F60C6B1502DF587 => team timesheets with timerange filter only - * columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ??? - * columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ??? */ class Timesheet implements EntityWithMetaFields, ExportItemInterface { - public const TYPE_TIMESHEET = 'timesheet'; - public const CATEGORY_WORK = 'work'; + /** + * Category: Normal work-time (default category) + */ + public const WORK = 'work'; + /** + * Category: Holiday + */ + public const HOLIDAY = 'holiday'; + /** + * Category: Sickness + */ + public const SICKNESS = 'sickness'; + /** + * Category: Parental leave + */ + public const PARENTAL = 'parental'; + /** + * Category: Overtime reduction + */ + public const OVERTIME = 'overtime'; /** * @var int|null @@ -161,6 +173,30 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface */ private $exported = false; + /** + * @var bool + * + * @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true}) + * @Assert\NotNull() + */ + private $billable = true; + + /** + * @var string + * + * @ORM\Column(name="category", type="string", length=10, nullable=false, options={"default": "work"}) + * @Assert\NotNull() + */ + private $category = self::WORK; + + /** + * @var DateTime|null + * + * @Gedmo\Timestampable + * @ORM\Column(name="modified_at", type="datetime", nullable=true) + */ + private $modifiedAt; + /** * @var Tag[]|ArrayCollection * @@ -185,9 +221,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface */ private $meta; - /** - * Default constructor, initializes collections - */ public function __construct() { $this->tags = new ArrayCollection(); @@ -472,16 +505,44 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface return $this; } + /** + * This method returns ALWAYS: "timesheet" + * + * @return string + */ public function getType(): string { - // this will be improved in a future version - return self::TYPE_TIMESHEET; + return 'timesheet'; } public function getCategory(): string { - // this will be improved in a future version - return self::CATEGORY_WORK; + return $this->category; + } + + public function setCategory(string $category): Timesheet + { + $allowed = [self::WORK, self::HOLIDAY, self::SICKNESS, self::PARENTAL, self::OVERTIME]; + + if (!\in_array($category, $allowed)) { + throw new \InvalidArgumentException(sprintf('Invalid timesheet category "%s" given, expected one of: %s', $category, implode(', ', $allowed))); + } + + $this->category = $category; + + return $this; + } + + public function isBillable(): bool + { + return $this->billable; + } + + public function setBillable(bool $billable): Timesheet + { + $this->billable = $billable; + + return $this; } public function getFixedRate(): ?float @@ -508,6 +569,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface return $this; } + public function getModifiedAt(): ?DateTime + { + return $this->modifiedAt; + } + /** * @return Collection|MetaTableTypeInterface[] */ diff --git a/src/EventSubscriber/UserEnvironmentSubscriber.php b/src/EventSubscriber/UserEnvironmentSubscriber.php index 4e2dee4d..6e8f2df9 100644 --- a/src/EventSubscriber/UserEnvironmentSubscriber.php +++ b/src/EventSubscriber/UserEnvironmentSubscriber.php @@ -52,9 +52,11 @@ class UserEnvironmentSubscriber implements EventSubscriberInterface $user = $this->storage->getToken()->getUser(); + // the locale depends on the request, not on the user configuration + \Locale::setDefault($event->getRequest()->getLocale()); + if ($user instanceof User) { date_default_timezone_set($user->getTimezone()); - \Locale::setDefault($user->getLocale()); $user->initCanSeeAllData($this->auth->isGranted('view_all_data')); } } diff --git a/src/Migrations/Version20200705152310.php b/src/Migrations/Version20200705152310.php new file mode 100644 index 00000000..35e3b60d --- /dev/null +++ b/src/Migrations/Version20200705152310.php @@ -0,0 +1,63 @@ +getTable('kimai2_invoices'); + $invoices->getColumn('invoice_filename')->setLength(150); + + $timesheet = $schema->getTable('kimai2_timesheet'); + $timesheet->addColumn('billable', 'boolean', ['notnull' => false, 'default' => true]); + $timesheet->addColumn('category', 'string', ['length' => 10, 'notnull' => true, 'default' => 'work']); + $timesheet->addColumn('modified_at', 'datetime', ['notnull' => false]); + } + + public function down(Schema $schema): void + { + $invoices = $schema->getTable('kimai2_invoices'); + $invoices->getColumn('invoice_filename')->setLength(100); + + $timesheet = $schema->getTable('kimai2_timesheet'); + $timesheet->dropColumn('billable'); + $timesheet->dropColumn('category'); + $timesheet->dropColumn('modified_at'); + } + + protected function isSupportingForeignKeys(): bool + { + return false; + } + + public function isTransactional(): bool + { + if ($this->isPlatformSqlite()) { + // does fail if we use transactions, as tables are re-created and foreign keys would fail + return false; + } + + return true; + } +} diff --git a/src/Repository/Query/InvoiceQuery.php b/src/Repository/Query/InvoiceQuery.php index 92aa1e4d..750456ed 100644 --- a/src/Repository/Query/InvoiceQuery.php +++ b/src/Repository/Query/InvoiceQuery.php @@ -22,6 +22,12 @@ class InvoiceQuery extends TimesheetQuery */ private $markAsExported = false; + public function __construct() + { + parent::__construct(); + $this->setBillable(InvoiceQuery::STATE_BILLABLE); + } + public function getTemplate(): ?InvoiceTemplate { return $this->template; diff --git a/src/Repository/Query/TimesheetQuery.php b/src/Repository/Query/TimesheetQuery.php index 0d664d6e..cd60fa9b 100644 --- a/src/Repository/Query/TimesheetQuery.php +++ b/src/Repository/Query/TimesheetQuery.php @@ -24,6 +24,8 @@ class TimesheetQuery extends ActivityQuery public const STATE_STOPPED = 3; public const STATE_EXPORTED = 4; public const STATE_NOT_EXPORTED = 5; + public const STATE_BILLABLE = 6; + public const STATE_NOT_BILLABLE = 7; public const TIMESHEET_ORDER_ALLOWED = ['begin', 'end', 'duration', 'rate', 'customer', 'project', 'activity', 'description']; @@ -43,6 +45,14 @@ class TimesheetQuery extends ActivityQuery * @var int */ protected $exported = self::STATE_ALL; + /** + * @var int + */ + private $billable = self::STATE_ALL; + /** + * @var \DateTime|null + */ + private $modifiedAfter; /** * @var DateRange */ @@ -115,7 +125,7 @@ class TimesheetQuery extends ActivityQuery /** * @return Activity|int|null - * @deprecated since 1.9 - use getProjects() instead - will be removed with 2.0 + * @deprecated since 1.9 - use getActivities() instead - will be removed with 2.0 */ public function getActivity() { @@ -151,7 +161,7 @@ class TimesheetQuery extends ActivityQuery * @param Activity|int $activity * @return $this */ - public function addActivity($activity) + public function addActivity($activity): TimesheetQuery { $this->activities[] = $activity; @@ -162,7 +172,7 @@ class TimesheetQuery extends ActivityQuery * @param Activity[]|int[] $activities * @return $this */ - public function setActivities(array $activities) + public function setActivities(array $activities): TimesheetQuery { $this->activities = $activities; @@ -174,19 +184,22 @@ class TimesheetQuery extends ActivityQuery return !empty($this->activities); } - /** - * @return int - */ - public function getState() + public function getState(): int { return $this->state; } - /** - * @param int $state - * @return TimesheetQuery - */ - public function setState($state) + public function isRunning(): bool + { + return $this->state === self::STATE_RUNNING; + } + + public function isStopped(): bool + { + return $this->state === self::STATE_STOPPED; + } + + public function setState(int $state): TimesheetQuery { $state = (int) $state; if (\in_array($state, [self::STATE_ALL, self::STATE_RUNNING, self::STATE_STOPPED], true)) { @@ -196,19 +209,22 @@ class TimesheetQuery extends ActivityQuery return $this; } - /** - * @return int - */ - public function getExported() + public function getExported(): int { return $this->exported; } - /** - * @param int $exported - * @return TimesheetQuery - */ - public function setExported($exported) + public function isExported(): bool + { + return $this->exported === self::STATE_EXPORTED; + } + + public function isNotExported(): bool + { + return $this->exported === self::STATE_NOT_EXPORTED; + } + + public function setExported(int $exported): TimesheetQuery { $exported = (int) $exported; if (\in_array($exported, [self::STATE_ALL, self::STATE_EXPORTED, self::STATE_NOT_EXPORTED], true)) { @@ -223,11 +239,7 @@ class TimesheetQuery extends ActivityQuery return $this->dateRange->getBegin(); } - /** - * @param \DateTime $begin - * @return TimesheetQuery - */ - public function setBegin($begin) + public function setBegin(\DateTime $begin): TimesheetQuery { $this->dateRange->setBegin($begin); @@ -239,40 +251,26 @@ class TimesheetQuery extends ActivityQuery return $this->dateRange->getEnd(); } - /** - * @param \DateTime $end - * @return TimesheetQuery - */ - public function setEnd($end) + public function setEnd(\DateTime $end): TimesheetQuery { $this->dateRange->setEnd($end); return $this; } - /** - * @return DateRange - */ public function getDateRange(): DateRange { return $this->dateRange; } - /** - * @param DateRange $dateRange - * @return TimesheetQuery - */ - public function setDateRange(DateRange $dateRange) + public function setDateRange(DateRange $dateRange): TimesheetQuery { $this->dateRange = $dateRange; return $this; } - /** - * @return iterable - */ - public function getTags($allowUnknown = false) + public function getTags(bool $allowUnknown = false): iterable { if (empty($this->tags)) { return []; @@ -290,14 +288,46 @@ class TimesheetQuery extends ActivityQuery return $result; } - /** - * @param iterable $tags - * @return $this - */ - public function setTags(iterable $tags) + public function setTags(iterable $tags): TimesheetQuery { $this->tags = $tags; return $this; } + + public function getBillable(): int + { + return $this->billable; + } + + public function isBillable(): bool + { + return $this->billable === self::STATE_BILLABLE; + } + + public function isNotBillable(): bool + { + return $this->billable === self::STATE_NOT_BILLABLE; + } + + public function setBillable(int $billable): TimesheetQuery + { + if (\in_array($billable, [self::STATE_ALL, self::STATE_BILLABLE, self::STATE_NOT_BILLABLE], true)) { + $this->billable = $billable; + } + + return $this; + } + + public function getModifiedAfter(): ?\DateTime + { + return $this->modifiedAfter; + } + + public function setModifiedAfter(\DateTime $modifiedAfter): TimesheetQuery + { + $this->modifiedAfter = $modifiedAfter; + + return $this; + } } diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index bc4beca2..559b7c95 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -753,9 +753,9 @@ class TimesheetRepository extends EntityRepository ->setParameter('begin', $query->getBegin()); } - if (TimesheetQuery::STATE_RUNNING == $query->getState()) { + if ($query->isRunning()) { $qb->andWhere($qb->expr()->isNull('t.end')); - } elseif (TimesheetQuery::STATE_STOPPED == $query->getState()) { + } elseif ($query->isStopped()) { $qb->andWhere($qb->expr()->isNotNull('t.end')); } @@ -764,12 +764,23 @@ class TimesheetRepository extends EntityRepository ->setParameter('end', $query->getEnd()); } - if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) { + if ($query->isExported()) { $qb->andWhere('t.exported = :exported')->setParameter('exported', true, \PDO::PARAM_BOOL); - } elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) { + } elseif ($query->isNotExported()) { $qb->andWhere('t.exported = :exported')->setParameter('exported', false, \PDO::PARAM_BOOL); } + if ($query->isBillable()) { + $qb->andWhere('t.billable = :billable')->setParameter('billable', true, \PDO::PARAM_BOOL); + } elseif ($query->isNotBillable()) { + $qb->andWhere('t.billable = :billable')->setParameter('billable', false, \PDO::PARAM_BOOL); + } + + if (null !== $query->getModifiedAfter()) { + $qb->andWhere($qb->expr()->gte($this->getDatetimeFieldSql('t.modifiedAt'), ':modified_at')) + ->setParameter('modified_at', $query->getModifiedAfter()); + } + if ($query->hasActivities()) { $qb->andWhere($qb->expr()->in('t.activity', ':activity')) ->setParameter('activity', $query->getActivities()); diff --git a/src/Twig/LocaleExtensions.php b/src/Twig/LocaleExtensions.php index 2c01f7c7..f3c19ce7 100644 --- a/src/Twig/LocaleExtensions.php +++ b/src/Twig/LocaleExtensions.php @@ -16,7 +16,6 @@ use App\Utils\Duration; use App\Utils\LocaleFormats; use App\Utils\LocaleHelper; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Intl\Languages; use Symfony\Component\Intl\Locales; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -163,6 +162,8 @@ final class LocaleExtensions extends AbstractExtension } /** + * Returns the currency symbol. + * * @param string $currency * @return string */ diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index 09c269a0..2f4494bf 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -141,6 +141,7 @@ class TimesheetControllerTest extends APIControllerBaseTest public function testGetCollectionWithQuery() { + $modifiedAfter = new \DateTime('-1 hour'); $begin = new \DateTime('first day of this month'); $begin->setTime(0, 0, 0); $end = new \DateTime('last day of this month'); @@ -155,6 +156,7 @@ class TimesheetControllerTest extends APIControllerBaseTest 'order' => 'DESC', 'orderBy' => 'rate', 'active' => 0, + 'modified_after' => $modifiedAfter->format(self::DATE_FORMAT_HTML5), 'begin' => $begin->format(self::DATE_FORMAT_HTML5), 'end' => $end->format(self::DATE_FORMAT_HTML5), 'exported' => 0, diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php index 697e91ec..bf5c41ea 100644 --- a/tests/Entity/TimesheetTest.php +++ b/tests/Entity/TimesheetTest.php @@ -33,6 +33,8 @@ class TimesheetTest extends TestCase self::assertNull($sut->getId()); self::assertNull($sut->getBegin()); self::assertNull($sut->getEnd()); + self::assertTrue($sut->isBillable()); + self::assertNull($sut->getModifiedAt()); self::assertSame(0, $sut->getDuration()); self::assertNull($sut->getUser()); self::assertNull($sut->getActivity()); @@ -140,4 +142,34 @@ class TimesheetTest extends TestCase self::assertEquals(3, $sut->getMetaFields()->count()); self::assertCount(2, $sut->getVisibleMetaFields()); } + + public function testBillable() + { + $sut = new Timesheet(); + self::assertTrue($sut->isBillable()); + self::assertInstanceOf(Timesheet::class, $sut->setBillable(false)); + self::assertFalse($sut->isBillable()); + self::assertInstanceOf(Timesheet::class, $sut->setBillable(true)); + self::assertTrue($sut->isBillable()); + } + + public function testCategory() + { + $sut = new Timesheet(); + self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::HOLIDAY)); + self::assertEquals('holiday', $sut->getCategory()); + self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::WORK)); + self::assertEquals('work', $sut->getCategory()); + self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::SICKNESS)); + self::assertEquals('sickness', $sut->getCategory()); + self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::PARENTAL)); + self::assertEquals('parental', $sut->getCategory()); + self::assertInstanceOf(Timesheet::class, $sut->setCategory(Timesheet::OVERTIME)); + self::assertEquals('overtime', $sut->getCategory()); + + self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime'); + + $sut->setCategory('foo'); + } } diff --git a/tests/Repository/Query/InvoiceQueryTest.php b/tests/Repository/Query/InvoiceQueryTest.php index c0f1f27e..09158b31 100644 --- a/tests/Repository/Query/InvoiceQueryTest.php +++ b/tests/Repository/Query/InvoiceQueryTest.php @@ -10,6 +10,7 @@ namespace App\Tests\Repository\Query; use App\Repository\Query\InvoiceQuery; +use App\Repository\Query\TimesheetQuery; /** * @covers \App\Repository\Query\InvoiceQuery @@ -33,13 +34,19 @@ class InvoiceQueryTest extends TimesheetQueryTest $this->assertState($sut); $this->assertExported($sut); $this->assertMarkAsExported($sut); + $this->assertModifiedAfter($sut); + + self::assertEquals(TimesheetQuery::STATE_BILLABLE, $sut->getBillable()); + self::assertTrue($sut->isBillable()); + self::assertFalse($sut->isNotBillable()); + $this->assertBillable($sut); } protected function assertMarkAsExported(InvoiceQuery $sut) { - $this->assertFalse($sut->isMarkAsExported()); + self::assertFalse($sut->isMarkAsExported()); $sut->setMarkAsExported(true); - $this->assertTrue($sut->isMarkAsExported()); + self::assertTrue($sut->isMarkAsExported()); } } diff --git a/tests/Repository/Query/TimesheetQueryTest.php b/tests/Repository/Query/TimesheetQueryTest.php index 63f8066d..26c4a1f6 100644 --- a/tests/Repository/Query/TimesheetQueryTest.php +++ b/tests/Repository/Query/TimesheetQueryTest.php @@ -35,23 +35,29 @@ class TimesheetQueryTest extends BaseQueryTest $this->assertState($sut); $this->assertExported($sut); $this->assertSearchTerm($sut); + $this->assertModifiedAfter($sut); + + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable()); + self::assertFalse($sut->isBillable()); + self::assertFalse($sut->isNotBillable()); + $this->assertBillable($sut); $this->assertResetByFormError(new TimesheetQuery(), 'begin', 'DESC'); } protected function assertUser(TimesheetQuery $sut) { - $this->assertNull($sut->getUser()); + self::assertNull($sut->getUser()); $expected = new User(); $expected->setUsername('foo-bar'); - $sut->setUser($expected); - $this->assertEquals($expected, $sut->getUser()); + self::assertInstanceOf(TimesheetQuery::class, $sut->setUser($expected)); + self::assertEquals($expected, $sut->getUser()); } protected function assertUsers(TimesheetQuery $sut) { - $this->assertEmpty($sut->getUsers()); + self::assertEmpty($sut->getUsers()); $user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock(); $user->method('getId')->willReturn(1); @@ -70,43 +76,86 @@ class TimesheetQueryTest extends BaseQueryTest $sut->addUser($user); $sut->removeUser($user); - $this->assertCount(2, $sut->getUsers()); + self::assertCount(2, $sut->getUsers()); } protected function assertState(TimesheetQuery $sut) { - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); + self::assertFalse($sut->isRunning()); + self::assertFalse($sut->isStopped()); - $sut->setState(PHP_INT_MAX); - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); + self::assertInstanceOf(TimesheetQuery::class, $sut->setState(PHP_INT_MAX)); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); $sut->setState(TimesheetQuery::STATE_STOPPED); - $this->assertEquals(TimesheetQuery::STATE_STOPPED, $sut->getState()); + self::assertEquals(TimesheetQuery::STATE_STOPPED, $sut->getState()); + self::assertFalse($sut->isRunning()); + self::assertTrue($sut->isStopped()); $sut->setState(TimesheetQuery::STATE_RUNNING); - $this->assertEquals(TimesheetQuery::STATE_RUNNING, $sut->getState()); + self::assertEquals(TimesheetQuery::STATE_RUNNING, $sut->getState()); + self::assertTrue($sut->isRunning()); + self::assertFalse($sut->isStopped()); $sut->setState(TimesheetQuery::STATE_ALL); - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getState()); } protected function assertExported(TimesheetQuery $sut) { - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + self::assertFalse($sut->isExported()); + self::assertFalse($sut->isNotExported()); - $sut->setExported(PHP_INT_MAX); - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + self::assertInstanceOf(TimesheetQuery::class, $sut->setExported(PHP_INT_MAX)); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); $sut->setExported(TimesheetQuery::STATE_EXPORTED); - $this->assertEquals(TimesheetQuery::STATE_EXPORTED, $sut->getExported()); + self::assertEquals(TimesheetQuery::STATE_EXPORTED, $sut->getExported()); + self::assertTrue($sut->isExported()); + self::assertFalse($sut->isNotExported()); $sut->setExported(TimesheetQuery::STATE_NOT_EXPORTED); - $this->assertEquals(TimesheetQuery::STATE_NOT_EXPORTED, $sut->getExported()); + self::assertEquals(TimesheetQuery::STATE_NOT_EXPORTED, $sut->getExported()); + self::assertFalse($sut->isExported()); + self::assertTrue($sut->isNotExported()); $sut->setExported(TimesheetQuery::STATE_ALL); - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); $sut->setExported('02'); - $this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported()); + } + + protected function assertModifiedAfter(TimesheetQuery $sut) + { + self::assertNull($sut->getModifiedAfter()); + $date = new \DateTime('-3 hours'); + + self::assertInstanceOf(TimesheetQuery::class, $sut->setModifiedAfter($date)); + self::assertNotNull($sut->getModifiedAfter()); // just here to fix a PHPStan issue + self::assertSame($date, $sut->getModifiedAfter()); + } + + protected function assertBillable(TimesheetQuery $sut) + { + self::assertInstanceOf(TimesheetQuery::class, $sut->setBillable(TimesheetQuery::STATE_ALL)); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable()); + self::assertFalse($sut->isBillable()); + self::assertFalse($sut->isNotBillable()); + + $sut->setBillable(PHP_INT_MAX); + self::assertEquals(TimesheetQuery::STATE_ALL, $sut->getBillable()); + + $sut->setBillable(TimesheetQuery::STATE_BILLABLE); + self::assertEquals(TimesheetQuery::STATE_BILLABLE, $sut->getBillable()); + self::assertTrue($sut->isBillable()); + self::assertFalse($sut->isNotBillable()); + + $sut->setBillable(TimesheetQuery::STATE_NOT_BILLABLE); + self::assertEquals(TimesheetQuery::STATE_NOT_BILLABLE, $sut->getBillable()); + self::assertFalse($sut->isBillable()); + self::assertTrue($sut->isNotBillable()); } } diff --git a/var/data/kimai_test.sqlite b/var/data/kimai_test.sqlite index 3babc73b..6ae4a7c7 100644 Binary files a/var/data/kimai_test.sqlite and b/var/data/kimai_test.sqlite differ