Release 2.0.15 (#3970)

This commit is contained in:
Kevin Papst
2023-04-16 00:44:00 +02:00
committed by GitHub
parent ecd8ee85f3
commit 046ed313c9
42 changed files with 796 additions and 935 deletions

View File

@@ -34,10 +34,10 @@ p {
.text-center {
text-align: center;
}
.text-left {
.text-left, .text-begin {
text-align: left;
}
.text-right {
.text-right, .text-end {
text-align: right;
}
.text-nowrap {

View File

@@ -80,7 +80,6 @@ body {
font-size: 20px;
> small {
color: #666;
display: block;
margin-top: 5px;
}
@@ -107,6 +106,13 @@ body {
text-align: right;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.invoice-items {
.table {
thead th {

98
composer.lock generated
View File

@@ -1449,16 +1449,16 @@
},
{
"name": "doctrine/orm",
"version": "2.14.1",
"version": "2.14.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/orm.git",
"reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e"
"reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/orm/zipball/de7eee5ed7b1b35c99b118f26f210a8281e6db8e",
"reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e",
"url": "https://api.github.com/repos/doctrine/orm/zipball/e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52",
"reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52",
"shasum": ""
},
"require": {
@@ -1487,14 +1487,14 @@
"doctrine/annotations": "^1.13 || ^2",
"doctrine/coding-standard": "^9.0.2 || ^11.0",
"phpbench/phpbench": "^0.16.10 || ^1.0",
"phpstan/phpstan": "~1.4.10 || 1.9.8",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"phpstan/phpstan": "~1.4.10 || 1.10.6",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.1",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"vimeo/psalm": "4.30.0 || 5.4.0"
"vimeo/psalm": "4.30.0 || 5.9.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
@@ -1544,9 +1544,9 @@
],
"support": {
"issues": "https://github.com/doctrine/orm/issues",
"source": "https://github.com/doctrine/orm/tree/2.14.1"
"source": "https://github.com/doctrine/orm/tree/2.14.2"
},
"time": "2023-01-16T18:36:59+00:00"
"time": "2023-03-30T15:18:54+00:00"
},
{
"name": "doctrine/persistence",
@@ -4068,16 +4068,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.17.1",
"version": "1.18.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "d3753fcb3abc6f78f5de6f72153d4b9c99c72dee"
"reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/d3753fcb3abc6f78f5de6f72153d4b9c99c72dee",
"reference": "d3753fcb3abc6f78f5de6f72153d4b9c99c72dee",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f",
"reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f",
"shasum": ""
},
"require": {
@@ -4107,9 +4107,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.17.1"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1"
},
"time": "2023-04-04T11:11:22+00:00"
"time": "2023-04-07T11:51:11+00:00"
},
{
"name": "psr/cache",
@@ -4265,21 +4265,21 @@
},
{
"name": "psr/http-client",
"version": "1.0.1",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0"
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
@@ -4299,7 +4299,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
@@ -4311,27 +4311,27 @@
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client/tree/master"
"source": "https://github.com/php-fig/http-client/tree/1.0.2"
},
"time": "2020-06-29T06:28:15+00:00"
"time": "2023-04-10T20:12:12+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
"reference": "e616d01114759c4c489f93b099585439f795fe35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
"reference": "e616d01114759c4c489f93b099585439f795fe35",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
@@ -4351,7 +4351,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
@@ -4366,9 +4366,9 @@
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory/tree/master"
"source": "https://github.com/php-fig/http-factory/tree/1.0.2"
},
"time": "2019-04-30T12:38:16+00:00"
"time": "2023-04-10T20:10:41+00:00"
},
{
"name": "psr/http-message",
@@ -10691,16 +10691,16 @@
},
{
"name": "doctrine/doctrine-fixtures-bundle",
"version": "3.4.2",
"version": "3.4.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineFixturesBundle.git",
"reference": "601988c5b46dbd20a0f886f967210aba378a6fd5"
"reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/601988c5b46dbd20a0f886f967210aba378a6fd5",
"reference": "601988c5b46dbd20a0f886f967210aba378a6fd5",
"url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/fd39829fed8f090ef6e185d33449d47c2fb59c9c",
"reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c",
"shasum": ""
},
"require": {
@@ -10754,7 +10754,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues",
"source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.2"
"source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.3"
},
"funding": [
{
@@ -10770,7 +10770,7 @@
"type": "tidelift"
}
],
"time": "2022-04-28T17:58:29+00:00"
"time": "2023-04-11T12:37:36+00:00"
},
{
"name": "fakerphp/faker",
@@ -11174,16 +11174,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.10.11",
"version": "1.10.12",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21"
"reference": "7b2aaf999e522e2b10011a594cf06dc37de7fbd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8aa62e6ea8b58ffb650e02940e55a788cbc3fe21",
"reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/7b2aaf999e522e2b10011a594cf06dc37de7fbd8",
"reference": "7b2aaf999e522e2b10011a594cf06dc37de7fbd8",
"shasum": ""
},
"require": {
@@ -11232,7 +11232,7 @@
"type": "tidelift"
}
],
"time": "2023-04-04T19:17:42+00:00"
"time": "2023-04-12T10:33:29+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
@@ -11407,16 +11407,16 @@
},
{
"name": "phpstan/phpstan-symfony",
"version": "1.2.24",
"version": "1.2.25",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096"
"reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/db81b1861aac7cc2e66115cb33b4d1ea2a73d096",
"reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/1da7bf450c6b351fec08ca0aa97298473d4f6ab3",
"reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3",
"shasum": ""
},
"require": {
@@ -11472,9 +11472,9 @@
"description": "Symfony Framework extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.24"
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.25"
},
"time": "2023-03-30T08:38:10+00:00"
"time": "2023-04-05T12:16:20+00:00"
},
{
"name": "phpunit/php-code-coverage",

View File

@@ -4330,11 +4330,6 @@ parameters:
count: 2
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getAlias\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getCurrency\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1
@@ -4345,26 +4340,11 @@ parameters:
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
count: 2
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getTitle\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Cannot call method getUserIdentifier\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydrator\\:\\:hydrate\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@@ -4430,11 +4410,6 @@ parameters:
count: 1
path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
-
message: "#^Cannot call method getBegin\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
-
message: "#^Cannot call method getCompany\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#"
count: 1
@@ -4450,11 +4425,6 @@ parameters:
count: 1
path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
-
message: "#^Cannot call method getEnd\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php
-
message: "#^Cannot call method getLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#"
count: 1
@@ -4790,11 +4760,6 @@ parameters:
count: 1
path: src/Kernel.php
-
message: "#^Method App\\\\Ldap\\\\FormLoginLdapFactory\\:\\:createAuthenticator\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
count: 1
path: src/Ldap/FormLoginLdapFactory.php
-
message: "#^Method App\\\\Ldap\\\\LdapAuthenticator\\:\\:supports\\(\\) should return bool but returns bool\\|null\\.$#"
count: 1
@@ -5735,11 +5700,6 @@ parameters:
count: 1
path: src/Repository/TagRepository.php
-
message: "#^Method App\\\\Repository\\\\TagRepository\\:\\:findIdsByTagNameList\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/TagRepository.php
-
message: "#^Method App\\\\Repository\\\\TagRepository\\:\\:findTagsByName\\(\\) has parameter \\$tagNames with no value type specified in iterable type array\\.$#"
count: 1

View File

@@ -24,7 +24,7 @@
"/build/invoice.c8ae95ad.js"
],
"css": [
"/build/invoice.3c80ee80.css"
"/build/invoice.d0ae14e1.css"
]
},
"invoice-pdf": {
@@ -33,7 +33,7 @@
"/build/invoice-pdf.d86b82ee.js"
],
"css": [
"/build/invoice-pdf.c88953bb.css"
"/build/invoice-pdf.2b749265.css"
]
},
"chart": {
@@ -68,9 +68,9 @@
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
"/build/invoice.3c80ee80.css": "sha384-xjFM2m/EeN7z42ygpt77ll5zAcHTeOjbZFAw2Qcu3IJutgebiAbrXjnH5aJfF5Z6",
"/build/invoice.d0ae14e1.css": "sha384-DZ9f2EsTAIOVoFBZe4jMaW3R6yNVGCtcn3Dq3hxZnWRDdFzhyt1O1iQqRZUyQFTM",
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
"/build/calendar.8577d9cd.js": "sha384-1qlF+4tr7/zve+wvZef7sVcbUxhK0/yEm5Hbuct8K/ZV4JtENdNY7YRLqOcZ/8Y3",
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",

View File

@@ -0,0 +1 @@
body{font-family:sans-serif;font-size:10pt;margin:4px;padding:0}h1,h2,h3,h4,h5,p,table,td,th,tr{margin:0;padding:0}th{border:none;text-align:left}td{vertical-align:top}p{margin-bottom:12px}.text-small{font-size:7pt}.padding-left{padding-left:15px}.text-center{text-align:center}.text-begin,.text-left{text-align:left}.text-end,.text-right{text-align:right}.text-nowrap{white-space:nowrap}.date{font-size:80%;padding-top:6px;text-align:right}table{border-spacing:0;width:100%}p,table.addresses{line-height:14pt}table.addresses{margin-bottom:30px}table.header{border-bottom:1px solid #999;padding-bottom:4px}table.header .title{font-size:140%;font-weight:400}table.footer{border-top:1px solid #999;font-size:80%;line-height:14px;padding-top:8px}table.footer td{vertical-align:top}table.items{border-collapse:collapse;margin-bottom:60px;margin-top:50px}table.items tbody td,table.items thead th{border-bottom:1px solid #ccc}table.items tbody td.first,table.items thead th.first{padding-left:0}table.items tbody td.last,table.items tfoot td.last,table.items thead th.last{padding-right:0}table.items td,table.items th{padding:8px 6px}table.items tfoot td,table.items tfoot th{line-height:1px;padding:20px 0 0}table.items tfoot td.last{padding-left:10px}table.items tr.odd{background-color:#f5f5f5}

View File

@@ -1 +0,0 @@
body{font-family:sans-serif;font-size:10pt;margin:4px;padding:0}h1,h2,h3,h4,h5,p,table,td,th,tr{margin:0;padding:0}th{border:none;text-align:left}td{vertical-align:top}p{margin-bottom:12px}.text-small{font-size:7pt}.padding-left{padding-left:15px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-nowrap{white-space:nowrap}.date{font-size:80%;padding-top:6px;text-align:right}table{border-spacing:0;width:100%}p,table.addresses{line-height:14pt}table.addresses{margin-bottom:30px}table.header{border-bottom:1px solid #999;padding-bottom:4px}table.header .title{font-size:140%;font-weight:400}table.footer{border-top:1px solid #999;font-size:80%;line-height:14px;padding-top:8px}table.footer td{vertical-align:top}table.items{border-collapse:collapse;margin-bottom:60px;margin-top:50px}table.items tbody td,table.items thead th{border-bottom:1px solid #ccc}table.items tbody td.first,table.items thead th.first{padding-left:0}table.items tbody td.last,table.items tfoot td.last,table.items thead th.last{padding-right:0}table.items td,table.items th{padding:8px 6px}table.items tfoot td,table.items tfoot th{line-height:1px;padding:20px 0 0}table.items tfoot td.last{padding-left:10px}table.items tr.odd{background-color:#f5f5f5}

File diff suppressed because one or more lines are too long

View File

@@ -3,9 +3,9 @@
"build/app.js": "/build/app.cd312ca8.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
"build/invoice.css": "/build/invoice.3c80ee80.css",
"build/invoice.css": "/build/invoice.d0ae14e1.css",
"build/invoice.js": "/build/invoice.c8ae95ad.js",
"build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css",
"build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css",
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",
"build/chart.js": "/build/chart.f5becfac.js",
"build/calendar.css": "/build/calendar.bb473428.css",

View File

@@ -76,6 +76,7 @@ final class TimesheetController extends BaseApiController
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Rest\QueryParam(name: 'user', requirements: '\d+|all', strict: true, nullable: true, description: "User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")]
#[Rest\QueryParam(name: 'users', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of user IDs to filter, e.g.: users[]=1&users[]=2 (ignored if user=all)')]
#[Rest\QueryParam(name: 'customer', requirements: '\d+', strict: true, nullable: true, description: 'Customer ID to filter timesheets')]
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
#[Rest\QueryParam(name: 'project', requirements: '\d+', strict: true, nullable: true, description: 'Project ID to filter timesheets')]
@@ -98,24 +99,32 @@ final class TimesheetController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
{
$query = new TimesheetQuery(false);
$query->setUser($this->getUser());
$seeAll = false;
if ($this->isGranted('view_other_timesheet')) {
/** @var array<int> $users */
$users = $paramFetcher->get('users');
$userId = $paramFetcher->get('user');
if (\is_string($userId) && $userId !== '') {
if ('all' === $userId) {
$query->setUser(null);
} else {
/** @var User|null $user */
$user = $userRepository->find($userId);
if ($user === null) {
throw $this->createNotFoundException('Unknown user: ' . $userId);
}
$query->setUser($user);
if ('all' === $userId) {
$seeAll = true;
} elseif (\is_string($userId) && $userId !== '') {
$users[] = (int) $userId;
}
if (!$seeAll) {
foreach ($userRepository->findByIds($users) as $user) {
$query->addUser($user);
}
}
}
if ($seeAll) {
$query->setUser(null);
} elseif (!$query->hasUsers()) {
$query->setUser($this->getUser());
}
/** @var array<int> $customers */
$customers = $paramFetcher->get('customers');
$customer = $paramFetcher->get('customer');
@@ -476,6 +485,7 @@ final class TimesheetController extends BaseApiController
#[IsGranted('start', 'timesheet')]
#[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
#[Rest\Get(path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])]
#[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.14';
public const VERSION = '2.0.15';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20014;
public const VERSION_ID = 20015;
/**
* The software name
*/

View File

@@ -672,6 +672,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
$this->id = null;
}
// field will not be set, if it contains a value
$this->modifiedAt = null;
$this->exported = false;
$currentMeta = $this->meta;

View File

@@ -25,7 +25,7 @@ final class ExportToolbarForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivitySelect($builder, [], true, true, false);

View File

@@ -26,7 +26,7 @@ final class InvoiceToolbarForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivitySelect($builder, [], true, true, false);

View File

@@ -155,10 +155,10 @@ trait ToolbarFormTrait
]);
}
protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true, bool $required = false): void
protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true): void
{
$params = [
'required' => $required,
'required' => !$allowEmpty,
'allow_empty' => $allowEmpty,
];

View File

@@ -134,6 +134,10 @@ final class DateRangeType extends AbstractType
return $range;
}
if ($dates === null) {
throw new TransformationFailedException('Date range missing');
}
if (preg_match($pattern, $dates) !== 1) {
throw new TransformationFailedException('Invalid date range given');
}

