Release 2.14 (#4710)

- show "link has expired message" in password reset screen
- added date objects as hydrator variables - for custom date formats in invoice templates
- show meta-fields with null values (e.g. booleans with `false` where hidden)
- fix permission check: allow to remove `view_own_timesheet` but still record times
- prevent error 500 if customer country is empty
- fix API 500 error if project does not exist when creating new timesheet
- fix tags are not created in remote-search mode
- do not check "export items" by default
- fix daterange query, if user an request locale are different
- added logging for invalid SAML responses (see various discussions)
This commit is contained in:
Kevin Papst
2024-04-05 12:38:21 +02:00
committed by GitHub
parent 19b2d47591
commit b6c98f871d
45 changed files with 318 additions and 317 deletions

159
composer.lock generated
View File

@@ -745,16 +745,16 @@
}, },
{ {
"name": "doctrine/doctrine-bundle", "name": "doctrine/doctrine-bundle",
"version": "2.11.3", "version": "2.12.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git", "url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68" "reference": "5418e811a14724068e95e0ba43353b903ada530f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/492725310ae9a1b5b20d6ae09fb5ae6404616e68", "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f",
"reference": "492725310ae9a1b5b20d6ae09fb5ae6404616e68", "reference": "5418e811a14724068e95e0ba43353b903ada530f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -792,6 +792,7 @@
"symfony/property-info": "^5.4 || ^6.0 || ^7.0", "symfony/property-info": "^5.4 || ^6.0 || ^7.0",
"symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0",
"symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/stopwatch": "^5.4 || ^6.0 || ^7.0",
"symfony/string": "^5.4 || ^6.0 || ^7.0", "symfony/string": "^5.4 || ^6.0 || ^7.0",
"symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0",
"symfony/validator": "^5.4 || ^6.0 || ^7.0", "symfony/validator": "^5.4 || ^6.0 || ^7.0",
@@ -809,7 +810,7 @@
"type": "symfony-bundle", "type": "symfony-bundle",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Doctrine\\Bundle\\DoctrineBundle\\": "" "Doctrine\\Bundle\\DoctrineBundle\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@@ -844,7 +845,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues", "issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.3" "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0"
}, },
"funding": [ "funding": [
{ {
@@ -860,7 +861,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-02-10T20:56:20+00:00" "time": "2024-03-19T07:20:37+00:00"
}, },
{ {
"name": "doctrine/doctrine-migrations-bundle", "name": "doctrine/doctrine-migrations-bundle",
@@ -1386,16 +1387,16 @@
}, },
{ {
"name": "doctrine/orm", "name": "doctrine/orm",
"version": "2.19.0", "version": "2.19.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/orm.git", "url": "https://github.com/doctrine/orm.git",
"reference": "a809a71aa6a233a6c82e68ebaaf8954adc4998dc" "reference": "40a0964f06e142e173b783c45688e5f5efaed6aa"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/orm/zipball/a809a71aa6a233a6c82e68ebaaf8954adc4998dc", "url": "https://api.github.com/repos/doctrine/orm/zipball/40a0964f06e142e173b783c45688e5f5efaed6aa",
"reference": "a809a71aa6a233a6c82e68ebaaf8954adc4998dc", "reference": "40a0964f06e142e173b783c45688e5f5efaed6aa",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1481,22 +1482,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/orm/issues", "issues": "https://github.com/doctrine/orm/issues",
"source": "https://github.com/doctrine/orm/tree/2.19.0" "source": "https://github.com/doctrine/orm/tree/2.19.2"
}, },
"time": "2024-03-03T17:43:41+00:00" "time": "2024-03-18T19:12:56+00:00"
}, },
{ {
"name": "doctrine/persistence", "name": "doctrine/persistence",
"version": "3.3.1", "version": "3.3.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/persistence.git", "url": "https://github.com/doctrine/persistence.git",
"reference": "b6fd1f126b13c1f7e7321f7338b14a19116b5de4" "reference": "477da35bd0255e032826f440b94b3e37f2d56f42"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/persistence/zipball/b6fd1f126b13c1f7e7321f7338b14a19116b5de4", "url": "https://api.github.com/repos/doctrine/persistence/zipball/477da35bd0255e032826f440b94b3e37f2d56f42",
"reference": "b6fd1f126b13c1f7e7321f7338b14a19116b5de4", "reference": "477da35bd0255e032826f440b94b3e37f2d56f42",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1565,7 +1566,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/persistence/issues", "issues": "https://github.com/doctrine/persistence/issues",
"source": "https://github.com/doctrine/persistence/tree/3.3.1" "source": "https://github.com/doctrine/persistence/tree/3.3.2"
}, },
"funding": [ "funding": [
{ {
@@ -1581,7 +1582,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-01T19:53:13+00:00" "time": "2024-03-12T14:54:36+00:00"
}, },
{ {
"name": "doctrine/sql-formatter", "name": "doctrine/sql-formatter",
@@ -2876,16 +2877,16 @@
}, },
{ {
"name": "mpdf/mpdf", "name": "mpdf/mpdf",
"version": "v8.2.2", "version": "v8.2.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/mpdf/mpdf.git", "url": "https://github.com/mpdf/mpdf.git",
"reference": "596a87b876d7793be7be060a8ac13424de120dd5" "reference": "6f723a96becf989a831e38caf758d28364a69939"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/596a87b876d7793be7be060a8ac13424de120dd5", "url": "https://api.github.com/repos/mpdf/mpdf/zipball/6f723a96becf989a831e38caf758d28364a69939",
"reference": "596a87b876d7793be7be060a8ac13424de120dd5", "reference": "6f723a96becf989a831e38caf758d28364a69939",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2953,7 +2954,7 @@
"type": "custom" "type": "custom"
} }
], ],
"time": "2023-11-07T13:52:14+00:00" "time": "2024-03-11T12:55:53+00:00"
}, },
{ {
"name": "mpdf/psr-http-message-shim", "name": "mpdf/psr-http-message-shim",
@@ -3108,22 +3109,23 @@
}, },
{ {
"name": "nelmio/api-doc-bundle", "name": "nelmio/api-doc-bundle",
"version": "v4.23.1", "version": "v4.24.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nelmio/NelmioApiDocBundle.git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git",
"reference": "a15b5923602c669007ea53a1a87991e9e147daab" "reference": "9fb5170b5f7a88d1aaade05e3eb399e697ac0941"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/a15b5923602c669007ea53a1a87991e9e147daab", "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/9fb5170b5f7a88d1aaade05e3eb399e697ac0941",
"reference": "a15b5923602c669007ea53a1a87991e9e147daab", "reference": "9fb5170b5f7a88d1aaade05e3eb399e697ac0941",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"php": ">=7.2", "php": ">=7.4",
"phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^4.3.4|^5.0",
"phpdocumentor/type-resolver": "^1.8.2",
"psr/cache": "^1.0|^2.0|^3.0", "psr/cache": "^1.0|^2.0|^3.0",
"psr/container": "^1.0|^2.0", "psr/container": "^1.0|^2.0",
"psr/log": "^1.0|^2.0|^3.0", "psr/log": "^1.0|^2.0|^3.0",
@@ -3135,9 +3137,9 @@
"symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0",
"symfony/options-resolver": "^5.4|^6.0|^7.0", "symfony/options-resolver": "^5.4|^6.0|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4.10|^6.0|^7.0",
"symfony/routing": "^5.4|^6.0|^7.0", "symfony/routing": "^5.4|^6.0|^7.0",
"zircote/swagger-php": "^4.2.15" "zircote/swagger-php": "^4.6.1"
}, },
"require-dev": { "require-dev": {
"api-platform/core": "^2.7.0|^3", "api-platform/core": "^2.7.0|^3",
@@ -3209,9 +3211,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues",
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.23.1" "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.24.0"
}, },
"time": "2024-03-07T09:21:17+00:00" "time": "2024-03-14T15:14:05+00:00"
}, },
{ {
"name": "nelmio/cors-bundle", "name": "nelmio/cors-bundle",
@@ -10237,16 +10239,16 @@
}, },
{ {
"name": "zircote/swagger-php", "name": "zircote/swagger-php",
"version": "4.8.4", "version": "4.8.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/zircote/swagger-php.git", "url": "https://github.com/zircote/swagger-php.git",
"reference": "bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c" "reference": "fe45a0e98a156999ddafc31c9ad333fdcadbcb2d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c", "url": "https://api.github.com/repos/zircote/swagger-php/zipball/fe45a0e98a156999ddafc31c9ad333fdcadbcb2d",
"reference": "bdee7f5a9216ce103ba2c953c1c43c4a3e139e4c", "reference": "fe45a0e98a156999ddafc31c9ad333fdcadbcb2d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10312,24 +10314,24 @@
], ],
"support": { "support": {
"issues": "https://github.com/zircote/swagger-php/issues", "issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/4.8.4" "source": "https://github.com/zircote/swagger-php/tree/4.8.6"
}, },
"time": "2024-02-04T21:16:47+00:00" "time": "2024-03-15T01:13:55+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.1.2", "version": "3.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10371,7 +10373,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.1.2" "source": "https://github.com/composer/pcre/tree/3.1.3"
}, },
"funding": [ "funding": [
{ {
@@ -10387,7 +10389,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-07T15:38:35+00:00" "time": "2024-03-19T10:26:25+00:00"
}, },
{ {
"name": "composer/xdebug-handler", "name": "composer/xdebug-handler",
@@ -10759,16 +10761,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.51.0", "version": "v3.52.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "127fa74f010da99053e3f5b62672615b72dd6efd" "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/127fa74f010da99053e3f5b62672615b72dd6efd", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc",
"reference": "127fa74f010da99053e3f5b62672615b72dd6efd", "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10839,7 +10841,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.51.0" "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1"
}, },
"funding": [ "funding": [
{ {
@@ -10847,7 +10849,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-02-28T19:50:06+00:00" "time": "2024-03-19T21:02:43+00:00"
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
@@ -11094,16 +11096,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.10.60", "version": "1.10.63",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe" "reference": "ad12836d9ca227301f5fb9960979574ed8628339"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339",
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe", "reference": "ad12836d9ca227301f5fb9960979574ed8628339",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11152,25 +11154,25 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-07T13:30:19+00:00" "time": "2024-03-18T16:53:53+00:00"
}, },
{ {
"name": "phpstan/phpstan-doctrine", "name": "phpstan/phpstan-doctrine",
"version": "1.3.62", "version": "1.3.63",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git", "url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "f3abbd8e93e12fed8091be3aeec216b06bed0950" "reference": "6ccde2b243e30ba47f7cd29f87fb752263bcb25c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f3abbd8e93e12fed8091be3aeec216b06bed0950", "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/6ccde2b243e30ba47f7cd29f87fb752263bcb25c",
"reference": "f3abbd8e93e12fed8091be3aeec216b06bed0950", "reference": "6ccde2b243e30ba47f7cd29f87fb752263bcb25c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2 || ^8.0", "php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10.48" "phpstan/phpstan": "^1.10.63"
}, },
"conflict": { "conflict": {
"doctrine/collections": "<1.0", "doctrine/collections": "<1.0",
@@ -11222,9 +11224,9 @@
"description": "Doctrine extensions for PHPStan", "description": "Doctrine extensions for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues", "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.62" "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.63"
}, },
"time": "2024-02-12T11:52:17+00:00" "time": "2024-03-18T18:05:58+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",
@@ -11329,22 +11331,22 @@
}, },
{ {
"name": "phpstan/phpstan-symfony", "name": "phpstan/phpstan-symfony",
"version": "1.3.8", "version": "1.3.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git", "url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a" "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/d8a0bc03a68d95288b6471c37d435647fbdaff1a", "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a32bc86da24495025d7aafd1ba62444d4a364a98",
"reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a", "reference": "a32bc86da24495025d7aafd1ba62444d4a364a98",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-simplexml": "*", "ext-simplexml": "*",
"php": "^7.2 || ^8.0", "php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10.36" "phpstan/phpstan": "^1.10.62"
}, },
"conflict": { "conflict": {
"symfony/framework-bundle": "<3.0" "symfony/framework-bundle": "<3.0"
@@ -11395,9 +11397,9 @@
"description": "Symfony Framework extensions and rules for PHPStan", "description": "Symfony Framework extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues", "issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.8" "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.9"
}, },
"time": "2024-03-05T16:33:08+00:00" "time": "2024-03-16T16:50:20+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@@ -12622,16 +12624,16 @@
}, },
{ {
"name": "sebastian/resource-operations", "name": "sebastian/resource-operations",
"version": "3.0.3", "version": "3.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git", "url": "https://github.com/sebastianbergmann/resource-operations.git",
"reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -12643,7 +12645,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.0-dev" "dev-main": "3.0-dev"
} }
}, },
"autoload": { "autoload": {
@@ -12664,8 +12666,7 @@
"description": "Provides a list of PHP built-in functions that operate on resources", "description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations", "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/resource-operations/issues", "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
"source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
}, },
"funding": [ "funding": [
{ {
@@ -12673,7 +12674,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2020-09-28T06:45:17+00:00" "time": "2024-03-14T16:00:52+00:00"
}, },
{ {
"name": "sebastian/type", "name": "sebastian/type",

View File

@@ -2202,21 +2202,6 @@ parameters:
count: 1 count: 1
path: src/Form/DataTransformer/StringToArrayTransformer.php path: src/Form/DataTransformer/StringToArrayTransformer.php
-
message: "#^Class App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
count: 1
path: src/Form/DataTransformer/TagArrayToStringTransformer.php
-
message: "#^Parameter \\#1 \\$stringOfTags \\(string\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer\\:\\:reverseTransform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:reverseTransform\\(\\)$#"
count: 1
path: src/Form/DataTransformer/TagArrayToStringTransformer.php
-
message: "#^Parameter \\#1 \\$tags \\(array\\<App\\\\Entity\\\\Tag\\>\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\TagArrayToStringTransformer\\:\\:transform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:transform\\(\\)$#"
count: 1
path: src/Form/DataTransformer/TagArrayToStringTransformer.php
- -
message: "#^Method App\\\\Form\\\\Extension\\\\DocumentationLinkExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Form\\\\Extension\\\\DocumentationLinkExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1 count: 1

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.13.0'; public const VERSION = '2.14.0';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 21300; public const VERSION_ID = 21400;
/** /**
* The software name * The software name
*/ */

View File

@@ -255,6 +255,8 @@ abstract class AbstractController extends BaseAbstractController implements Serv
} }
/** /**
* Use "performSearch=1" to skip loading session searches.
*
* @param array<string> $filterParams parameter names, which should not be saved (neither session, nor database) * @param array<string> $filterParams parameter names, which should not be saved (neither session, nor database)
* @throws \Exception * @throws \Exception
*/ */
@@ -298,7 +300,6 @@ abstract class AbstractController extends BaseAbstractController implements Serv
} }
$searchName = $this->getSearchName($data); $searchName = $this->getSearchName($data);
/** @var BookmarkRepository $bookmarkRepo */
$bookmarkRepo = $this->getBookmark(); $bookmarkRepo = $this->getBookmark();
$bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $searchName); $bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $searchName);

