Release 2.0.15 (#3970)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
98
composer.lock
generated
@@ -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",
|
||||
|
||||
40
phpstan.neon
40
phpstan.neon
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
public/build/invoice-pdf.2b749265.css
Normal file
1
public/build/invoice-pdf.2b749265.css
Normal 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}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() ?? '',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'] }} – {{ 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 }} – {{ 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>
|
||||
|
||||
@@ -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 }} – {{ model.template.address|nl2str(' – ') }}
|
||||
{% if model.template.vatId is not empty %}
|
||||
– {{ 'vat_id'|trans }}: {{ model.template.vatId }}
|
||||
{{ invoice['template.company'] }} – {{ invoice['template.address']|nl2str(' – ') }}
|
||||
{% if invoice['template.vat_id'] is not empty %}
|
||||
– {{ '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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -65,4 +65,9 @@ class DebugFormatter implements InvoiceFormatter
|
||||
{
|
||||
// does nothing
|
||||
}
|
||||
|
||||
public function getFormattedAmount(float $amount): string
|
||||
{
|
||||
return (string) $amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -44,6 +44,7 @@ class InvoiceModelCustomerHydratorTest extends TestCase
|
||||
'customer.contact',
|
||||
'customer.company',
|
||||
'customer.vat',
|
||||
'customer.vat_id',
|
||||
'customer.country',
|
||||
'customer.number',
|
||||
'customer.homepage',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,6 +33,7 @@ class InvoiceModelUserHydratorTest extends TestCase
|
||||
protected function assertModelStructure(array $model)
|
||||
{
|
||||
$keys = [
|
||||
'user.display',
|
||||
'user.alias',
|
||||
'user.email',
|
||||
'user.name',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user