View File

@@ -76,9 +76,20 @@ final class TagsSelectType extends AbstractType
$foundTagNames[] = $tag->getName();
}
/** @var array<string> $newNames */
$newNames = array_diff($newNames, $foundTagNames);
foreach ($newNames as $name) {
/** @var array<string> $newNamesCreate */
$newNamesCreate = array_udiff($newNames, $foundTagNames, function (mixed $userTag, mixed $existingTag) {
if (!\is_string($userTag) || !\is_string($existingTag)) {
return -1;
}
if (mb_strtolower($userTag) === mb_strtolower($existingTag)) {
return 0;
}
return strcmp($userTag, $existingTag);
});
foreach ($newNamesCreate as $name) {
$tag = new Tag();
$tag->setName(mb_substr($name, 0, 100));
$this->tagRepository->saveTag($tag);

View File

@@ -77,4 +77,9 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter
$this->locale = $locale;
$this->formatter = null;
}
public function getFormattedAmount(float $amount): string
{
return $this->getFormatter()->amount($amount);
}
}

View File

@@ -34,7 +34,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
if ($item->isFixedRate()) {
$appliedRate = $item->getFixedRate();
$amount = $item->getAmount();
$amount = $formatter->getFormattedAmount($item->getAmount());
}
$activity = $item->getActivity();
@@ -83,12 +83,18 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.date' => $formatter->getFormattedDateTime($begin),
'entry.week' => \intval($begin->format('W')),
'entry.weekyear' => $begin->format('o'),
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUserIdentifier(),
'entry.user_title' => $user->getTitle() ?? '',
'entry.user_alias' => $user->getAlias() ?? '',
];
if (null !== $user) {
$values = array_merge($values, [
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUserIdentifier(),
'entry.user_title' => $user->getTitle() ?? '',
'entry.user_alias' => $user->getAlias() ?? '',
'entry.user_display' => $user->getDisplayName() ?? '',
]);
}
if (null !== $activity) {
$values = array_merge($values, [
'entry.activity' => $activity->getName(),

View File

@@ -35,7 +35,8 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
'customer.name' => $customer->getName() ?? '',
'customer.contact' => $customer->getContact() ?? '',
'customer.company' => $customer->getCompany() ?? '',
'customer.vat' => $customer->getVatId() ?? '',
'customer.vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
'customer.vat_id' => $customer->getVatId() ?? '',
'customer.number' => $customer->getNumber() ?? '',
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage() ?? '',

View File

@@ -22,9 +22,6 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
$subtotal = $model->getCalculator()->getSubtotal();
$formatter = $model->getFormatter();
$begin = $model->getQuery()->getBegin();
$end = $model->getQuery()->getEnd();
$values = [
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
@@ -33,6 +30,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
'invoice.vat' => $model->getCalculator()->getVat(),
'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00,
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false),
'invoice.tax_plain' => $tax,
@@ -65,25 +63,66 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'query.end_month' => '', // since 1.9
'query.end_month_number' => '', // since 1.9
'query.end_year' => '', // since 1.9
// since 2.0.15
'user.see_others' => ($model->getQuery()?->getUser() === null),
];
if ($begin !== null) {
$values = array_merge($values, [
'query.day' => $begin->format('d'), // @deprecated - but impossible to delete
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete
'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete
'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin),
'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'),
'query.begin_year' => $begin->format('Y'),
'query.end' => $formatter->getFormattedDateTime($end), // since 1.9
'query.end_day' => $end->format('d'), // since 1.9
'query.end_month' => $formatter->getFormattedMonthName($end), // since 1.9
'query.end_month_number' => $end->format('m'), // since 1.9
'query.end_year' => $end->format('Y'), // since 1.9
]);
$query = $model->getQuery();
if ($query !== null) {
$begin = $query->getBegin();
$end = $query->getEnd();
if ($begin !== null) {
$values = array_merge($values, [
'query.day' => $begin->format('d'),
// @deprecated - but impossible to delete
'query.month' => $formatter->getFormattedMonthName($begin),
// @deprecated - but impossible to delete
'query.month_number' => $begin->format('m'),
// @deprecated - but impossible to delete
'query.year' => $begin->format('Y'),
// @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin),
'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'),
'query.begin_year' => $begin->format('Y'),
'query.end' => $formatter->getFormattedDateTime($end),
// since 1.9
'query.end_day' => $end->format('d'),
// since 1.9
'query.end_month' => $formatter->getFormattedMonthName($end),
// since 1.9
'query.end_month_number' => $end->format('m'),
// since 1.9
'query.end_year' => $end->format('Y'),
// since 1.9
]);
}
// since 2.0.15
$activity = $query->getActivity();
if ($activity !== null) {
$prefix = 'query.activity.';
$values = array_merge($values, [
$prefix . 'name' => $activity->getName() ?? '',
$prefix . 'comment' => $activity->getComment() ?? '',
]);
}
// since 2.0.15
$project = $query->getProject();
if ($project !== null) {
$prefix = 'query.project.';
$values = array_merge($values, [
$prefix . 'name' => $project->getName() ?? '',
$prefix . 'comment' => $project->getComment() ?? '',
$prefix . 'order_number' => $project->getOrderNumber(),
]);
}
}
$entries = $model->getEntries();

View File

@@ -9,7 +9,6 @@
namespace App\Invoice\Hydrator;
use App\Entity\UserPreference;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
@@ -28,9 +27,9 @@ final class InvoiceModelUserHydrator implements InvoiceModelHydrator
'user.email' => $user->getEmail(),
'user.title' => $user->getTitle() ?? '',
'user.alias' => $user->getAlias() ?? '',
'user.display' => $user->getDisplayName() ?? '',
];
/** @var UserPreference $metaField */
foreach ($user->getPreferences() as $metaField) {
$values = array_merge($values, [
'user.meta.' . $metaField->getName() => $metaField->getValue(),

View File

@@ -24,6 +24,8 @@ interface InvoiceFormatter
public function getFormattedTime(DateTime $date): string;
public function getFormattedAmount(float $amount): string;
public function getFormattedMoney(float $amount, ?string $currency, bool $withCurrency = true): string;
public function getFormattedMonthName(DateTime $date): string;

View File

@@ -278,7 +278,7 @@ final class ServiceInvoice
{
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
if (null === $document) {
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
throw new \Exception('Please adjust your invoice template, the renderer is invalid: ' . $model->getTemplate()->getRenderer());
}
foreach ($this->getRenderer() as $renderer) {
@@ -426,7 +426,7 @@ final class ServiceInvoice
$calculator = $this->getCalculatorByName($template->getCalculator());
if (null === $calculator) {
throw new \Exception('Please adjust your invoice template, the invoice calculator is invalid: ' . $template->getCalculator());
throw new \Exception('Please adjust your invoice template, the sum calculator is invalid: ' . $template->getCalculator());
}
$model->setCalculator($calculator);

View File

@@ -93,6 +93,14 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
return array_values($this->users);
}
/**
* Check if there is one or more users in the query
*/
public function hasUsers(): bool
{
return $this->timesheetUser !== null || \count($this->users) > 0;
}
/**
* Limit the data exclusively to the user.
*/

View File

@@ -66,28 +66,6 @@ class TagRepository extends EntityRepository
return $this->findOneBy(['name' => $tagName]);
}
/**
* Find ids of the given tagNames separated by comma
* @param string $tagNames
* @return array
*/
public function findIdsByTagNameList(string $tagNames): array
{
$qb = $this
->createQueryBuilder('t')
->select('t.id');
$list = array_filter(array_unique(array_map('trim', explode(',', $tagNames))));
$cnt = 0;
foreach ($list as $listElem) {
$qb
->orWhere('t.name like :elem' . $cnt)
->setParameter('elem' . $cnt, '%' . $listElem . '%');
$cnt++;
}
return array_column($qb->getQuery()->getScalarResult(), 'id');
}
/**
* Find all tag names in an alphabetical order
*

View File

@@ -129,7 +129,7 @@
<td class="text-nowrap text-end">{{ stats.internalRate|money(currency) }}</td>
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
</tr>
{% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 %}
{% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 and stats.duration|chart_duration > 0.00 %}
{% set realHourlyRate = stats.rateBillable / stats.duration|chart_duration %}
<tr>
<td>{{ 'hourlyRate'|trans }} ({{ 'billable'|trans }} / {{ durationTrans|trans }})</td>

View File

@@ -1,13 +1,8 @@
<!DOCTYPE html>
{%- set fallback = app.request is not null ? app.request.locale : 'en' -%}
{%- set language = model.template.language|default(fallback) -%}
{%- set isDecimal = true -%}
{%- set currency = model.currency -%}
{%- set project = model.query.project -%}
<html lang="{{ language }}">
<html lang="{{ invoice['invoice.language'] }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
<meta charset="utf-8">
<title>{% block title %}{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}{% endblock %}</title>
<style>
{{ encore_entry_css_source('invoice-pdf')|raw }}
</style>
@@ -18,10 +13,10 @@
<table class="header">
<tr>
<td class="title">
{{ model.template.title }}
{{ invoice['template.title'] }}
</td>
<td class="date text-right">
{{ 'date'|trans }}: {{ model.invoiceDate|date_short }}
{{ 'date'|trans }}: {{ invoice['invoice.date'] }}
</td>
</tr>
</table>
@@ -32,10 +27,10 @@
<tr>
<td>
<strong>{{ 'contact'|trans }}</strong>:
{{ model.template.contact|nl2str(' ') }}
{{ invoice['template.contact']|nl2str(' ') }}
<br>
<strong>{{ 'invoice_bank_account'|trans }}</strong>:
{{ model.template.paymentDetails|nl2str(' ') }}
{{ invoice['template.payment_details']|nl2str(' ') }}
</td>
<td align="right">
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
@@ -50,24 +45,24 @@ mpdf-->
<td style="width:60%">
{{ 'invoice.to'|trans }}
<br>
<strong>{{ model.customer.company|default(model.customer.name) }}</strong>
<strong>{{ invoice['customer.company']|default(invoice['customer.name']) }}</strong>
<br>
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
{{ invoice['customer.address']|nl2br }}
{% if invoice['customer.vat_id'] is not empty %}
<br>
{{ 'vat_id'|trans }}: {{ model.customer.vatId }}
{{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }}
{% endif %}
</td>
<td>
{{ 'invoice.from'|trans }}
<br>
<strong>{{ model.template.company }}</strong>
<strong>{{ invoice['template.company'] }}</strong>
<br>
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
{{ invoice['template.address']|trim|nl2br }}
{% if invoice['template.vat_id'] is not empty %}
<br>
{{ 'vat_id'|trans }}:
{{ model.template.vatId }}
{{ invoice['template.vat_id'] }}
{% endif %}
</td>
</tr>
@@ -75,27 +70,27 @@ mpdf-->
<p>
{{ 'invoice.number'|trans }}:
{{ model.invoiceNumber }}
{{ invoice['invoice.number'] }}
<br>
{{ 'invoice.due_days'|trans }}:
{{ model.dueDate|date_short }}
{{ invoice['invoice.due_date'] }}
{% if model.customer.number is not empty %}
{% if invoice['customer.number'] is not empty %}
<br>
{{ 'number'|trans }}:
{{ model.customer.number }}
{{ invoice['customer.number'] }}
{% endif %}
{% if project is not null and project.orderNumber is not empty %}
{% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
<br>
{{ 'orderNumber'|trans }}:
{{ project.orderNumber }}
{{ invoice['query.project.order_number'] }}
{% endif %}
</p>
{% if model.customer.invoiceText is not empty%}
<p>{{ model.customer.invoiceText|md2html }}</p>
{% if invoice['customer.invoice_text'] is not empty%}
<p>{{ invoice['customer.invoice_text']|md2html }}</p>
{% endif %}
<table class="items">
@@ -109,43 +104,39 @@ mpdf-->
</tr>
</thead>
<tbody>
{% for id, entry in model.calculator.entries %}
{% for invoiceLineItem in entries %}
<!-- CONTENT_PART -->
{% set duration = entry.duration|duration(isDecimal) %}
{% if entry.fixedRate %}
{% set rate = entry.fixedRate %}
{% set duration = entry.amount|amount %}
{% else %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td nowrap class="first text-nowrap">{{ entry.begin|date_short }}</td>
<td contenteditable="true">
{% if entry.description is not empty %}
{{ entry.description|nl2br }}
<td nowrap class="first text-nowrap">{{ invoiceLineItem['entry.begin'] }}</td>
<td>
{% if invoiceLineItem['entry.description'] is not empty %}
{{ invoiceLineItem['entry.description']|nl2br }}
{% else %}
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% if invoiceLineItem['entry.activity'] is defined %}
{{ invoiceLineItem['entry.activity'] }} /
{% endif %}
{{ invoiceLineItem['entry.project'] }}
{% endif %}
</td>
<td nowrap class="text-nowrap text-right">{{ rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
<td nowrap class="last text-nowrap text-right">{{ entry.rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ invoiceLineItem['entry.rate'] }}</td>
<td nowrap class="text-nowrap text-right">{{ invoiceLineItem['entry.amount'] }}</td>
<td nowrap class="last text-nowrap text-right">{{ invoiceLineItem['entry.total'] }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if model.calculator.tax > 0 or not model.isHideZeroTax() %}
{% if not invoice['invoice.tax_hide'] %}
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.subtotal'|trans }}
</td>
<td class="last text-right">{{ model.calculator.subtotal|money(currency) }}</td>
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
</tr>
<tr>
<td colspan="4" class="text-right">
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
</td>
<td class="last text-right">{{ model.calculator.tax|money(currency) }}</td>
<td class="last text-right">{{ invoice['invoice.tax'] }}</td>
</tr>
{% endif %}
<tr>
@@ -153,16 +144,16 @@ mpdf-->
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="last text-right">
<strong>{{ model.calculator.total|money(currency) }}</strong>
<strong>{{ invoice['invoice.total'] }}</strong>
</td>
</tr>
</tfoot>
</table>
{% if model.template.paymentTerms is not empty %}
<div class="paymentTerms">
{{ model.template.paymentTerms|md2html }}
</div>
{% if invoice['template.payment_terms'] is not empty %}
<div class="paymentTerms">
{{ invoice['template.payment_terms']|md2html }}
</div>
{% endif %}
</body>
</html>

View File

@@ -1,166 +1,158 @@
<!DOCTYPE html>
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = true %}
{% set currency = model.currency %}
{% set project = model.query.project %}
<html lang="{{ language }}">
<html lang="{{ invoice['invoice.language'] }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
<style type="text/css">
<title>{% block title %}{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}{% endblock %}</title>
<style>
{{ encore_entry_css_source('invoice')|raw }}
</style>
</head>
<body class="invoice_print">
<div class="wrapper">
<section class="invoice">
<div class="wrapper">
<section class="invoice">
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">{{ model.template.title }}</span>
<small class="pull-right">{{ 'date'|trans }}: {{ model.invoiceDate|date_short }}</small>
</h2>
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">{{ invoice['template.title'] }}</span>
</h2>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-5">
{{ 'invoice.to'|trans }}
<address contenteditable="true">
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
{% if model.customer.number is not empty %}
<br>
{{ 'number'|trans }}: {{ model.customer.number }}
{% endif %}
{% if project is not null and project.orderNumber is not empty %}
<br>
{{ 'orderNumber'|trans }}: {{ project.orderNumber }}
{% endif %}
</address>
<div class="row">
<div class="col-sm-5">
{{ 'invoice.to'|trans }}
<address contenteditable="true">
<strong>{{ invoice['customer.company']|default(invoice['customer.name']) }}</strong><br>
{{ invoice['customer.address']|nl2br }}
{% if invoice['customer.vat_id'] is not empty %}
<br>
{{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }}
{% endif %}
{% if invoice['customer.number'] is not empty %}
<br>
{{ 'number'|trans }}: {{ invoice['customer.number'] }}
{% endif %}
{% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
<br>
{{ 'orderNumber'|trans }}: {{ invoice['query.project.order_number'] }}
{% endif %}
</address>
</div>
<div class="col-sm-2"></div>
<div class="col-sm-5">
{{ 'invoice.from'|trans }}
<address contenteditable="true">
<strong>{{ invoice['template.company'] }}</strong><br>
{{ invoice['template.address']|trim|nl2br }}
{% if invoice['template.vat_id'] is not empty %}
<br>
{{ 'vat_id'|trans }}:
{{ invoice['template.vat_id'] }}
{% endif %}
</address>
</div>
</div>
<div class="col-sm-2"></div>
<div class="col-sm-5">
{{ 'invoice.from'|trans }}
<address contenteditable="true">
<strong>{{ model.template.company }}</strong><br>
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
<div class="row">
<div class="col-sm-5">
<p contenteditable="true">
<strong>{{ 'date'|trans }}:</strong>
{{ invoice['invoice.date'] }}
<br>
{{ 'vat_id'|trans }}:
{{ model.template.vatId }}
{% endif %}
</address>
<strong>{{ 'invoice.number'|trans }}:</strong>
{{ invoice['invoice.number'] }}
<br>
<strong>{{ 'invoice.due_days'|trans }}:</strong>
{{ invoice['invoice.due_date'] }}
</p>
</div>
<div class="col-sm-7"></div>
</div>
</div>
<div class="row">
<div class="col-sm-5">
<p contenteditable="true">
<strong>{{ 'invoice.number'|trans }}:</strong>
{{ model.invoiceNumber }}
<br>
<strong>{{ 'invoice.due_days'|trans }}:</strong>
{{ model.dueDate|date_short }}
</p>
</div>
<div class="col-sm-7"></div>
</div>
<div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ 'date'|trans }}</th>
<th>{{ 'description'|trans }}</th>
<th class="text-end">{{ 'unit_price'|trans }}</th>
<th class="text-end">{{ 'amount'|trans }}</th>
<th class="text-end">{{ 'total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
{% for entry in model.calculator.entries %}
{% set duration = entry.duration|duration(isDecimal) %}
{% if entry.fixedRate %}
{% set rate = entry.fixedRate %}
{% set duration = entry.amount|amount %}
{% else %}
{% set rate = entry.hourlyRate %}
<div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ 'date'|trans }}</th>
<th>{{ 'description'|trans }}</th>
<th class="text-end">{{ 'unit_price'|trans }}</th>
<th class="text-end">{{ 'amount'|trans }}</th>
<th class="text-end">{{ 'total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
{% for invoiceLineItem in entries %}
<tr>
<td nowrap class="text-nowrap">{{ invoiceLineItem['entry.begin'] }}</td>
<td contenteditable="true">
{% if invoiceLineItem['entry.description'] is not empty %}
{{ invoiceLineItem['entry.description']|nl2br }}
{% else %}
{% if invoiceLineItem['entry.activity'] is defined %}
{{ invoiceLineItem['entry.activity'] }} /
{% endif %}
{{ invoiceLineItem['entry.project'] }}
{% endif %}
</td>
<td nowrap class="text-nowrap text-end">{{ invoiceLineItem['entry.rate'] }}</td>
<td nowrap class="text-nowrap text-end">{{ invoiceLineItem['entry.amount'] }}</td>
<td nowrap class="text-nowrap text-end">{{ invoiceLineItem['entry.total'] }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if not invoice['invoice.tax_hide'] %}
<tr>
<td colspan="4" class="text-end">
{{ 'invoice.subtotal'|trans }}
</td>
<td class="text-end">{{ invoice['invoice.subtotal'] }}</td>
</tr>
<tr>
<td colspan="4" class="text-end">
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
</td>
<td class="text-end">{{ invoice['invoice.tax'] }}</td>
</tr>
{% endif %}
<tr>
<td nowrap class="text-nowrap">{{ entry.begin|date_short }}</td>
<td contenteditable="true">
{% if entry.description is not empty %}
{{ entry.description|nl2br }}
{% else %}
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% endif %}
<td colspan="4" class="text-end text-nowrap">
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td nowrap class="text-nowrap text-end">{{ rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-end">{{ duration }}</td>
<td nowrap class="text-nowrap text-end">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if model.calculator.tax > 0 or not model.isHideZeroTax() %}
<tr>
<td colspan="4" class="text-end">
{{ 'invoice.subtotal'|trans }}
<td class="text-end">
<strong>{{ invoice['invoice.total'] }}</strong>
</td>
<td class="text-end">{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-end">
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
</td>
<td class="text-end">{{ model.calculator.tax|money(currency) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12">
{% if invoice['template.payment_terms'] is not empty %}
<div contenteditable="true" class="paymentTerms">
{{ invoice['template.payment_terms']|md2html }}
</div>
{% endif %}
<tr>
<td colspan="4" class="text-end text-nowrap">
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="text-end">
<strong>{{ model.calculator.total|money(currency) }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
{% if model.template.paymentTerms is not empty %}
<div contenteditable="true" class="paymentTerms">
{{ model.template.paymentTerms|md2html }}
</div>
{% endif %}
</div>
</div>
<footer class="footer">
<p>
<strong>{{ 'address'|trans }}</strong>: {{ invoice['template.company'] }} &ndash; {{ invoice['template.address']|nl2str(' ') }}
<br>
<strong>{{ 'invoice_bank_account'|trans }}</strong>: {{ invoice['template.payment_details']|nl2str(' ') }}
<br>
<strong>{{ 'contact'|trans }}</strong>: {{ invoice['template.contact']|nl2str(' ') }}
</p>
</footer>
<footer class="footer">
<p>
<strong>{{ 'address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' ') }}
<br>
<strong>{{ 'invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' ') }}
<br>
<strong>{{ 'contact'|trans }}</strong>: {{ model.template.contact|nl2str(' ') }}
</p>
</footer>
</section>
</div>
</section>
</div>
</body>
</html>

View File

@@ -1,13 +1,8 @@
<!DOCTYPE html>
{%- set fallback = app.request is not null ? app.request.locale : 'en' -%}
{%- set language = model.template.language|default(fallback) -%}
{%- set isDecimal = true -%}
{%- set currency = model.currency -%}
{%- set project = model.query.project -%}
<html lang="{{ language }}">
<html lang="{{ invoice['invoice.language'] }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
<meta charset="utf-8">
<title>{% block title %}{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}{% endblock %}</title>
<style>
{{ encore_entry_css_source('invoice-pdf')|raw }}
</style>
@@ -18,9 +13,9 @@
<table class="header">
<tr>
<td class="text-small">
{{ model.template.company }} &ndash; {{ model.template.address|nl2str(' ') }}
{% if model.template.vatId is not empty %}
&ndash; {{ 'vat_id'|trans }}: {{ model.template.vatId }}
{{ invoice['template.company'] }} &ndash; {{ invoice['template.address']|nl2str(' ') }}
{% if invoice['template.vat_id'] is not empty %}
&ndash; {{ 'vat_id'|trans }}: {{ invoice['template.vat_id'] }}
{% endif %}
</td>
<td class="text-small text-right">
@@ -36,18 +31,18 @@
<td style="width: 33%">
<strong>{{ 'address'|trans }}</strong>
<br>
{{ model.template.company }}<br>
{{ model.template.address|nl2br }}
{{ invoice['template.company'] }}<br>
{{ invoice['template.address']|nl2br }}
</td>
<td class="text-center">
<strong>{{ 'invoice_bank_account'|trans }}</strong>
<br>
{{ model.template.paymentDetails|nl2br }}
{{ invoice['template.payment_details']|nl2br }}
</td>
<td class="text-right" style="width: 33%">
<strong>{{ 'contact'|trans }}</strong>
<br>
{{ model.template.contact|nl2br }}
{{ invoice['template.contact']|nl2br }}
</td>
</tr>
</table>
@@ -57,8 +52,8 @@ mpdf-->
<table class="addresses">
<tr>
<td>
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
{{ model.customer.address|nl2br }}
<strong>{{ invoice['customer.company']|default(invoice['customer.name']) }}</strong><br>
{{ invoice['customer.address']|nl2br }}
</td>
<td class="text-right">
{% set classLeft = 'text-left' %}
@@ -66,36 +61,36 @@ mpdf-->
<table style="width: 240px">
<tr>
<td class="{{ classLeft }}">{{ 'date'|trans }}</td>
<td class="{{ classRight }}">{{ model.invoiceDate|date_short }}</td>
<td class="{{ classRight }}">{{ invoice['invoice.date'] }}</td>
</tr>
<tr>
<td class="{{ classLeft }}">{{ 'invoice.service_date'|trans }}</td>
<td class="{{ classRight }}">{{ model.query.end|month_name }} {{ model.query.end|date('Y') }}</td>
<td class="{{ classRight }}">{{ invoice['query.end_month'] }} {{ invoice['query.end_year'] }}</td>
</tr>
<tr>
<td class="{{ classLeft }}">{{ 'invoice.number'|trans }}</td>
<td class="{{ classRight }}">{{ model.invoiceNumber }}</td>
<td class="{{ classRight }}">{{ invoice['invoice.number'] }}</td>
</tr>
<tr>
<td class="{{ classLeft }}">{{ 'invoice.due_days'|trans }}</td>
<td class="{{ classRight }}">{{ model.dueDate|date_short }}</td>
<td class="{{ classRight }}">{{ invoice['invoice.due_date'] }}</td>
</tr>
{% if model.customer.number is not empty %}
{% if invoice['customer.number'] is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'number'|trans }}</td>
<td class="{{ classRight }}">{{ model.customer.number }}</td>
<td class="{{ classRight }}">{{ invoice['customer.number'] }}</td>
</tr>
{% endif %}
{% if project is not null and project.orderNumber is not empty %}
{% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'orderNumber'|trans }}</td>
<td class="{{ classRight }}">{{ project.orderNumber }}</td>
<td class="{{ classRight }}">{{ invoice['query.project.order_number'] }}</td>
</tr>
{% endif %}
{% if model.customer.vatId is not empty %}
{% if invoice['customer.vat_id'] is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'vat_id'|trans }}</td>
<td class="{{ classRight }}">{{ model.customer.vatId }}</td>
<td class="{{ classRight }}">{{ invoice['customer.vat_id'] }}</td>
</tr>
{% endif %}
</table>
@@ -103,10 +98,10 @@ mpdf-->
</tr>
</table>
<h2 style="margin-top: 60px">{{ model.template.title }}</h2>
<h2 style="margin-top: 60px">{{ invoice['template.title'] }}</h2>
{% if model.customer.invoiceText is not empty%}
<p>{{ model.customer.invoiceText|md2html }}</p>
{% if invoice['customer.invoice_text'] is not empty%}
<p>{{ invoice['customer.invoice_text']|md2html }}</p>
{% endif %}
<table class="items">
@@ -119,43 +114,38 @@ mpdf-->
</tr>
</thead>
<tbody>
{% for entry in model.calculator.entries %}
{% for invoiceLineItem in entries %}
<!-- CONTENT_PART -->
{% set duration = entry.duration|duration(isDecimal) %}
{% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %}
{% set duration = entry.amount|amount %}
{% else %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td class="first">
{% if entry.description is not empty %}
{{ entry.description|nl2br }}
{% if invoiceLineItem['entry.description'] is not empty %}
{{ invoiceLineItem['entry.description']|nl2br }}
{% else %}
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% if invoiceLineItem['entry.activity'] is defined %}
{{ invoiceLineItem['entry.activity'] }} /
{% endif %}
{{ invoiceLineItem['entry.project'] }}
{% endif %}
</td>
<td class="text-right text-nowrap">{{ rate|money(currency) }}</td>
<td class="text-right text-nowrap">{{ duration }}</td>
<td class="last text-right text-nowrap">{{ entry.rate|money(currency) }}</td>
<td class="text-right text-nowrap">{{ invoiceLineItem['entry.rate'] }}</td>
<td class="text-right text-nowrap">{{ invoiceLineItem['entry.amount'] }}</td>
<td class="last text-right text-nowrap">{{ invoiceLineItem['entry.total'] }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if model.calculator.tax > 0 or not model.isHideZeroTax() %}
{% if not invoice['invoice.tax_hide'] %}
<tr>
<td colspan="3" class="text-right">
{{ 'invoice.subtotal'|trans }}
</td>
<td class="last text-right">{{ model.calculator.subtotal|money(currency) }}</td>
<td class="last text-right">{{ invoice['invoice.subtotal'] }}</td>
</tr>
<tr>
<td colspan="3" class="text-right">
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
{{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%)
</td>
<td class="last text-right">{{ model.calculator.tax|money(currency) }}</td>
<td class="last text-right">{{ invoice['invoice.tax'] }}</td>
</tr>
{% endif %}
<tr>
@@ -163,16 +153,16 @@ mpdf-->
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="last text-right">
<strong>{{ model.calculator.total|money(currency) }}</strong>
<strong>{{ invoice['invoice.total'] }}</strong>
</td>
</tr>
</tfoot>
</table>
{% if model.template.paymentTerms is not empty %}
<p>
{{ model.template.paymentTerms|md2html }}
</p>
{% if invoice['template.payment_terms'] is not empty %}
<div class="paymentTerms">
{{ invoice['template.payment_terms']|md2html }}
</div>
{% endif %}
</body>
</html>

View File

@@ -1,88 +1,91 @@
<!DOCTYPE html>
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = true %}
{% set project = model.query.project %}
{% set activity = model.query.activity %}
<html lang="{{ language }}">
<html lang="{{ invoice['invoice.language'] }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<title>{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}</title>
<style type="text/css">
<title>{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}</title>
<style>
{{ encore_entry_css_source('invoice')|raw }}
</style>
</head>
<body class="invoice_print">
<div class="wrapper">
<section class="invoice">
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">{{ model.template.title }}</span>
<span contenteditable="true">{{ invoice['template.title'] }}</span>
</h2>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table no-border table-sm">
<tr>
<th class="ps-0">{{ 'invoice.from'|trans }}</th>
<td contenteditable="true">
{% if model.query.user is not empty %}
{{ model.query.user.displayName }}
{% if not invoice['user.see_others'] and invoice['user.display'] is defined %}
{{ invoice['user.display'] }}
{% else %}
{{ model.template.company }}
{{ invoice['template.company'] }}
{% endif %}
</td>
</tr>
<tr>
<th class="ps-0">{{ 'date'|trans }}</th>
<td contenteditable="true">
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %}
{{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
{% if invoice['query.begin_month_number'] != invoice['query.end_month_number'] or invoice['query.begin_year'] != invoice['query.end_year'] %}
{{ invoice['query.begin'] }} - {{ invoice['query.end'] }}
{% else %}
{{ model.query.end|month_name }} {{ model.query.end|date('Y') }}
{{ invoice['query.end_month'] }} {{ invoice['query.end_year'] }}
{% endif %}
</td>
</tr>
<tr>
<th class="ps-0">{{ 'customer'|trans }}</th>
<td contenteditable="true">
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
{% if invoice['customer.number'] is not empty %}
[{{ invoice['customer.number'] }}]
{% endif %}
{{ invoice['customer.name'] }}
{% if invoice['customer.contact'] is not empty %}
/ {{ invoice['customer.contact'] }}
{% endif %}
</td>
</tr>
{% if project is not null %}
{% if invoice['query.project.name'] is defined %}
<tr>
<th class="ps-0">{{ 'project'|trans }}</th>
<td contenteditable="true">
{{ project.name }}
{% if project.orderNumber is not empty %}
({{ 'orderNumber'|trans }}: {{ project.orderNumber }})
{{ invoice['query.project.name'] }}
{% if invoice['query.project.order_number'] is not empty %}
({{ 'orderNumber'|trans }}: {{ invoice['query.project.order_number'] }})
{% endif %}
</td>
</tr>
{% endif %}
{% if activity is not null %}
{% if invoice['query.activity.name'] is defined %}
<tr>
<th class="ps-0">{{ 'activity'|trans }}</th>
<td contenteditable="true">
{{ activity.name }}
{{ invoice['query.activity.name'] }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{{ 'date'|trans }}</th>
{% if model.query.user is empty %}
{% if invoice['user.see_others'] %}
<th>{{ 'user'|trans }}</th>
{% endif %}
<th>{{ 'activity'|trans }}</th>
@@ -90,44 +93,48 @@
</tr>
</thead>
<tbody>
{% for entry in model.calculator.entries %}
{% for timesheet in entries %}
<tr>
<td>{{ entry.begin|date_short }}</td>
{% if model.query.user is empty %}
<td>{{ entry.user.displayName }}</td>
<td>{{ timesheet['entry.begin'] }}</td>
{% if invoice['user.see_others'] %}
<td>{{ timesheet['entry.user_display'] }}</td>
{% endif %}
<td contenteditable="true">
{% if entry.description is not empty %}
{{ entry.description|nl2br }}
{% if timesheet['entry.description'] is not empty %}
{{ timesheet['entry.description']|nl2br }}
{% else %}
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% if timesheet['entry.activity'] is defined %}
{{ timesheet['entry.activity'] }} /
{% endif %}
{{ timesheet['entry.project'] }}
{% endif %}
</td>
<td class="text-right text-nowrap">{{ entry.duration|duration(isDecimal) }}</td>
<td class="text-right text-nowrap">{{ timesheet['entry.amount'] }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th></th>
{% if model.query.user is empty %}
{% if invoice['user.see_others'] %}
<th></th>
{% endif %}
<th>{{ 'invoice.total_working_time'|trans }}</th>
<th class="text-right text-nowrap">{{ model.calculator.timeWorked|duration(isDecimal) }}</th>
<th class="text-right text-nowrap">{{ invoice['invoice.duration_decimal'] }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12">
{% if model.template.paymentTerms is not empty %}
{% if invoice['template.payment_terms'] is not empty %}
<p class="lead">{{ 'payment_terms'|trans }}</p>
<p class="text-muted well well-sm no-shadow" contenteditable="true" style="margin-bottom: 100px">
{{ model.template.paymentTerms|trim|nl2br }}
</p>
<div class="paymentTerms" contenteditable="true" style="margin-bottom: 100px">
{{ invoice['template.payment_terms']|md2html }}
</div>
{% endif %}
<div class="table-responsive">
@@ -136,6 +143,7 @@
</div>
</div>
</div>
</section>
</div>
</body>

View File

@@ -49,38 +49,44 @@ class TimesheetControllerTest extends APIControllerBaseTest
return $this->importFixture($fixture);
}
public function testIsSecure()
public function testIsSecure(): void
{
$this->assertUrlIsSecured('/api/timesheets');
}
public function testGetCollection()
public function testGetCollection(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets');
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionFull()
public function testGetCollectionFull(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', ['full' => 'true']);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
public function testGetCollectionForOtherUser()
public function testGetCollectionForOtherUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER);
@@ -97,15 +103,30 @@ class TimesheetControllerTest extends APIControllerBaseTest
$query = ['user' => 2];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['users' => [2]];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionForAllUser()
public function testGetCollectionForAllUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER);
@@ -122,26 +143,32 @@ class TimesheetControllerTest extends APIControllerBaseTest
$query = ['user' => 'all'];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(17, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(17, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionForEmptyResult()
public function testGetCollectionForEmptyResult(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->assertAccessIsGranted($client, '/api/timesheets');
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetCollectionWithQuery()
public function testGetCollectionWithQuery(): void
{
$modifiedAfter = new \DateTime('-1 hour');
$begin = new \DateTime('first day of this month');
@@ -167,16 +194,18 @@ class TimesheetControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER, 22);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertPagination($client->getResponse(), 2, 4, 6, 22);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(4, \count($result));
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(4, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionWithQueryFailsWith404OnOutOfRangedPage()
public function testGetCollectionWithQueryFailsWith404OnOutOfRangedPage(): void
{
$begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0);
@@ -194,7 +223,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiException($client->getResponse(), ['code' => 404, 'message' => 'Not Found']);
}
public function testGetCollectionWithSingleParamsQuery()
public function testGetCollectionWithSingleParamsQuery(): void
{
$begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0);
@@ -218,15 +247,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(5, \count($result));
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(5, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testExportedFilter()
public function testExportedFilter(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -255,11 +286,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(7, \count($result));
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(7, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [
@@ -271,11 +304,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = [
@@ -285,15 +320,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => $end->format(self::DATE_FORMAT_HTML5),
];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(17, \count($result));
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(17, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetEntity()
public function testGetEntity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
@@ -323,9 +360,11 @@ class TimesheetControllerTest extends APIControllerBaseTest
$em->flush();
$this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheet->getId());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$expected = [
@@ -353,7 +392,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
}
}
public function testGetEntityAccessDenied()
public function testGetEntityAccessDenied(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -363,23 +402,25 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiAccessDenied($client, '/api/timesheets/' . $timesheets[0]->getId(), 'Access denied.');
}
public function testGetEntityAccessAllowedForAdmin()
public function testGetEntityAccessAllowedForAdmin(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
}
public function testGetEntityNotFound()
public function testGetEntityNotFound(): void
{
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Timesheet object not found by the @ParamConverter annotation.');
}
public function testPostAction()
public function testPostAction(): void
{
$dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -396,16 +437,19 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
self::assertEquals(2016, $result['rate']);
$this->assertFalse($result['billable']);
}
public function testPostActionWithFullExpandedResponse()
public function testPostActionWithFullExpandedResponse(): void
{
$dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -422,16 +466,19 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets?full=true', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetExpanded', $result);
$this->assertNotEmpty($result['id']);
self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
self::assertEquals(2016, $result['rate']);
$this->assertTrue($result['billable']);
}
public function testPostActionForDifferentUser()
public function testPostActionForDifferentUser(): void
{
$dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -451,18 +498,21 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
$this->assertEquals($user->getId(), $result['user']);
self::assertNotEmpty($result['id']);
self::assertEquals($user->getId(), $result['user']);
$this->assertNotEquals($admin->getId(), $result['user']);
$this->assertTrue($result['billable']);
}
// check for project, as this is a required field. It will not be included in the select, as it is
// already filtered within the repository due to the hidden customer
public function testPostActionWithInvisibleProject()
public function testPostActionWithInvisibleProject(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
@@ -496,7 +546,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
// check for activity, as this is a required field. It will not be included in the select, as it is
// already filtered within the repository due to the hidden flag
public function testPostActionWithInvisibleActivity()
public function testPostActionWithInvisibleActivity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
@@ -528,7 +578,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiCallValidationError($client->getResponse(), ['activity']);
}
public function testPostActionWithNonBillableCustomer()
public function testPostActionWithNonBillableCustomer(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
@@ -557,13 +607,16 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertFalse($result['billable']);
}
public function testPostActionWithNonBillableCustomerExplicit()
public function testPostActionWithNonBillableCustomerExplicit(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
@@ -593,14 +646,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
// explicit overwritten values win!
$this->assertTrue($result['billable']);
}
public function testPatchAction()
public function testPatchAction(): void
{
$dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
@@ -616,16 +672,19 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
$this->assertEquals(25200, $result['duration']);
$this->assertEquals('foo', $result['description']);
self::assertNotEmpty($result['id']);
self::assertEquals(25200, $result['duration']);
self::assertEquals('foo', $result['description']);
$this->assertFalse($result['billable']);
}
public function testPatchActionWithInvalidUser()
public function testPatchActionWithInvalidUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -654,12 +713,12 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($response);
}
public function testPatchActionWithUnknownTimesheet()
public function testPatchActionWithUnknownTimesheet(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/255', []);
}
public function testInvalidPatchAction()
public function testInvalidPatchAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -674,48 +733,51 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
self::assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['activity'], false, ['End date must not be earlier then start date.']);
}
// TODO: TEST PATCH FOR EXPORTED TIMESHEET FOR USER WITHOUT PERMISSION IS REJECTED
public function testDeleteAction()
public function testDeleteAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
self::assertNotEmpty($result['id']);
$id = $result['id'];
$this->request($client, '/api/timesheets/' . $id, 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
}
public function testDeleteActionWithUnknownTimesheet()
public function testDeleteActionWithUnknownTimesheet(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertNotFoundForDelete($client, '/api/timesheets/255');
}
public function testDeleteActionForDifferentUser()
public function testDeleteActionForDifferentUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
}
public function testDeleteActionWithoutAuthorization()
public function testDeleteActionWithoutAuthorization(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -727,7 +789,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($response);
}
public function testDeleteActionForExportedRecordIsNotAllowed()
public function testDeleteActionForExportedRecordIsNotAllowed(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -746,7 +808,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($response);
}
public function testDeleteActionForExportedRecordIsAllowedForAdmin()
public function testDeleteActionForExportedRecordIsAllowedForAdmin(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
@@ -763,7 +825,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
}
public function testGetRecentAction()
public function testGetRecentAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -785,15 +847,18 @@ class TimesheetControllerTest extends APIControllerBaseTest
];
$this->assertAccessIsGranted($client, '/api/timesheets/recent', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(1, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(1, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
public function testActiveAction()
public function testActiveAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
@@ -813,14 +878,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/active');
$this->assertTrue($client->getResponse()->isSuccessful());
$results = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals(3, \count($results));
foreach ($results as $timesheet) {
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertEquals(3, \count($result));
foreach ($result as $timesheet) {
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet);
}
}
public function testStopAction()
public function testStopAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -842,9 +910,12 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em = $this->getEntityManager();
@@ -853,7 +924,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
}
public function testStopActionTriggersValidationOnLongRunning()
public function testStopActionTriggersValidationOnLongRunning(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->setSystemConfiguration('timesheet.rules.long_running_duration', 750);
@@ -877,12 +948,12 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiCallValidationError($client->getResponse(), ['duration' => 'Maximum 12:30 hours allowed.']);
}
public function testStopThrowsNotFound()
public function testStopThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/11/stop', []);
}
public function testStopNotAllowedForUser()
public function testStopNotAllowedForUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -905,7 +976,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.');
}
public function testGetCollectionWithTags()
public function testGetCollectionWithTags(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
@@ -924,33 +995,42 @@ class TimesheetControllerTest extends APIControllerBaseTest
$query = ['tags' => ['Test']];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Test', 'Admin']];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(10, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
$query = ['tags' => ['Nothing-2-see', 'here']];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(20, \count($result));
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(20, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testRestartAction()
public function testRestartAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -965,7 +1045,10 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']);
@@ -975,13 +1058,13 @@ class TimesheetControllerTest extends APIControllerBaseTest
$timesheet = $em->getRepository(Timesheet::class)->find($result['id']);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
$this->assertNull($timesheet->getEnd());
$this->assertEquals(1, $timesheet->getActivity()->getId());
$this->assertEquals(1, $timesheet->getProject()->getId());
self::assertEquals(1, $timesheet->getActivity()->getId());
self::assertEquals(1, $timesheet->getProject()->getId());
$this->assertEmpty($timesheet->getDescription());
$this->assertEmpty($timesheet->getTags());
}
public function testRestartActionWithBegin()
public function testRestartActionWithBegin(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -997,7 +1080,10 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]);
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEmpty($result['description']);
$this->assertEmpty($result['tags']);
@@ -1006,15 +1092,15 @@ class TimesheetControllerTest extends APIControllerBaseTest
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find($result['id']);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
$this->assertEquals($begin->format(BaseApiController::DATE_FORMAT_PHP), $timesheet->getBegin()->format(BaseApiController::DATE_FORMAT_PHP));
self::assertEquals($begin->format(BaseApiController::DATE_FORMAT_PHP), $timesheet->getBegin()->format(BaseApiController::DATE_FORMAT_PHP));
$this->assertNull($timesheet->getEnd());
$this->assertEquals(1, $timesheet->getActivity()->getId());
$this->assertEquals(1, $timesheet->getProject()->getId());
self::assertEquals(1, $timesheet->getActivity()->getId());
self::assertEquals(1, $timesheet->getProject()->getId());
$this->assertEmpty($timesheet->getDescription());
$this->assertEmpty($timesheet->getTags());
}
public function testRestartActionWithCopyData()
public function testRestartActionWithCopyData(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1034,29 +1120,32 @@ class TimesheetControllerTest extends APIControllerBaseTest
$em->flush();
$timesheet = $em->getRepository(Timesheet::class)->find($id);
$this->assertEquals('foo', $timesheet->getDescription());
self::assertEquals('foo', $timesheet->getDescription());
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['copy' => 'all']);
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEquals('foo', $result['description']);
$this->assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
$this->assertEquals(['another', 'testing', 'bar'], $result['tags']);
self::assertEquals('foo', $result['description']);
self::assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']);
self::assertEquals(['another', 'testing', 'bar'], $result['tags']);
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find($result['id']);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
$this->assertNull($timesheet->getEnd());
$this->assertEquals(1, $timesheet->getActivity()->getId());
$this->assertEquals(1, $timesheet->getProject()->getId());
$this->assertEquals('foo', $timesheet->getDescription());
$this->assertEquals(['another', 'testing', 'bar'], $timesheet->getTagsAsArray());
self::assertEquals(1, $timesheet->getActivity()->getId());
self::assertEquals(1, $timesheet->getProject()->getId());
self::assertEquals('foo', $timesheet->getDescription());
self::assertEquals(['another', 'testing', 'bar'], $timesheet->getTagsAsArray());
}
public function testRestartNotAllowedForUser()
public function testRestartNotAllowedForUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
@@ -1078,12 +1167,12 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.');
}
public function testRestartThrowsNotFound()
public function testRestartThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/42/restart', []);
}
public function testDuplicateAction()
public function testDuplicateAction(): void
{
$dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE));
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -1099,30 +1188,36 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
self::assertEquals(2016, $result['rate']);
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
self::assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
self::assertEquals(2016, $result['rate']);
}
public function testDuplicateThrowsNotFound()
public function testDuplicateThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/11/duplicate', []);
}
public function testExportAction()
public function testExportAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1135,7 +1230,11 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->request($client, '/api/timesheets/' . $id . '/export', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$em->clear();
@@ -1151,7 +1250,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertFalse($timesheet->isExported());
}
public function testExportNotAllowedForUser()
public function testExportNotAllowedForUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1161,17 +1260,17 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.');
}
public function testExportThrowsNotFound()
public function testExportThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . PHP_INT_MAX . '/export', []);
}
public function testMetaActionThrowsNotFound()
public function testMetaActionThrowsNotFound(): void
{
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . PHP_INT_MAX . '/meta', []);
}
public function testMetaActionThrowsExceptionOnMissingName()
public function testMetaActionThrowsExceptionOnMissingName(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1183,7 +1282,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
]);
}
public function testMetaActionThrowsExceptionOnMissingValue()
public function testMetaActionThrowsExceptionOnMissingValue(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1195,7 +1294,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
]);
}
public function testMetaActionThrowsExceptionOnMissingMetafield()
public function testMetaActionThrowsExceptionOnMissingMetafield(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1207,7 +1306,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
]);
}
public function testMetaAction()
public function testMetaAction(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$timesheets = $this->importFixtureForUser(User::ROLE_USER);
@@ -1222,13 +1321,16 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]);
self::assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]);
$em = $this->getEntityManager();
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find($id);
$this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue());
self::assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue());
}
}

View File

@@ -179,9 +179,20 @@ class TimesheetTest extends TestCase
public function testClone(): void
{
$sut = new Timesheet();
self::assertNull($sut->getModifiedAt());
$sut->setExported(true);
$sut->setDescription('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
$modifiedDate = new \DateTime();
$reflection = new \ReflectionClass($sut);
$property = $reflection->getProperty('modifiedAt');
$property->setAccessible(true);
$property->setValue($sut, $modifiedDate);
$property->setAccessible(false);
self::assertEquals($modifiedDate, $sut->getModifiedAt());
$meta = new TimesheetMeta();
$meta->setName('blabla');
$meta->setValue('1234567890');
@@ -198,6 +209,9 @@ class TimesheetTest extends TestCase
$clone = clone $sut;
self::assertNotNull($sut->getModifiedAt());
self::assertNull($clone->getModifiedAt());
foreach ($sut->getMetaFields() as $metaField) {
$cloneMeta = $clone->getMetaField($metaField->getName());
self::assertEquals($cloneMeta->getValue(), $metaField->getValue());

View File

@@ -65,4 +65,9 @@ class DebugFormatter implements InvoiceFormatter
{
// does nothing
}
public function getFormattedAmount(float $amount): string
{
return (string) $amount;
}
}

View File

@@ -67,6 +67,7 @@ class InvoiceItemDefaultHydratorTest extends TestCase
'entry.user_id',
'entry.user_name',
'entry.user_alias',
'entry.user_display',
'entry.user_title',
'entry.activity',
'entry.activity_id',

View File

@@ -44,6 +44,7 @@ class InvoiceModelCustomerHydratorTest extends TestCase
'customer.contact',
'customer.company',
'customer.vat',
'customer.vat_id',
'customer.country',
'customer.number',
'customer.homepage',

View File

@@ -41,6 +41,7 @@ class InvoiceModelDefaultHydratorTest extends TestCase
'invoice.vat',
'invoice.language',
'invoice.tax',
'invoice.tax_hide',
'invoice.tax_nc',
'invoice.tax_plain',
'invoice.total_time',
@@ -76,6 +77,7 @@ class InvoiceModelDefaultHydratorTest extends TestCase
'query.end_month',
'query.end_month_number',
'query.end_year',
'user.see_others',
];
$givenKeys = array_keys($model);

View File

@@ -33,6 +33,7 @@ class InvoiceModelUserHydratorTest extends TestCase
protected function assertModelStructure(array $model)
{
$keys = [
'user.display',
'user.alias',
'user.email',
'user.name',

View File

@@ -84,6 +84,7 @@ class DebugRendererTest extends TestCase
'invoice.currency',
'invoice.currency_symbol',
'invoice.vat',
'invoice.tax_hide',
'invoice.tax',
'invoice.language',
'invoice.tax_nc',
@@ -127,6 +128,7 @@ class DebugRendererTest extends TestCase
'customer.contact',
'customer.company',
'customer.vat',
'customer.vat_id',
'customer.country',
'customer.number',
'customer.homepage',
@@ -150,14 +152,23 @@ class DebugRendererTest extends TestCase
'activity.time_budget_open',
'activity.time_budget_open_plain',
'user.alias',
'user.display',
'user.email',
'user.name',
'user.title',
'user.see_others',
'user.meta.hello',
'user.meta.kitty',
'testFromModelHydrator',
];
if ($activityCounter === 1) {
$keys = array_merge($keys, [
'query.activity.comment',
'query.activity.name',
]);
}
if ($activityCounter > 1) {
$keys = array_merge($keys, [
'activity.1.budget_open',
@@ -171,6 +182,14 @@ class DebugRendererTest extends TestCase
]);
}
if ($projectCounter === 1) {
$keys = array_merge($keys, [
'query.project.name',
'query.project.comment',
'query.project.order_number',
]);
}
if ($projectCounter > 0) {
$keys = array_merge($keys, [
'project.id',
@@ -254,6 +273,7 @@ class DebugRendererTest extends TestCase
'entry.weekyear',
'entry.user_id',
'entry.user_name',
'entry.user_display',
'entry.user_alias',
'entry.user_title',
'entry.activity',

View File

@@ -61,20 +61,27 @@ class TimesheetQueryTest extends BaseQueryTest
protected function assertUser(TimesheetQuery $sut): void
{
self::assertNull($sut->getUser());
self::assertFalse($sut->hasUsers());
$expected = new User();
$expected->setUserIdentifier('foo-bar');
$sut->setUser($expected);
self::assertEquals($expected, $sut->getUser());
self::assertTrue($sut->hasUsers());
$sut->setUser(null);
self::assertFalse($sut->hasUsers());
}
protected function assertUsers(TimesheetQuery $sut): void
{
self::assertEmpty($sut->getUsers());
self::assertFalse($sut->hasUsers());
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);
$sut->addUser($user);
self::assertTrue($sut->hasUsers());
$user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock();
$user->method('getId')->willReturn(1);

View File

@@ -28,30 +28,6 @@ class TagRepositoryTest extends AbstractRepositoryTest
$this->importFixture($data);
}
public function testFindIds()
{
$em = $this->getEntityManager();
/** @var TagRepository $repository */
$repository = $em->getRepository(Tag::class);
$result = $repository->findIdsByTagNameList('2018,Test');
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(7, \count($result));
}
public function testFindNoIds()
{
$em = $this->getEntityManager();
/** @var TagRepository $repository */
$repository = $em->getRepository(Tag::class);
$result = $repository->findIdsByTagNameList('Simply');
$this->assertIsArray($result);
$this->assertEmpty($result);
$this->assertEquals(0, \count($result));
}
public function testFindAllTagNames()
{
$em = $this->getEntityManager();

View File

@@ -1277,276 +1277,6 @@ parameters:
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testActiveAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForDifferentUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForExportedRecordIsAllowedForAdmin\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForExportedRecordIsNotAllowed\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionWithUnknownTimesheet\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionWithoutAuthorization\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDuplicateAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDuplicateThrowsNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportNotAllowedForUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportThrowsNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportedFilter\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollection\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForAllUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForEmptyResult\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForOtherUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionFull\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithQuery\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithQueryFailsWith404OnOutOfRangedPage\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithSingleParamsQuery\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithTags\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntity\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityAccessAllowedForAdmin\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityAccessDenied\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetRecentAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testInvalidPatchAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testIsSecure\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingMetafield\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingName\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingValue\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchActionWithInvalidUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchActionWithUnknownTimesheet\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionForDifferentUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithFullExpandedResponse\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithInvisibleActivity\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithInvisibleProject\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithNonBillableCustomer\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithNonBillableCustomerExplicit\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartActionWithBegin\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartActionWithCopyData\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartNotAllowedForUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartThrowsNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopAction\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopActionTriggersValidationOnLongRunning\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopNotAllowedForUser\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopThrowsNotFound\\(\\) has no return type specified\\.$#"
count: 1
path: API/TimesheetControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 32
path: API/TimesheetControllerTest.php
-
message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#"
count: 1
@@ -8692,16 +8422,6 @@ parameters:
count: 1
path: Repository/TagRepositoryTest.php
-
message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindIds\\(\\) has no return type specified\\.$#"
count: 1
path: Repository/TagRepositoryTest.php
-
message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindNoIds\\(\\) has no return type specified\\.$#"
count: 1
path: Repository/TagRepositoryTest.php
-
message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindNoTagNames\\(\\) has no return type specified\\.$#"
count: 1