View File

@@ -325,7 +325,7 @@ final class CustomerController extends AbstractController
$rates = $rateRepository->getRatesForCustomer($customer); $rates = $rateRepository->getRatesForCustomer($customer);
} }
if (null !== $customer->getTimezone()) { if ($customer->getTimezone() !== null && $customer->getTimezone() !== '') {
$timezone = new \DateTimeZone($customer->getTimezone()); $timezone = new \DateTimeZone($customer->getTimezone());
} }

View File

@@ -132,6 +132,8 @@ final class PasswordResetController extends AbstractController
} }
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) { if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) {
$this->flashWarning('This link has already expired');
return $this->redirectToRoute('resetting_request'); return $this->redirectToRoute('resetting_request');
} }

View File

@@ -19,8 +19,15 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* No permission check on controller level, only for single routes.
*
* There was "view_own_timesheet" here once, but it is a bug, as some companies (rarely, but existing) want their
* employees to enter time, but not to see it afterward.
*
* It is legit to only own "create_own_timesheet" without "view_own_timesheet".
*/
#[Route(path: '/timesheet')] #[Route(path: '/timesheet')]
#[IsGranted('view_own_timesheet')]
final class TimesheetController extends TimesheetAbstractController final class TimesheetController extends TimesheetAbstractController
{ {
#[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])] #[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])]

View File

@@ -27,8 +27,15 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* No permission check on controller level, only for single routes.
*
* There was "view_other_timesheet" here once, but it is a bug.
* Some companies (rarely, but existing) want their employees to enter time, but not to see it afterward.
*
* It is legit to only own "create_other_timesheet" without "view_other_timesheet".
*/
#[Route(path: '/team/timesheet')] #[Route(path: '/team/timesheet')]
#[IsGranted('view_other_timesheet')]
final class TimesheetTeamController extends TimesheetAbstractController final class TimesheetTeamController extends TimesheetAbstractController
{ {
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])] #[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])]

View File

@@ -14,32 +14,30 @@ use App\Repository\TagRepository;
use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<array<Tag>, string>
*/
final class TagArrayToStringTransformer implements DataTransformerInterface final class TagArrayToStringTransformer implements DataTransformerInterface
{ {
private bool $create = true; public function __construct(
private readonly TagRepository $tagRepository,
public function __construct(private TagRepository $tagRepository) private readonly bool $create
)
{ {
} }
public function setCreate(bool $create): void
{
$this->create = $create;
}
/** /**
* Transforms an array of tags to a string. * Transforms an array of tags to a string.
* *
* @param Tag[]|null $tags * @param Tag[]|null $value
* @return string
*/ */
public function transform(mixed $tags): mixed public function transform(mixed $value): string
{ {
if (empty($tags)) { if (empty($value)) {
return ''; return '';
} }
return implode(', ', $tags); return implode(', ', $value);
} }
/** /**
@@ -47,18 +45,17 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
* *
* @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform() * @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
* *
* @param string|null $stringOfTags * @param string|null $value
* @return Tag[] * @return Tag[]
* @throws TransformationFailedException * @throws TransformationFailedException
*/ */
public function reverseTransform(mixed $stringOfTags): mixed public function reverseTransform(mixed $value): mixed
{ {
// check for empty tag list // check for empty tag list
if ('' === $stringOfTags || null === $stringOfTags) { if ('' === $value || null === $value) {
return []; return [];
} }
$names = array_filter(array_unique(array_map('trim', explode(',', $value))));
$names = array_filter(array_unique(array_map('trim', explode(',', $stringOfTags))));
// get the current tags and find the new ones that should be created // get the current tags and find the new ones that should be created
$tags = $this->tagRepository->findBy(['name' => $names]); $tags = $this->tagRepository->findBy(['name' => $names]);
@@ -69,9 +66,9 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
foreach ($newNames as $name) { foreach ($newNames as $name) {
$tag = new Tag(); $tag = new Tag();
$tag->setName(mb_substr($name, 0, 100)); $tag->setName(mb_substr($name, 0, 100));
$tags[] = $tag; $this->tagRepository->saveTag($tag);
// new tags persist automatically thanks to the cascade={"persist"} $tags[] = $tag;
} }
} }

View File

@@ -73,14 +73,13 @@ trait FormTrait
if ($isNew && \is_int($project)) { if ($isNew && \is_int($project)) {
/** @var Project $project */ /** @var Project $project */
$project = $repo->find($project); $project = $repo->find($project);
if ($project === null) { if ($project !== null) {
throw new \Exception('Unknown project'); if (!$project->getCustomer()->isVisible()) {
} $customer = null;
if (!$project->getCustomer()->isVisible()) { $project = null;
$customer = null; } elseif (!$project->isVisible()) {
$project = null; $project = null;
} elseif (!$project->isVisible()) { }
$project = null;
} }
} }

View File

@@ -42,7 +42,7 @@ final class CustomerToolbarForm extends AbstractType
$countries = $qb->getQuery()->getSingleColumnResult(); $countries = $qb->getQuery()->getSingleColumnResult();
$choices = []; $choices = [];
foreach ($countries as $country) { foreach ($countries as $country) {
if (\is_string($country) && \is_string($options['locale'])) { if (\is_string($country) && $country !== '' && \is_string($options['locale'])) {
$choices[$country] = Countries::getName($country, $options['locale']); $choices[$country] = Countries::getName($country, $options['locale']);
} }
} }

View File

@@ -65,7 +65,6 @@ final class DateRangeType extends AbstractType
return ['pattern' => $pattern . self::DATE_SPACER . $pattern]; return ['pattern' => $pattern . self::DATE_SPACER . $pattern];
}); });
} }
public function buildView(FormView $view, FormInterface $form, array $options): void public function buildView(FormView $view, FormInterface $form, array $options): void

View File

@@ -10,6 +10,7 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\Form\DataTransformer\TagArrayToStringTransformer; use App\Form\DataTransformer\TagArrayToStringTransformer;
use App\Repository\TagRepository;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -24,18 +25,17 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
*/ */
final class TagsInputType extends AbstractType final class TagsInputType extends AbstractType
{ {
public function __construct(private TagArrayToStringTransformer $transformer, private UrlGeneratorInterface $router) public function __construct(
private readonly TagRepository $tagRepository,
private readonly UrlGeneratorInterface $router
)
{ {
} }
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
if ($options['allow_create'] === false) {
$this->transformer->setCreate(false);
}
$builder->addModelTransformer(new CollectionToArrayTransformer(), true); $builder->addModelTransformer(new CollectionToArrayTransformer(), true);
$builder->addModelTransformer($this->transformer, true); $builder->addModelTransformer(new TagArrayToStringTransformer($this->tagRepository, (bool) $options['allow_create']), true);
} }
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void
@@ -48,6 +48,7 @@ final class TagsInputType extends AbstractType
'allow_create' => false, 'allow_create' => false,
'label' => 'tag', 'label' => 'tag',
]); ]);
$resolver->setAllowedTypes('allow_create', 'bool');
} }
public function buildView(FormView $view, FormInterface $form, array $options): void public function buildView(FormView $view, FormInterface $form, array $options): void

View File

@@ -28,78 +28,47 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/ */
final class TagsSelectType extends AbstractType final class TagsSelectType extends AbstractType
{ {
public function __construct(private TagRepository $tagRepository) public function __construct(
private readonly TagRepository $tagRepository
)
{ {
} }
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { if (!$options['allow_create']) {
if (!$options['allow_create']) { return;
return; }
}
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
/** @var array<string> $tagIds */
$tagIds = $event->getData(); $tagIds = $event->getData();
if (!\is_array($tagIds)) { if (!\is_array($tagIds)) {
return; return;
} }
$ids = array_filter($tagIds, function ($tagId) {
$tags = [];
foreach ($tagIds as $tagId) {
$tag = null;
if (is_numeric($tagId)) { if (is_numeric($tagId)) {
return true; $tag = $this->tagRepository->find($tagId);
} }
return false; if ($tag === null) {
}); $tag = $this->tagRepository->findTagByName($tagId);
// get the current tags and find the new ones that should be created
$tags = $this->tagRepository->findBy(['id' => $ids]);
$foundIds = [];
foreach ($tags as $tag) {
$foundIds[] = (string) $tag->getId();
}
$newData = [];
/** @var array<string> $newNames */
$newNames = [];
foreach ($tagIds as $tag) {
if (!\in_array($tag, $foundIds, true)) {
$newNames[] = $tag;
} else {
$newData[] = $tag;
}
}
// 1. in case someone is using tags like "1234" this can interfere with the ID
// 2. if we would load only visible tags, we would try to create new ones below
// and that would trigger the unique constraint
$tags = $this->tagRepository->findTagsByName($newNames, null);
$foundTagNames = [];
foreach ($tags as $tag) {
$newData[] = (string) $tag->getId();
$foundTagNames[] = $tag->getName();
}
/** @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)) { if ($tag === null) {
return 0; $tag = new Tag();
$tag->setName(mb_substr($tagId, 0, 100));
$this->tagRepository->saveTag($tag);
} }
return strcmp($userTag, $existingTag); $tags[] = $tag->getId();
});
foreach ($newNamesCreate as $name) {
$tag = new Tag();
$tag->setName(mb_substr($name, 0, 100));
$this->tagRepository->saveTag($tag);
$newData[] = $tag->getId();
} }
$event->setData($newData); $event->setData($tags);
}, 1000); }, 1000);
} }
@@ -132,6 +101,8 @@ final class TagsSelectType extends AbstractType
return $repo->getQueryBuilderForFormType($query); return $repo->getQueryBuilderForFormType($query);
}; };
}); });
$resolver->setAllowedTypes('allow_create', 'bool');
} }
public function buildView(FormView $view, FormInterface $form, array $options): void public function buildView(FormView $view, FormInterface $form, array $options): void

View File

@@ -14,6 +14,8 @@ use App\Invoice\InvoiceModelHydrator;
final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
{ {
private const DATE_PROCESS_FORMAT = 'Y-m-d h:i:s';
public function hydrate(InvoiceModel $model): array public function hydrate(InvoiceModel $model): array
{ {
$currency = $model->getCurrency(); $currency = $model->getCurrency();
@@ -24,7 +26,9 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
$values = [ $values = [
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()), 'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
'invoice.due_date_process' => $model->getDueDate()->format(self::DATE_PROCESS_FORMAT), // since 2.14
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()), 'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
'invoice.date_process' => $model->getInvoiceDate()->format(self::DATE_PROCESS_FORMAT), // since 2.14
'invoice.number' => $model->getInvoiceNumber(), 'invoice.number' => $model->getInvoiceNumber(),
'invoice.currency' => $currency, 'invoice.currency' => $currency,
'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9 'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9
@@ -55,11 +59,13 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'query.begin' => '', 'query.begin' => '',
'query.begin_day' => '', 'query.begin_day' => '',
'query.begin_process' => null, // since 2.14
'query.begin_month' => '', 'query.begin_month' => '',
'query.begin_month_number' => '', 'query.begin_month_number' => '',
'query.begin_year' => '', 'query.begin_year' => '',
'query.end' => '', // since 1.9 'query.end' => '', // since 1.9
'query.end_day' => '', // since 1.9 'query.end_day' => '', // since 1.9
'query.end_process' => null, // since 2.14
'query.end_month' => '', // since 1.9 'query.end_month' => '', // since 1.9
'query.end_month_number' => '', // since 1.9 'query.end_month_number' => '', // since 1.9
'query.end_year' => '', // since 1.9 'query.end_year' => '', // since 1.9
@@ -82,6 +88,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'query.year' => $begin->format('Y'), 'query.year' => $begin->format('Y'),
// @deprecated - but impossible to delete // @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin), 'query.begin' => $formatter->getFormattedDateTime($begin),
'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14
'query.begin_day' => $begin->format('d'), 'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin), 'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'), 'query.begin_month_number' => $begin->format('m'),
@@ -93,13 +100,10 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
if ($end !== null) { if ($end !== null) {
$values = array_merge($values, [ $values = array_merge($values, [
'query.end' => $formatter->getFormattedDateTime($end), 'query.end' => $formatter->getFormattedDateTime($end),
// since 1.9 'query.end_process' => $end->format(self::DATE_PROCESS_FORMAT), // since 2.14
'query.end_day' => $end->format('d'), 'query.end_day' => $end->format('d'),
// since 1.9
'query.end_month' => $formatter->getFormattedMonthName($end), 'query.end_month' => $formatter->getFormattedMonthName($end),
// since 1.9
'query.end_month_number' => $end->format('m'), 'query.end_month_number' => $end->format('m'),
// since 1.9
'query.end_year' => $end->format('Y'), 'query.end_year' => $end->format('Y'),
]); ]);
} }
@@ -145,7 +149,9 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
if ($min !== null && $max !== null) { if ($min !== null && $max !== null) {
$values = array_merge($values, [ $values = array_merge($values, [
'invoice.first' => $formatter->getFormattedDateTime($min->getBegin()), 'invoice.first' => $formatter->getFormattedDateTime($min->getBegin()),
'invoice.first_process' => $min->getBegin()?->format(self::DATE_PROCESS_FORMAT), // since 2.14
'invoice.last' => $formatter->getFormattedDateTime($max->getEnd()), 'invoice.last' => $formatter->getFormattedDateTime($max->getEnd()),
'invoice.last_process' => $max->getEnd()?->format(self::DATE_PROCESS_FORMAT), // since 2.14
]); ]);
} }

View File

@@ -12,6 +12,7 @@ namespace App\Saml;
use App\Configuration\SamlConfigurationInterface; use App\Configuration\SamlConfigurationInterface;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface;
@@ -24,7 +25,8 @@ final class SamlProvider
public function __construct( public function __construct(
private readonly UserRepository $repository, private readonly UserRepository $repository,
private readonly UserProviderInterface $userProvider, private readonly UserProviderInterface $userProvider,
private readonly SamlConfigurationInterface $configuration private readonly SamlConfigurationInterface $configuration,
private readonly LoggerInterface $logger
) { ) {
} }
@@ -37,7 +39,8 @@ final class SamlProvider
/** @var User $user */ /** @var User $user */
$user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier()); $user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier());
} }
} catch (UserNotFoundException $e) { } catch (UserNotFoundException $ex) {
$this->logger->error($ex->getMessage());
} }
try { try {
@@ -49,6 +52,7 @@ final class SamlProvider
$this->repository->saveUser($user); $this->repository->saveUser($user);
} catch (\Exception $ex) { } catch (\Exception $ex) {
$this->logger->error($ex->getMessage());
throw new AuthenticationException( throw new AuthenticationException(
sprintf('Failed creating or hydrating user "%s": %s', $token->getUserIdentifier(), $ex->getMessage()) sprintf('Failed creating or hydrating user "%s": %s', $token->getUserIdentifier(), $ex->getMessage())
); );
@@ -110,7 +114,8 @@ final class SamlProvider
if (method_exists($user, $setter)) { if (method_exists($user, $setter)) {
$user->$setter($value); $user->$setter($value);
} else { } else {
throw new \RuntimeException('Invalid mapping field given: ' . $field); // this should never happen, because it is validated when the container is built
throw new \RuntimeException('Invalid SAML mapping field: ' . $field);
} }
} }
@@ -136,7 +141,7 @@ final class SamlProvider
if ($part[0] === '$') { if ($part[0] === '$') {
$key = substr($part, 1); $key = substr($part, 1);
if (!\array_key_exists($key, $attributes)) { if (!\array_key_exists($key, $attributes)) {
throw new \RuntimeException('Missing user attribute: ' . $key); throw new \RuntimeException('Missing SAML attribute in response: ' . $key);
} }
if (\is_array($attributes[$key]) && isset($attributes[$key][0])) { if (\is_array($attributes[$key]) && isset($attributes[$key][0])) {

View File

@@ -69,7 +69,7 @@ final class ProjectVoter extends Voter
} }
// those cannot be assigned to teams // those cannot be assigned to teams
if (\in_array($attribute, ['create', 'delete'])) { if (\in_array($attribute, ['create', 'delete'], true)) {
return false; return false;
} }

View File

@@ -290,7 +290,7 @@
{% macro meta_field_value(entity, field) %} {% macro meta_field_value(entity, field) %}
{% set metaField = entity.metaField(field.name) %} {% set metaField = entity.metaField(field.name) %}
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %} {% if not metaField is null %}
{% set metaField = metaField.merge(field) %} {% set metaField = metaField.merge(field) %}
{{ _self.form_type_value(metaField.type, metaField.value, entity) }} {{ _self.form_type_value(metaField.type, metaField.value, entity) }}
{% endif %} {% endif %}

View File

@@ -8,7 +8,6 @@
<fieldset class="form-fieldset form-fieldset-light"> <fieldset class="form-fieldset form-fieldset-light">
<legend>{{ 'work_times_should'|trans }}</legend> <legend>{{ 'work_times_should'|trans }}</legend>
<div class="form-text mb-3 help-text">{{ 'work_times_should.help'|trans }}</div>
{{ form_row(form.workHoursMonday) }} {{ form_row(form.workHoursMonday) }}
{{ form_row(form.workHoursTuesday) }} {{ form_row(form.workHoursTuesday) }}
{{ form_row(form.workHoursWednesday) }} {{ form_row(form.workHoursWednesday) }}

View File

@@ -465,7 +465,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'hourlyRate' => 127, 'hourlyRate' => 127,
'billable' => false 'billable' => false
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -494,7 +496,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'hourlyRate' => 127, 'hourlyRate' => 127,
'billable' => true 'billable' => true
]; ];
$this->request($client, '/api/timesheets?full=true', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets?full=true', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -526,7 +530,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'), 'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo', 'description' => 'foo',
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -571,7 +577,60 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => (new \DateTime())->format('Y-m-d H:m:s'), 'end' => (new \DateTime())->format('Y-m-d H:m:s'),
'description' => 'foo', 'description' => 'foo',
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertApiCallValidationError($client->getResponse(), ['project']);
}
public function testPostActionWithUnknownActivity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
$customer = new Customer('foo-bar-1');
$customer->setVisible(false);
$customer->setCountry('DE');
$customer->setTimezone('Europe/Berlin');
$em->persist($customer);
$project = new Project();
$project->setName('foo-bar-2');
$project->setVisible(true);
$project->setCustomer($customer);
$em->persist($project);
$data = [
'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m:s'),
'end' => (new \DateTime())->format('Y-m-d H:m:s'),
'project' => $project->getId(),
'activity' => 99,
];
$json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertApiCallValidationError($client->getResponse(), ['project']);
}
public function testPostActionWithNonExistingProject(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
$activity = new Activity();
$activity->setName('foo-bar-3');
$activity->setVisible(true);
$em->persist($activity);
$em->flush();
$data = [
'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m:s'),
'end' => (new \DateTime())->format('Y-m-d H:m:s'),
'project' => 99,
'activity' => $activity->getId(),
];
$json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertApiCallValidationError($client->getResponse(), ['project']); $this->assertApiCallValidationError($client->getResponse(), ['project']);
} }
@@ -605,7 +664,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => (new \DateTime())->format('Y-m-d H:m'), 'end' => (new \DateTime())->format('Y-m-d H:m'),
'description' => 'foo', 'description' => 'foo',
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertApiCallValidationError($client->getResponse(), ['activity']); $this->assertApiCallValidationError($client->getResponse(), ['activity']);
} }
@@ -635,7 +696,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => (new \DateTime())->format('Y-m-d H:m'), 'end' => (new \DateTime())->format('Y-m-d H:m'),
'description' => 'foo', 'description' => 'foo',
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -674,7 +737,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'description' => 'foo', 'description' => 'foo',
'billable' => true, 'billable' => true,
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -700,7 +765,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'description' => 'foo', 'description' => 'foo',
'billable' => false, 'billable' => false,
]; ];
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -739,7 +806,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'description' => 'foo', 'description' => 'foo',
'exported' => true, 'exported' => true,
]; ];
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json);
$response = $client->getResponse(); $response = $client->getResponse();
$this->assertApiResponseAccessDenied($response); $this->assertApiResponseAccessDenied($response);
} }
@@ -761,7 +830,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'end' => (new \DateTime('- 1 hours'))->format('Y-m-d H:m'), 'end' => (new \DateTime('- 1 hours'))->format('Y-m-d H:m'),
'description' => 'foo', 'description' => 'foo',
]; ];
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], $json);
$response = $client->getResponse(); $response = $client->getResponse();
self::assertEquals(400, $response->getStatusCode()); self::assertEquals(400, $response->getStatusCode());
@@ -1066,7 +1137,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'description' => 'foo', 'description' => 'foo',
'tags' => ['another', 'testing', 'bar'] 'tags' => ['another', 'testing', 'bar']
]; ];
$this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $id, 'PATCH', [], $json);
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH'); $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
@@ -1100,7 +1173,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'description' => 'foo', 'description' => 'foo',
'tags' => ['another', 'testing', 'bar'] 'tags' => ['another', 'testing', 'bar']
]; ];
$this->request($client, '/api/timesheets/' . $id, 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $id, 'PATCH', [], $json);
$begin = new \DateTime('2019-11-27 13:55:00'); $begin = new \DateTime('2019-11-27 13:55:00');
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]); $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]);
@@ -1211,7 +1286,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'fixedRate' => 2016, 'fixedRate' => 2016,
'hourlyRate' => 127 'hourlyRate' => 127
]; ];
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets', 'POST', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
@@ -1343,7 +1420,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
'name' => 'metatestmock', 'name' => 'metatestmock',
'value' => 'another,testing,bar' 'value' => 'another,testing,bar'
]; ];
$this->request($client, '/api/timesheets/' . $id . '/meta', 'PATCH', [], json_encode($data)); $json = json_encode($data);
self::assertIsString($json);
$this->request($client, '/api/timesheets/' . $id . '/meta', 'PATCH', [], $json);
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());

View File

@@ -28,7 +28,7 @@ class TagArrayToStringTransformerTest extends TestCase
$repository = $this->getMockBuilder(TagRepository::class)->disableOriginalConstructor()->getMock(); $repository = $this->getMockBuilder(TagRepository::class)->disableOriginalConstructor()->getMock();
$sut = new TagArrayToStringTransformer($repository); $sut = new TagArrayToStringTransformer($repository, true);
$this->assertEquals('', $sut->transform([])); $this->assertEquals('', $sut->transform([]));
$this->assertEquals('', $sut->transform(null)); $this->assertEquals('', $sut->transform(null));
@@ -45,10 +45,12 @@ class TagArrayToStringTransformerTest extends TestCase
(new Tag())->setName('bar'), (new Tag())->setName('bar'),
]; ];
$repository = $this->getMockBuilder(TagRepository::class)->onlyMethods(['findBy'])->disableOriginalConstructor()->getMock(); $repository = $this->getMockBuilder(TagRepository::class)
->onlyMethods(['findBy', 'saveTag'])
->disableOriginalConstructor()->getMock();
$repository->expects($this->once())->method('findBy')->willReturn($results); $repository->expects($this->once())->method('findBy')->willReturn($results);
$sut = new TagArrayToStringTransformer($repository); $sut = new TagArrayToStringTransformer($repository, true);
$this->assertEquals([], $sut->reverseTransform('')); $this->assertEquals([], $sut->reverseTransform(''));
$this->assertEquals([], $sut->reverseTransform(null)); $this->assertEquals([], $sut->reverseTransform(null));

View File

@@ -34,7 +34,9 @@ class InvoiceModelDefaultHydratorTest extends TestCase
{ {
$keys = [ $keys = [
'invoice.due_date', 'invoice.due_date',
'invoice.due_date_process',
'invoice.date', 'invoice.date',
'invoice.date_process',
'invoice.number', 'invoice.number',
'invoice.currency', 'invoice.currency',
'invoice.currency_symbol', 'invoice.currency_symbol',
@@ -47,7 +49,9 @@ class InvoiceModelDefaultHydratorTest extends TestCase
'invoice.total_time', 'invoice.total_time',
'invoice.duration_decimal', 'invoice.duration_decimal',
'invoice.first', 'invoice.first',
'invoice.first_process',
'invoice.last', 'invoice.last',
'invoice.last_process',
'invoice.total', 'invoice.total',
'invoice.total_nc', 'invoice.total_nc',
'invoice.total_plain', 'invoice.total_plain',
@@ -68,11 +72,13 @@ class InvoiceModelDefaultHydratorTest extends TestCase
'query.month_number', 'query.month_number',
'query.year', 'query.year',
'query.begin', 'query.begin',
'query.begin_process',
'query.begin_day', 'query.begin_day',
'query.begin_month', 'query.begin_month',
'query.begin_month_number', 'query.begin_month_number',
'query.begin_year', 'query.begin_year',
'query.end', 'query.end',
'query.end_process',
'query.end_day', 'query.end_day',
'query.end_month', 'query.end_month',
'query.end_month_number', 'query.end_month_number',

View File

@@ -79,7 +79,9 @@ class DebugRendererTest extends TestCase
{ {
$keys = [ $keys = [
'invoice.due_date', 'invoice.due_date',
'invoice.due_date_process',
'invoice.date', 'invoice.date',
'invoice.date_process',
'invoice.number', 'invoice.number',
'invoice.currency', 'invoice.currency',
'invoice.currency_symbol', 'invoice.currency_symbol',
@@ -92,7 +94,9 @@ class DebugRendererTest extends TestCase
'invoice.total_time', 'invoice.total_time',
'invoice.duration_decimal', 'invoice.duration_decimal',
'invoice.first', 'invoice.first',
'invoice.first_process',
'invoice.last', 'invoice.last',
'invoice.last_process',
'invoice.total', 'invoice.total',
'invoice.total_nc', 'invoice.total_nc',
'invoice.total_plain', 'invoice.total_plain',
@@ -113,11 +117,13 @@ class DebugRendererTest extends TestCase
'query.month_number', 'query.month_number',
'query.year', 'query.year',
'query.begin', 'query.begin',
'query.begin_process',
'query.begin_day', 'query.begin_day',
'query.begin_month', 'query.begin_month',
'query.begin_month_number', 'query.begin_month_number',
'query.begin_year', 'query.begin_year',
'query.end', 'query.end',
'query.end_process',
'query.end_day', 'query.end_day',
'query.end_month', 'query.end_month',
'query.end_month_number', 'query.end_month_number',

View File

@@ -38,10 +38,10 @@ class ExportQueryTest extends TimesheetQueryTest
public function assertMarkAsExported(ExportQuery $sut): void public function assertMarkAsExported(ExportQuery $sut): void
{ {
$this->assertTrue($sut->isMarkAsExported());
$sut->setMarkAsExported(false);
$this->assertFalse($sut->isMarkAsExported()); $this->assertFalse($sut->isMarkAsExported());
$sut->setMarkAsExported(true);
$this->assertTrue($sut->isMarkAsExported());
} }
public function assertRenderer(ExportQuery $sut): void public function assertRenderer(ExportQuery $sut): void

View File

@@ -17,6 +17,7 @@ use App\Saml\SamlProvider;
use App\Tests\Configuration\TestConfigLoader; use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\SystemConfigurationFactory; use App\Tests\Mocks\SystemConfigurationFactory;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface;
@@ -57,7 +58,7 @@ class SamlProviderTest extends TestCase
$userProvider->method('loadUserByIdentifier')->willReturn(new User()); $userProvider->method('loadUserByIdentifier')->willReturn(new User());
} }
$provider = new SamlProvider($repository, $userProvider, $samlConfig); $provider = new SamlProvider($repository, $userProvider, $samlConfig, $this->createMock(LoggerInterface::class));
return $provider; return $provider;
} }
@@ -107,7 +108,7 @@ class SamlProviderTest extends TestCase
public function testAuthenticateThrowsAuthenticationException(): void public function testAuthenticateThrowsAuthenticationException(): void
{ {
$this->expectException(AuthenticationException::class); $this->expectException(AuthenticationException::class);
$this->expectExceptionMessage('Failed creating or hydrating user "foo1@example.com": Missing user attribute: Email'); $this->expectExceptionMessage('Failed creating or hydrating user "foo1@example.com": Missing SAML attribute in response: Email');
$user = new User(); $user = new User();
$user->setAuth(User::AUTH_SAML); $user->setAuth(User::AUTH_SAML);

View File

@@ -502,11 +502,6 @@ parameters:
count: 6 count: 6
path: API/TimesheetControllerTest.php path: API/TimesheetControllerTest.php
-
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 14
path: API/TimesheetControllerTest.php
- -
message: "#^Cannot access offset 'enabled' on mixed\\.$#" message: "#^Cannot access offset 'enabled' on mixed\\.$#"
count: 1 count: 1

View File

@@ -1466,7 +1466,7 @@
<target state="translated">مرَض</target> <target state="translated">مرَض</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">الساعات المستهدَفة</target> <target state="translated">الساعات المستهدَفة</target>
</trans-unit> </trans-unit>
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note"> <trans-unit id="AXr_aHI" resname="completed_month_pdf_note">

View File

@@ -1531,10 +1531,6 @@
<source>work_times_result</source> <source>work_times_result</source>
<target state="translated">Odpracované hodiny</target> <target state="translated">Odpracované hodiny</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Zde nakonfigurované hodnoty se používají k výpočtu hodinového účtu.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>
<target state="translated">Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny.</target> <target state="translated">Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny.</target>
@@ -1548,7 +1544,7 @@
<target state="translated">Je-hodin</target> <target state="translated">Je-hodin</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Cílové hodiny</target> <target state="translated">Cílové hodiny</target>
</trans-unit> </trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at"> <trans-unit id="8yqaKzX" resname="confirmed_by_at">

View File

@@ -647,12 +647,8 @@
<target>Arbeitszeitkonto</target> <target>Arbeitszeitkonto</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target>Soll-Stunden</target> <target>Erwartete Stundenzahl</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target>Die hier konfigurierten Werte werden zur Berechnung des Stundenkontos verwendet.</target>
</trans-unit> </trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>

View File

@@ -647,12 +647,8 @@
<target state="translated">Account working times</target> <target state="translated">Account working times</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target>Target-hours</target> <target>Expected number of hours</target>
</trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target>The values configured here are used to calculate the hourly account.</target>
</trans-unit> </trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>

View File

@@ -1496,13 +1496,9 @@
<target state="translated">Evaluación</target> <target state="translated">Evaluación</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Horas de objetivo</target> <target state="translated">Horas de objetivo</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Los valores configurados aquí se utilizan para calcular las horas.</target>
</trans-unit>
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note"> <trans-unit id="AXr_aHI" resname="completed_month_pdf_note">
<source>completed_month_pdf_note</source> <source>completed_month_pdf_note</source>
<target state="translated">Este documento sirve como prueba de las horas trabajadas.</target> <target state="translated">Este documento sirve como prueba de las horas trabajadas.</target>

View File

@@ -1504,7 +1504,7 @@
<target state="translated">Évaluation</target> <target state="translated">Évaluation</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Heures cibles</target> <target state="translated">Heures cibles</target>
</trans-unit> </trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
@@ -1535,10 +1535,6 @@
<source>Working times</source> <source>Working times</source>
<target state="translated">Heures de travail</target> <target state="translated">Heures de travail</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Les valeurs configurées ici sont utilisées pour calculer le compte horaire.</target>
</trans-unit>
<trans-unit id="YPO294a" resname="absence_other.intro" xml:space="preserve"> <trans-unit id="YPO294a" resname="absence_other.intro" xml:space="preserve">
<source>absence_other.intro</source> <source>absence_other.intro</source>
<target state="translated">Evènements familiaux, déménagement, catastrophes climatiques, etc.</target> <target state="translated">Evènements familiaux, déménagement, catastrophes climatiques, etc.</target>

View File

@@ -1446,10 +1446,6 @@
<source>Account working times</source> <source>Account working times</source>
<target state="translated">זמני העבודה בחשבון</target> <target state="translated">זמני העבודה בחשבון</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">הערכים שהוגדרו כאן משמשים לחישוב החשבון השעתי.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is"> <trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source> <source>work_times_is</source>
<target state="translated">שעות בפועל</target> <target state="translated">שעות בפועל</target>
@@ -1487,7 +1483,7 @@
<target state="translated">סקירה</target> <target state="translated">סקירה</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">שעות יעד</target> <target state="translated">שעות יעד</target>
</trans-unit> </trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">

View File

@@ -1500,13 +1500,9 @@
<target state="translated">Ovaj dokument služi kao dokaz o odrađenim satima.</target> <target state="translated">Ovaj dokument služi kao dokaz o odrađenim satima.</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Ciljani sati</target> <target state="translated">Ciljani sati</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Ovdje konfigurirane vrijednosti se koriste za računanje računa po satu.</target>
</trans-unit>
<trans-unit id="wZRRqg0" resname="manual_bookings"> <trans-unit id="wZRRqg0" resname="manual_bookings">
<source>manual_bookings</source> <source>manual_bookings</source>
<target state="translated">Ručne rezervacije</target> <target state="translated">Ručne rezervacije</target>

View File

@@ -1491,7 +1491,7 @@
<target state="final">Nessun obiettivo di ore è stato configurato per questo utente nelle impostazioni del contratto di lavoro.</target> <target state="final">Nessun obiettivo di ore è stato configurato per questo utente nelle impostazioni del contratto di lavoro.</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should" approved="yes"> <trans-unit id="1btzNDa" resname="work_times_should" approved="yes">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="final">Obiettivo di ore</target> <target state="final">Obiettivo di ore</target>
</trans-unit> </trans-unit>
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help" approved="yes"> <trans-unit id="znIJiOw" resname="manual_bookings.duration_help" approved="yes">
@@ -1522,10 +1522,6 @@
<source>completed_month_pdf_note</source> <source>completed_month_pdf_note</source>
<target state="final">Questo documento serve come prova delle ore lavorate.</target> <target state="final">Questo documento serve come prova delle ore lavorate.</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help" approved="yes">
<source>work_times_should.help</source>
<target state="final">I valori configurati qui vengono usati per calcolare il conto orario.</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at" approved="yes"> <trans-unit id="8yqaKzX" resname="confirmed_by_at" approved="yes">
<source>confirmed_by_at</source> <source>confirmed_by_at</source>
<target state="final">Confermato da %user% il %date%</target> <target state="final">Confermato da %user% il %date%</target>

View File

@@ -1484,7 +1484,7 @@
<target state="translated">Arbeidsovereenkomst</target> <target state="translated">Arbeidsovereenkomst</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Streefuren</target> <target state="translated">Streefuren</target>
</trans-unit> </trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at"> <trans-unit id="8yqaKzX" resname="confirmed_by_at">
@@ -1507,10 +1507,6 @@
<source>manual_bookings.work_contract_intro</source> <source>manual_bookings.work_contract_intro</source>
<target state="translated">Handmatige boeken kunnen niet achtereenvolgens worden gewijzigd of verwijderd!</target> <target state="translated">Handmatige boeken kunnen niet achtereenvolgens worden gewijzigd of verwijderd!</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">De hier geconfigureerde waarden worden gebruikt om de rekening per uur te berekenen.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>
<target state="translated">Voor deze gebruiker zijn geen streefuren geconfigureerd in de instellingen van de arbeidsovereenkomst.</target> <target state="translated">Voor deze gebruiker zijn geen streefuren geconfigureerd in de instellingen van de arbeidsovereenkomst.</target>

View File

@@ -1512,7 +1512,7 @@
<target state="translated">Czas pracy</target> <target state="translated">Czas pracy</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Godziny docelowe</target> <target state="translated">Godziny docelowe</target>
</trans-unit> </trans-unit>
<trans-unit id="t1r372x" resname="invisible"> <trans-unit id="t1r372x" resname="invisible">

View File

@@ -1688,13 +1688,9 @@
<target state="translated">Avaliação</target> <target state="translated">Avaliação</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Horas-alvo</target> <target state="translated">Horas-alvo</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Os valores configurados aqui são usados para calcular a hora da conta.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>
<target state="translated">Nenhuma hora-alvo foi configurada para esse utilizador nas configurações do contrato de trabalho.</target> <target state="translated">Nenhuma hora-alvo foi configurada para esse utilizador nas configurações do contrato de trabalho.</target>

View File

@@ -1496,13 +1496,9 @@
<target state="translated">Horários de trabalho da conta</target> <target state="translated">Horários de trabalho da conta</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Horas-alvo</target> <target state="translated">Horas-alvo</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Os valores configurados aqui são usados para calcular a hora da conta.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is"> <trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source> <source>work_times_is</source>
<target state="translated">Is-horas</target> <target state="translated">Is-horas</target>

View File

@@ -1329,10 +1329,6 @@
<source>work_times</source> <source>work_times</source>
<target state="translated">Время работы</target> <target state="translated">Время работы</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Настроенные здесь значения используются для расчета почасового счета.</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is"> <trans-unit id="31dFXhR" resname="work_times_is">
<source>work_times_is</source> <source>work_times_is</source>
<target state="needs-translation">Ис-часы</target> <target state="needs-translation">Ис-часы</target>
@@ -1406,7 +1402,7 @@
<target state="translated">Создано</target> <target state="translated">Создано</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Целевые часы</target> <target state="translated">Целевые часы</target>
</trans-unit> </trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result"> <trans-unit id="uHrZlbq" resname="work_times_result">

View File

@@ -1355,13 +1355,9 @@
<target state="translated">Táto faktúra je generovaná s touto šablónou.</target> <target state="translated">Táto faktúra je generovaná s touto šablónou.</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Cieľové hodiny</target> <target state="translated">Cieľové hodiny</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Nakonfigurované hodnoty se používajú k výpočtu hodinového účtu.</target>
</trans-unit>
<trans-unit id="gcFtM3o" resname="holiday"> <trans-unit id="gcFtM3o" resname="holiday">
<source>holiday</source> <source>holiday</source>
<target state="translated">Dovolenka</target> <target state="translated">Dovolenka</target>

View File

@@ -1516,7 +1516,7 @@
<target state="translated">İnceleme</target> <target state="translated">İnceleme</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Hedef saat</target> <target state="translated">Hedef saat</target>
</trans-unit> </trans-unit>
<trans-unit id="mS4SSmt" resname="work_times"> <trans-unit id="mS4SSmt" resname="work_times">
@@ -1531,10 +1531,6 @@
<source>manual_bookings</source> <source>manual_bookings</source>
<target state="translated">Elle rezervasyonlar</target> <target state="translated">Elle rezervasyonlar</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Burada yapılandırılan değerler saatlik hesabı hesaplamak için kullanılır.</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured"> <trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>
<target state="translated">İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış.</target> <target state="translated">İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış.</target>

View File

@@ -1426,10 +1426,6 @@
<source>work_times</source> <source>work_times</source>
<target state="translated">Робочі години</target> <target state="translated">Робочі години</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">Налаштовані тут значення використовуються для погодинного розрахунку облікового запису.</target>
</trans-unit>
<trans-unit id="EYXzfTO" resname="deleted"> <trans-unit id="EYXzfTO" resname="deleted">
<source>deleted</source> <source>deleted</source>
<target state="translated">Видалено</target> <target state="translated">Видалено</target>
@@ -1443,7 +1439,7 @@
<target state="needs-translation">Після завершення місяця користувач більше не зможе створювати або редагувати час для цього періоду.</target> <target state="needs-translation">Після завершення місяця користувач більше не зможе створювати або редагувати час для цього періоду.</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">Цільові години</target> <target state="translated">Цільові години</target>
</trans-unit> </trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result"> <trans-unit id="uHrZlbq" resname="work_times_result">

View File

@@ -1495,10 +1495,6 @@
<source>work_times_should.none_configured</source> <source>work_times_should.none_configured</source>
<target state="translated">在雇佣合同设置中未为此用户配置目标工时。</target> <target state="translated">在雇佣合同设置中未为此用户配置目标工时。</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">这里配置的数值用于计算每小时的账户。</target>
</trans-unit>
<trans-unit id="W8XQTkJ" resname="complete_month.help" xml:space="preserve"> <trans-unit id="W8XQTkJ" resname="complete_month.help" xml:space="preserve">
<source>complete_month.help</source> <source>complete_month.help</source>
<target state="translated">这将锁定所选日期之前一年中的所有日期。 用户将无法再创建或编辑锁定期间的时间。</target> <target state="translated">这将锁定所选日期之前一年中的所有日期。 用户将无法再创建或编辑锁定期间的时间。</target>
@@ -1528,7 +1524,7 @@
<target state="translated">工作时间</target> <target state="translated">工作时间</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">目标小时数</target> <target state="translated">目标小时数</target>
</trans-unit> </trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is"> <trans-unit id="31dFXhR" resname="work_times_is">

View File

@@ -1047,7 +1047,7 @@
<target state="translated">病假</target> <target state="translated">病假</target>
</trans-unit> </trans-unit>
<trans-unit id="1btzNDa" resname="work_times_should"> <trans-unit id="1btzNDa" resname="work_times_should">
<source>work_times_should</source> <source>Expected number of hours</source>
<target state="translated">目標時數</target> <target state="translated">目標時數</target>
</trans-unit> </trans-unit>
<trans-unit id="c3d6p33" resname="invoice.total_working_time"> <trans-unit id="c3d6p33" resname="invoice.total_working_time">
@@ -1078,10 +1078,6 @@
<source>api_password.missing_title</source> <source>api_password.missing_title</source>
<target state="translated">無法使用 API</target> <target state="translated">無法使用 API</target>
</trans-unit> </trans-unit>
<trans-unit id="PE2iiaW" resname="work_times_should.help">
<source>work_times_should.help</source>
<target state="translated">這裡所設定的數值會用於計算鐘點。</target>
</trans-unit>
<trans-unit id="Y0PRG9Z" resname="profile.2fa_confirmation"> <trans-unit id="Y0PRG9Z" resname="profile.2fa_confirmation">
<source>profile.2fa_confirmation</source> <source>profile.2fa_confirmation</source>
<target state="translated">請確認代碼,以啟用該帳號的雙重要素驗證。</target> <target state="translated">請確認代碼,以啟用該帳號的雙重要素驗證。</target>