diff --git a/composer.json b/composer.json index 1e7d277f..3eb307f2 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "friendsofsymfony/rest-bundle": "^3.0", "gedmo/doctrine-extensions": "^3.6", "horstoeko/zugferd": "^1.0", + "horstoeko/zugferdublbridge": "^1.0", "jms/serializer-bundle": "^5.0", "kevinpapst/tabler-bundle": "^1.4", "league/csv": "^9.4", diff --git a/composer.lock b/composer.lock index 4a775c35..a533ed66 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5de74433dcf1cf3ad650d7643b57e24f", + "content-hash": "0e017e736364638de484ae1799d11b8f", "packages": [ { "name": "azuyalabs/yasumi", @@ -775,16 +775,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.18.0", + "version": "2.18.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95" + "reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", - "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/b769877014de053da0e5cbbb63d0ea2f3b2fea76", + "reference": "b769877014de053da0e5cbbb63d0ea2f3b2fea76", "shasum": "" }, "require": { @@ -876,7 +876,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.1" }, "funding": [ { @@ -892,20 +892,20 @@ "type": "tidelift" } ], - "time": "2025-10-11T04:43:27+00:00" + "time": "2025-11-05T14:42:10+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "71c81279ca0e907c3edc718418b93fd63074856c" + "reference": "49ecc564568d7da101779112579e78b677fbc94a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/71c81279ca0e907c3edc718418b93fd63074856c", - "reference": "71c81279ca0e907c3edc718418b93fd63074856c", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/49ecc564568d7da101779112579e78b677fbc94a", + "reference": "49ecc564568d7da101779112579e78b677fbc94a", "shasum": "" }, "require": { @@ -913,7 +913,7 @@ "doctrine/migrations": "^3.2", "php": "^7.2 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "composer/semver": "^3.0", @@ -925,8 +925,8 @@ "phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpstan/phpstan-symfony": "^1.3 || ^2", "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/phpunit-bridge": "^6.3 || ^7", - "symfony/var-exporter": "^5.4 || ^6 || ^7" + "symfony/phpunit-bridge": "^6.3 || ^7 || ^8", + "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8" }, "type": "symfony-bundle", "autoload": { @@ -961,7 +961,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.5.0" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.6.0" }, "funding": [ { @@ -977,7 +977,7 @@ "type": "tidelift" } ], - "time": "2025-10-12T17:06:40+00:00" + "time": "2025-11-07T19:40:03+00:00" }, { "name": "doctrine/event-manager", @@ -2402,6 +2402,71 @@ }, "time": "2025-10-14T12:07:57+00:00" }, + { + "name": "horstoeko/zugferdublbridge", + "version": "v1.0.15", + "source": { + "type": "git", + "url": "https://github.com/horstoeko/zugferdublbridge.git", + "reference": "059bc2d70114432a42e2ac21f1ea7b50aa8f9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/horstoeko/zugferdublbridge/zipball/059bc2d70114432a42e2ac21f1ea7b50aa8f9b49", + "reference": "059bc2d70114432a42e2ac21f1ea7b50aa8f9b49", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "horstoeko/stringmanagement": "^1", + "nette/php-generator": "*", + "pdepend/pdepend": "^2", + "phpdocumentor/reflection-docblock": "^5", + "phploc/phploc": "^7", + "phpmd/phpmd": "^2", + "phpstan/phpstan": "^1|^2", + "phpunit/phpunit": "^9", + "rector/rector": "*", + "sebastian/phpcpd": "^6", + "squizlabs/php_codesniffer": "^3" + }, + "type": "package", + "autoload": { + "psr-4": { + "horstoeko\\zugferdublbridge\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Erling", + "email": "daniel@erling.com.de", + "role": "lead" + } + ], + "description": "Convert Factur-X/ZUGFeRD (CII-Syntax) to PEPPOL (UBL-Syntax) and visa versa", + "homepage": "https://github.com/horstoeko/zugferdublbridge", + "keywords": [ + "ZUGFeRD", + "cii", + "convert", + "electronic", + "factur-x", + "invoice", + "ubl", + "xrechnung" + ], + "support": { + "issues": "https://github.com/horstoeko/zugferdublbridge/issues", + "source": "https://github.com/horstoeko/zugferdublbridge/tree/v1.0.15" + }, + "time": "2025-05-17T20:05:53+00:00" + }, { "name": "jms/metadata", "version": "2.8.0", @@ -5503,16 +5568,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.27", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "3b9cf252b3bb54d5daddea5704b95ea7117b39f4" + "reference": "31628f36fc97c5714d181b3a8d29efb85c6a7677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/3b9cf252b3bb54d5daddea5704b95ea7117b39f4", - "reference": "3b9cf252b3bb54d5daddea5704b95ea7117b39f4", + "url": "https://api.github.com/repos/symfony/cache/zipball/31628f36fc97c5714d181b3a8d29efb85c6a7677", + "reference": "31628f36fc97c5714d181b3a8d29efb85c6a7677", "shasum": "" }, "require": { @@ -5579,7 +5644,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.27" + "source": "https://github.com/symfony/cache/tree/v6.4.28" }, "funding": [ { @@ -5599,7 +5664,7 @@ "type": "tidelift" } ], - "time": "2025-10-17T12:08:26+00:00" + "time": "2025-10-30T08:37:02+00:00" }, { "name": "symfony/cache-contracts", @@ -5757,16 +5822,16 @@ }, { "name": "symfony/config", - "version": "v6.4.26", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "f18dc5926cb203e125956987def795d052ee774e" + "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/f18dc5926cb203e125956987def795d052ee774e", - "reference": "f18dc5926cb203e125956987def795d052ee774e", + "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62", "shasum": "" }, "require": { @@ -5812,7 +5877,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.26" + "source": "https://github.com/symfony/config/tree/v6.4.28" }, "funding": [ { @@ -5832,7 +5897,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2025-11-01T19:52:02+00:00" }, { "name": "symfony/console", @@ -7116,16 +7181,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.26", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849" + "reference": "c9e69c185c4a845f9d46958cdb0dc7aa847f3981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/6740cdc1a3bffa127966b6056e883b3fe3709849", - "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849", + "url": "https://api.github.com/repos/symfony/http-client/zipball/c9e69c185c4a845f9d46958cdb0dc7aa847f3981", + "reference": "c9e69c185c4a845f9d46958cdb0dc7aa847f3981", "shasum": "" }, "require": { @@ -7190,7 +7255,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.26" + "source": "https://github.com/symfony/http-client/tree/v6.4.28" }, "funding": [ { @@ -7210,7 +7275,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2025-11-05T17:39:22+00:00" }, { "name": "symfony/http-client-contracts", @@ -7292,16 +7357,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.26", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521" + "reference": "1ba1d5fe6465b0fa39c8627ba552b2c29301aa81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/369241591d92bb5dfb4c6ccd6ee94378a45b1521", - "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1ba1d5fe6465b0fa39c8627ba552b2c29301aa81", + "reference": "1ba1d5fe6465b0fa39c8627ba552b2c29301aa81", "shasum": "" }, "require": { @@ -7349,7 +7414,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.26" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.28" }, "funding": [ { @@ -7369,20 +7434,20 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:22:30+00:00" + "time": "2025-11-06T10:41:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.27", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb" + "reference": "bbccadab358e236fcf13cc02a01fc0115d09d787" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb", - "reference": "4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/bbccadab358e236fcf13cc02a01fc0115d09d787", + "reference": "bbccadab358e236fcf13cc02a01fc0115d09d787", "shasum": "" }, "require": { @@ -7467,7 +7532,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.27" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.28" }, "funding": [ { @@ -7487,7 +7552,7 @@ "type": "tidelift" } ], - "time": "2025-10-28T10:06:47+00:00" + "time": "2025-11-06T20:52:25+00:00" }, { "name": "symfony/intl", @@ -7751,16 +7816,16 @@ }, { "name": "symfony/monolog-bridge", - "version": "v6.4.27", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "b950e76daaa5117c367cff22ececce93b9819522" + "reference": "d2f4b68e3247cf44d93f48545c8c072a75c17e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/b950e76daaa5117c367cff22ececce93b9819522", - "reference": "b950e76daaa5117c367cff22ececce93b9819522", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/d2f4b68e3247cf44d93f48545c8c072a75c17e5b", + "reference": "d2f4b68e3247cf44d93f48545c8c072a75c17e5b", "shasum": "" }, "require": { @@ -7810,7 +7875,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.27" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.28" }, "funding": [ { @@ -7830,7 +7895,7 @@ "type": "tidelift" } ], - "time": "2025-10-14T13:20:03+00:00" + "time": "2025-10-30T19:57:08+00:00" }, { "name": "symfony/monolog-bundle", @@ -8533,16 +8598,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.26", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de" + "reference": "ae064a6d9cf39507f9797658465a2ca702965fa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/6fc4c445f22857d4b8b40a02b73f423ddab295de", - "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de", + "url": "https://api.github.com/repos/symfony/routing/zipball/ae064a6d9cf39507f9797658465a2ca702965fa8", + "reference": "ae064a6d9cf39507f9797658465a2ca702965fa8", "shasum": "" }, "require": { @@ -8596,7 +8661,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.26" + "source": "https://github.com/symfony/routing/tree/v6.4.28" }, "funding": [ { @@ -8616,7 +8681,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2025-10-31T16:43:05+00:00" }, { "name": "symfony/runtime", @@ -9175,16 +9240,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -9238,7 +9303,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -9249,12 +9314,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/stopwatch", @@ -9512,16 +9581,16 @@ }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -9570,7 +9639,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -9581,12 +9650,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/twig-bridge", @@ -11478,16 +11551,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.89.1", + "version": "v3.89.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "f34967da2866ace090a2b447de1f357356474573" + "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f34967da2866ace090a2b447de1f357356474573", - "reference": "f34967da2866ace090a2b447de1f357356474573", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7569658f91e475ec93b99bd5964b059ad1336dcf", + "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf", "shasum": "" }, "require": { @@ -11523,7 +11596,7 @@ "justinrainbow/json-schema": "^6.5", "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.8", + "php-coveralls/php-coveralls": "^2.9", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", @@ -11569,7 +11642,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.2" }, "funding": [ { @@ -11577,7 +11650,7 @@ "type": "github" } ], - "time": "2025-10-24T12:05:10+00:00" + "time": "2025-11-06T21:12:50+00:00" }, { "name": "masterminds/html5", @@ -11866,16 +11939,16 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.10", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "5eaf37b87288474051469aee9f937fc9d862f330" + "reference": "368ad1c713a6d95763890bc2292694a603ece7c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/5eaf37b87288474051469aee9f937fc9d862f330", - "reference": "5eaf37b87288474051469aee9f937fc9d862f330", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/368ad1c713a6d95763890bc2292694a603ece7c8", + "reference": "368ad1c713a6d95763890bc2292694a603ece7c8", "shasum": "" }, "require": { @@ -11905,7 +11978,7 @@ "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0.2", - "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.8", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", @@ -11933,9 +12006,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.10" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.11" }, - "time": "2025-10-06T10:01:02+00:00" + "time": "2025-11-04T09:55:35+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -14020,16 +14093,16 @@ }, { "name": "symfony/browser-kit", - "version": "v6.4.24", + "version": "v6.4.28", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "3537d17782f8c20795b194acb6859071b60c6fac" + "reference": "067e301786bbb58048077fc10507aceb18226e23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3537d17782f8c20795b194acb6859071b60c6fac", - "reference": "3537d17782f8c20795b194acb6859071b60c6fac", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/067e301786bbb58048077fc10507aceb18226e23", + "reference": "067e301786bbb58048077fc10507aceb18226e23", "shasum": "" }, "require": { @@ -14068,7 +14141,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v6.4.24" + "source": "https://github.com/symfony/browser-kit/tree/v6.4.28" }, "funding": [ { @@ -14088,7 +14161,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2025-10-16T22:35:35+00:00" }, { "name": "symfony/debug-bundle", diff --git a/config/packages/fos_rest.yaml b/config/packages/fos_rest.yaml index 4ef939b2..f65e4f35 100644 --- a/config/packages/fos_rest.yaml +++ b/config/packages/fos_rest.yaml @@ -39,3 +39,5 @@ fos_rest: - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] } zone: - { path: ^/api/* } + service: + view_handler: App\API\ViewHandler \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index d03dbf59..be0b14f0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -112,102 +112,102 @@ services: # ================================================================================ App\Repository\TimesheetRepository: - class: App\Repository\TimesheetRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\TimesheetRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Timesheet'] App\Repository\UserRepository: - class: App\Repository\UserRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\UserRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\User'] App\Repository\TeamRepository: - class: App\Repository\TeamRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\TeamRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Team'] App\Repository\ActivityRepository: - class: App\Repository\ActivityRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ActivityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Activity'] App\Repository\ProjectRepository: - class: App\Repository\ProjectRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ProjectRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Project'] App\Repository\TagRepository: - class: App\Repository\TagRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\TagRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Tag'] App\Repository\CustomerRepository: - class: App\Repository\CustomerRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\CustomerRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Customer'] App\Repository\InvoiceTemplateRepository: - class: App\Repository\InvoiceTemplateRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\InvoiceTemplateRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\InvoiceTemplate'] App\Repository\ConfigurationRepository: - class: App\Repository\ConfigurationRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ConfigurationRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Configuration'] App\Repository\RoleRepository: - class: App\Repository\RoleRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\RoleRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Role'] App\Repository\RolePermissionRepository: - class: App\Repository\RolePermissionRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\RolePermissionRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\RolePermission'] - + App\Repository\InvoiceDocumentRepository: class: App\Repository\InvoiceDocumentRepository arguments: ['%kimai.invoice.documents%'] App\Repository\CustomerRateRepository: - class: App\Repository\CustomerRateRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\CustomerRateRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\CustomerRate'] App\Repository\ActivityRateRepository: - class: App\Repository\ActivityRateRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ActivityRateRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\ActivityRate'] App\Repository\ProjectRateRepository: - class: App\Repository\ProjectRateRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ProjectRateRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\ProjectRate'] App\Repository\InvoiceRepository: - class: App\Repository\InvoiceRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\InvoiceRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Invoice'] App\Repository\BookmarkRepository: - class: App\Repository\BookmarkRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\BookmarkRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\Bookmark'] App\Repository\WorkingTimeRepository: - class: App\Repository\WorkingTimeRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\WorkingTimeRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\WorkingTime'] App\Repository\AccessTokenRepository: - class: App\Repository\AccessTokenRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\AccessTokenRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\AccessToken'] App\Repository\ExportTemplateRepository: - class: App\Repository\ExportTemplateRepository - factory: ['@doctrine.orm.entity_manager', getRepository] + class: App\Repository\ExportTemplateRepository + factory: ['@doctrine.orm.entity_manager', getRepository] arguments: ['App\Entity\ExportTemplate'] monolog.formatter.kimai: @@ -221,3 +221,6 @@ services: arguments: - "[%%datetime%%] %%message%% %%context%%\n" - "Y-m-d H:i:s" + + App\API\ViewHandler: + arguments: ['@fos_rest.view_handler.default'] diff --git a/phpstan.neon b/phpstan.neon index 6eba82c3..0b6000d6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -800,11 +800,6 @@ parameters: count: 1 path: src/Controller/ProjectController.php - - - message: "#^Call to function array_key_exists\\(\\) with 'entry' and array\\{day\\?\\: mixed, entry\\: mixed} will always evaluate to true\\.$#" - count: 1 - path: src/Controller/QuickEntryController.php - - message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#" count: 1 diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 0a7856bc..21832968 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -161,7 +161,7 @@ final class ActivityController extends BaseApiController $form->submit($request->request->all()); if ($form->isValid()) { - $this->repository->saveActivity($activity); + $this->activityService->saveActivity($activity); $view = new View($activity, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -203,7 +203,7 @@ final class ActivityController extends BaseApiController return $this->viewHandler->handle($view); } - $this->repository->saveActivity($activity); + $this->activityService->saveActivity($activity); $view = new View($activity, Response::HTTP_OK); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -253,7 +253,7 @@ final class ActivityController extends BaseApiController $meta->setValue($value); - $this->repository->saveActivity($activity); + $this->activityService->saveActivity($activity); $view = new View($activity, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); diff --git a/src/API/BaseApiController.php b/src/API/BaseApiController.php index 4a9a6b16..ce475d54 100644 --- a/src/API/BaseApiController.php +++ b/src/API/BaseApiController.php @@ -12,13 +12,12 @@ namespace App\API; use App\Entity\User; use App\Repository\Query\BaseQuery; use App\Timesheet\DateTimeFactory; -use App\Utils\Pagination; use FOS\RestBundle\Request\ParamFetcherInterface; -use FOS\RestBundle\View\View; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; abstract class BaseApiController extends AbstractController { @@ -60,13 +59,19 @@ abstract class BaseApiController extends AbstractController return DateTimeFactory::createByUser($user); } - protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): void + /** + * @template T of BaseQuery + * @param T $query + * @param ParamFetcherInterface $paramFetcher + * @return T + */ + protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): BaseQuery { $query->setIsApiCall(true); $query->setCurrentUser($this->getUser()); // there is no function has() in ParamFetcherInterface, so we need to use all() and check for the key - $all = $paramFetcher->all(true); + $all = $paramFetcher->all(); if (\array_key_exists('page', $all)) { $page = $all['page']; @@ -78,33 +83,28 @@ abstract class BaseApiController extends AbstractController if (\array_key_exists('size', $all)) { $size = $all['size']; if (is_numeric($size)) { - $query->setPageSize((int) $size); + $size = (int) $size; + if ($size < 1 || $size > 500) { + throw new BadRequestHttpException('Size must be between 1 and 500'); + } + $query->setPageSize($size); } } - if (\array_key_exists('pageSize', $all)) { - $size = $all['pageSize']; - if (is_numeric($size)) { - $query->setPageSize((int) $size); + if (\array_key_exists('order', $all)) { + $order = $all['order']; + if (\is_string($order) && $order !== '') { + $query->setOrder($order); } } - } - protected function createPaginatedView(Pagination $pagination): View - { - $results = (array) $pagination->getCurrentPageResults(); + if (\array_key_exists('orderBy', $all)) { + $orderBy = $all['orderBy']; + if (\is_string($orderBy) && $orderBy !== '') { + $query->setOrderBy($orderBy); + } + } - $view = new View($results, 200); - $this->addPagination($view, $pagination); - - return $view; - } - - protected function addPagination(View $view, Pagination $pagination): void - { - $view->setHeader('X-Page', (string) $pagination->getCurrentPage()); - $view->setHeader('X-Total-Count', (string) $pagination->getNbResults()); - $view->setHeader('X-Total-Pages', (string) $pagination->getNbPages()); - $view->setHeader('X-Per-Page', (string) $pagination->getMaxPerPage()); + return $query; } } diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index b8ae22bb..b6ce3795 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -136,7 +136,7 @@ final class CustomerController extends BaseApiController $form->submit($request->request->all()); if ($form->isValid()) { - $this->repository->saveCustomer($customer); + $this->customerService->saveCustomer($customer); $view = new View($customer, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -178,7 +178,7 @@ final class CustomerController extends BaseApiController return $this->viewHandler->handle($view); } - $this->repository->saveCustomer($customer); + $this->customerService->saveCustomer($customer); $view = new View($customer, Response::HTTP_OK); $view->getContext()->setGroups(self::GROUPS_ENTITY); @@ -228,7 +228,7 @@ final class CustomerController extends BaseApiController $meta->setValue($value); - $this->repository->saveCustomer($customer); + $this->customerService->saveCustomer($customer); $view = new View($customer, 200); $view->getContext()->setGroups(self::GROUPS_ENTITY); diff --git a/src/API/InvoiceController.php b/src/API/InvoiceController.php index e860d62e..04ab6b80 100644 --- a/src/API/InvoiceController.php +++ b/src/API/InvoiceController.php @@ -80,7 +80,7 @@ final class InvoiceController extends BaseApiController } $data = $this->repository->getPagerfantaForQuery($query); - $view = $this->createPaginatedView($data); + $view = new View($data, 200); $view->getContext()->setGroups(self::GROUPS_COLLECTION); return $this->viewHandler->handle($view); diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index c317a45f..135dd10a 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -82,7 +82,7 @@ final class TimesheetController extends BaseApiController #[Rest\QueryParam(name: 'activity', requirements: '\d+', strict: true, nullable: true, description: 'Activity ID to filter timesheets')] #[Rest\QueryParam(name: 'activities', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of activity IDs to filter, e.g.: activities[]=1&activities[]=2')] #[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')] - #[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')] + #[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50, max: 500)')] #[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')] #[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')] #[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')] @@ -97,7 +97,7 @@ final class TimesheetController extends BaseApiController public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response { $query = new TimesheetQuery(false); - $query->setCurrentUser($this->getUser()); + $this->prepareQuery($query, $paramFetcher); $seeAll = false; if ($this->isGranted('view_other_timesheet')) { @@ -169,16 +169,6 @@ final class TimesheetController extends BaseApiController $query->addActivity($activity); } - $page = $paramFetcher->get('page'); - if (\is_string($page) && $page !== '') { - $query->setPage((int) $page); - } - - $size = $paramFetcher->get('size'); - if (\is_string($size) && $size !== '') { - $query->setPageSize((int) $size); - } - /** @var array $tags */ $tags = $paramFetcher->get('tags'); if (\is_array($tags) && \count($tags) > 0) { @@ -191,16 +181,6 @@ final class TimesheetController extends BaseApiController } } - $order = $paramFetcher->get('order'); - if (\is_string($order) && $order !== '') { - $query->setOrder($order); - } - - $orderBy = $paramFetcher->get('orderBy'); - if (\is_string($orderBy) && $orderBy !== '') { - $query->setOrderBy($orderBy); - } - $factory = $this->getDateTimeFactory(); $begin = $paramFetcher->get('begin'); @@ -252,12 +232,9 @@ final class TimesheetController extends BaseApiController $query->setModifiedAfter($factory->createDateTime($modifiedAfter)); } - $query->setIsApiCall(true); $data = $this->repository->getPagerfantaForQuery($query); - $results = (array) $data->getCurrentPageResults(); - $view = new View($results, 200); - $this->addPagination($view, $data); + $view = new View($data, 200); $full = $paramFetcher->get('full'); if ($full === '1' || $full === 'true') { diff --git a/src/API/ViewHandler.php b/src/API/ViewHandler.php new file mode 100644 index 00000000..492c2dd8 --- /dev/null +++ b/src/API/ViewHandler.php @@ -0,0 +1,78 @@ +baseViewHandler->setExclusionStrategyGroups($groups); + } + + public function setExclusionStrategyVersion(string $version): void + { + $this->baseViewHandler->setExclusionStrategyVersion($version); + } + + public function setSerializeNullStrategy(bool $isEnabled): void + { + $this->baseViewHandler->setSerializeNullStrategy($isEnabled); + } + + public function supports(string $format): bool + { + return $this->baseViewHandler->supports($format); + } + + public function registerHandler(string $format, callable $callable): void + { + $this->baseViewHandler->registerHandler($format, $callable); + } + + public function handle(View $view, ?Request $request = null): Response + { + $data = $view->getData(); + + if ($data instanceof Pagination) { + $results = (array) $data->getCurrentPageResults(); + $view->setData($results); + + $view->setHeader('X-Page', (string) $data->getCurrentPage()); + $view->setHeader('X-Total-Count', (string) $data->getNbResults()); + $view->setHeader('X-Total-Pages', (string) $data->getNbPages()); + $view->setHeader('X-Per-Page', (string) $data->getMaxPerPage()); + } + + return $this->baseViewHandler->handle($view, $request); + } + + public function createRedirectResponse(View $view, string $location, string $format): Response + { + return $this->baseViewHandler->createRedirectResponse($view, $location, $format); + } + + public function createResponse(View $view, Request $request, string $format): Response + { + return $this->baseViewHandler->createResponse($view, $request, $format); + } +} diff --git a/src/Command/InvoiceCreateCommand.php b/src/Command/InvoiceCreateCommand.php index ca8838c7..514a379f 100644 --- a/src/Command/InvoiceCreateCommand.php +++ b/src/Command/InvoiceCreateCommand.php @@ -277,7 +277,7 @@ final class InvoiceCreateCommand extends Command $tpl = $this->getTemplateForCustomer($input, $customer); if (null === $tpl) { - $io->warning(\sprintf('Could not find invoice template for project "%s", skipping!', $project->getName())); + $io->warning('Could not find invoice template for project, skipping.'); continue; } $query->setTemplate($tpl); @@ -295,7 +295,7 @@ final class InvoiceCreateCommand extends Command $invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher); } } catch (\Exception $ex) { - $io->error(\sprintf('Failed to create invoice for project "%s" with: %s', $project->getName(), $ex->getMessage())); + $io->error(\sprintf('Failed to create invoice for project with: %s', $ex->getMessage())); } } @@ -352,7 +352,7 @@ final class InvoiceCreateCommand extends Command $tpl = $this->getTemplateForCustomer($input, $customer); if (null === $tpl) { - $io->warning(\sprintf('Could not find invoice template for customer "%s", skipping!', $customer->getName())); + $io->warning('Could not find invoice template for customer, skipping.'); continue; } $query->setTemplate($tpl); @@ -370,7 +370,7 @@ final class InvoiceCreateCommand extends Command $invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher); } } catch (\Exception $ex) { - $io->error(\sprintf('Failed to create invoice for customer "%s" with: %s', $customer->getName(), $ex->getMessage())); + $io->error(\sprintf('Failed to create invoice for customer with: %s', $ex->getMessage())); } } @@ -416,7 +416,7 @@ final class InvoiceCreateCommand extends Command $file = $this->serviceInvoice->getInvoiceFile($invoice); if (null === $file) { $io->warning( - \sprintf('Created invoice with ID %s, but file was not found %s', $invoice->getId(), $invoice->getInvoiceFilename()) + \sprintf('Created invoice with ID %s, but file was not found %s', $invoice->getId() ?? 'unknown', $invoice->getInvoiceFilename() ?? 'unknown') ); continue; } diff --git a/src/Constants.php b/src/Constants.php index cddb5ffd..a90a5ea3 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ final class Constants /** * The current release version */ - public const VERSION = '2.40.0'; + public const VERSION = '2.41.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 24000; + public const VERSION_ID = 24100; /** * The software name */ diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index c122f73e..0e469d9f 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -35,6 +35,7 @@ use App\Repository\Query\CustomerQuery; use App\Repository\Query\ProjectQuery; use App\Repository\Query\TeamQuery; use App\Repository\Query\TimesheetQuery; +use App\Repository\Query\VisibilityInterface; use App\Repository\TeamRepository; use App\Utils\DataTable; use App\Utils\PageSetup; @@ -146,7 +147,7 @@ final class CustomerController extends AbstractController { $customer = $customerService->createNewCustomer(''); - return $this->renderCustomerForm($customer, $request, true); + return $this->renderCustomerForm($customer, $request, true, $customerService); } #[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])] @@ -281,7 +282,7 @@ final class CustomerController extends AbstractController $query->setPage($page); $query->setPageSize(5); $query->addCustomer($customer); - $query->setShowBoth(); + $query->setVisibility(VisibilityInterface::SHOW_BOTH); $query->addOrderGroup('visible', ProjectQuery::ORDER_DESC); $query->addOrderGroup('name', ProjectQuery::ORDER_ASC); @@ -421,9 +422,9 @@ final class CustomerController extends AbstractController #[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])] #[IsGranted('edit', 'customer')] - public function editAction(Customer $customer, Request $request): Response + public function editAction(Customer $customer, Request $request, CustomerService $customerService): Response { - return $this->renderCustomerForm($customer, $request); + return $this->renderCustomerForm($customer, $request, false, $customerService); } #[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])] @@ -496,7 +497,7 @@ final class CustomerController extends AbstractController return $writer->getFileResponse($spreadsheet); } - private function renderCustomerForm(Customer $customer, Request $request, bool $create = false): Response + private function renderCustomerForm(Customer $customer, Request $request, bool $create, CustomerService $customerService): Response { $editForm = $this->createEditForm($customer); @@ -504,7 +505,7 @@ final class CustomerController extends AbstractController if ($editForm->isSubmitted() && $editForm->isValid()) { try { - $this->repository->saveCustomer($customer); + $customerService->saveCustomer($customer); $this->flashSuccess('action.update.success'); if ($create) { diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 13aec0ad..6dcaca66 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -321,7 +321,7 @@ final class InvoiceController extends AbstractController if (null === $file) { throw $this->createNotFoundException( - \sprintf('Invoice file "%s" could not be found for invoice ID "%s"', $invoice->getInvoiceFilename(), $invoice->getId()) + \sprintf('Invoice file could not be found for invoice ID "%s"', $invoice->getId()) ); } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index d69546fa..e84a9c45 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -38,6 +38,7 @@ use App\Repository\Query\ActivityQuery; use App\Repository\Query\ProjectQuery; use App\Repository\Query\TeamQuery; use App\Repository\Query\TimesheetQuery; +use App\Repository\Query\VisibilityInterface; use App\Repository\TeamRepository; use App\Utils\Context; use App\Utils\DataTable; @@ -313,7 +314,7 @@ final class ProjectController extends AbstractController $query->setPageSize(5); $query->addProject($project); $query->setExcludeGlobals(true); - $query->setShowBoth(); + $query->setVisibility(VisibilityInterface::SHOW_BOTH); $query->addOrderGroup('visible', ActivityQuery::ORDER_DESC); $query->addOrderGroup('name', ActivityQuery::ORDER_ASC); diff --git a/src/Controller/QuickEntryController.php b/src/Controller/QuickEntryController.php index 7a297231..5c6bb2bc 100644 --- a/src/Controller/QuickEntryController.php +++ b/src/Controller/QuickEntryController.php @@ -77,6 +77,7 @@ final class QuickEntryController extends AbstractController $endWeek = $factory->getEndOfWeek($begin); $tmpDay = clone $startWeek; + /** @var array $week */ $week = []; while ($tmpDay < $endWeek) { $nextDay = clone $tmpDay; @@ -125,7 +126,7 @@ final class QuickEntryController extends AbstractController if ($amount > 0) { $takeOverWeeks = $this->configuration->find('quick_entry.recent_activity_weeks'); $startFrom = null; - if ($takeOverWeeks !== null && \intval($takeOverWeeks) > 0) { + if (is_numeric($takeOverWeeks) && \intval($takeOverWeeks) > 0) { $startFrom = clone $startWeek; $startFrom->modify(\sprintf('-%s weeks', (string) $takeOverWeeks)); } diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 58700cfa..a249fc7b 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -22,6 +22,7 @@ use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -94,6 +95,10 @@ final class TeamController extends AbstractController { $newTeam = clone $team; + if ($team->getName() === null) { + throw new BadRequestHttpException('Team with empty name cannot be duplicated'); + } + $i = 1; do { $newName = \sprintf('%s (%s)', $team->getName(), $i++); diff --git a/src/DataFixtures/TimesheetFixtures.php b/src/DataFixtures/TimesheetFixtures.php index 42dbb2dd..117b1c0d 100644 --- a/src/DataFixtures/TimesheetFixtures.php +++ b/src/DataFixtures/TimesheetFixtures.php @@ -153,18 +153,18 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface $qb = $manager->getRepository($class)->createQueryBuilder('entity'); - /** @var array $all */ - $all = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult(); + /** @var array $result */ + $result = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult(); - if (\count($all) === 0) { - throw new \Exception('Need users to setup teams'); + if (\count($result) === 0) { + throw new \Exception('Could not find any entity: ' . $class); } - return $all; + return $result; } /** - * @return array + * @return non-empty-array */ private function getAllTags(ObjectManager $manager): array { diff --git a/src/Export/ColumnConverter.php b/src/Export/ColumnConverter.php index acefb1ac..833e3bda 100644 --- a/src/Export/ColumnConverter.php +++ b/src/Export/ColumnConverter.php @@ -236,6 +236,8 @@ final class ColumnConverter $columns[$column] = (new Column('vat_id', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getVatId()); } elseif ($column === 'project.order_number') { $columns[$column] = (new Column('orderNumber', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getOrderNumber()); + } elseif ($column === 'id') { + $columns[$column] = (new Column('id', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getId()); } elseif (str_starts_with($column, 'timesheet.meta.') && \array_key_exists($column, $timesheetMeta)) { $columns[$column] = $timesheetMeta[$column]; } elseif (str_starts_with($column, 'customer.meta.') && \array_key_exists($column, $customerMeta)) { diff --git a/src/Form/Type/DurationType.php b/src/Form/Type/DurationType.php index 7a87998c..d5ffd487 100644 --- a/src/Form/Type/DurationType.php +++ b/src/Form/Type/DurationType.php @@ -49,6 +49,7 @@ final class DurationType extends AbstractType $class .= ' ' . $view->vars['attr']['class']; } $view->vars['attr']['class'] = $class; + $view->vars['attr']['autocomplete'] = 'off'; $view->vars['toggle'] = $options['toggle']; if ($options['preset_hours'] !== null && $options['preset_minutes'] !== null) { diff --git a/src/Form/Type/ExportColumnsType.php b/src/Form/Type/ExportColumnsType.php index b158f4de..b9811b9d 100644 --- a/src/Form/Type/ExportColumnsType.php +++ b/src/Form/Type/ExportColumnsType.php @@ -49,6 +49,7 @@ final class ExportColumnsType extends AbstractType { $columns = [ 'timesheet' => [ + 'id' => 'id', 'date' => 'date', 'begin' => 'begin', 'end' => 'end', diff --git a/src/Invoice/Calculator/AbstractCalculator.php b/src/Invoice/Calculator/AbstractCalculator.php index bf958a44..c46ea3ab 100644 --- a/src/Invoice/Calculator/AbstractCalculator.php +++ b/src/Invoice/Calculator/AbstractCalculator.php @@ -12,15 +12,48 @@ namespace App\Invoice\Calculator; use App\Invoice\InvoiceItem; use App\Invoice\InvoiceModel; use App\Invoice\TaxRow; +use App\Timesheet\Util; abstract class AbstractCalculator { protected InvoiceModel $model; + /** + * @var InvoiceItem[] + */ + private array $cached = []; /** + * TODO make this method abstract in 3.0 + * * @return InvoiceItem[] */ - abstract public function getEntries(): array; + protected function calculateEntries(): array + { + return []; + } + + /** + * TODO make this method final in 3.0 + * + * @return InvoiceItem[] + */ + public function getEntries(): array + { + if (\count($this->cached) === 0) { + foreach ($this->calculateEntries() as $entry) { + if (!$entry->isFixedRate() && $entry->getHourlyRate() !== null && $entry->getHourlyRate() > 0) { + $entry->setDuration(Util::decimalizeDuration($entry->getDuration())); + // when merging many entries, we might run into rounding issues + // so we have to recalculate the hourly rate here + $entry->setRate(Util::calculateRate($entry->getHourlyRate(), $entry->getDuration())); + } + + $this->cached[] = $entry; + } + } + + return $this->cached; + } /** * @param array $items @@ -45,11 +78,12 @@ abstract class AbstractCalculator public function getSubtotal(): float { $amount = 0.00; - foreach ($this->model->getEntries() as $entry) { + // using the entries and not the raw data, so we make sure to use the same base for everything + foreach ($this->getEntries() as $entry) { $amount += $entry->getRate(); } - return round($amount, 2); + return round($amount, 2, PHP_ROUND_HALF_UP); } /** @@ -80,12 +114,12 @@ abstract class AbstractCalculator $tax += $row->getAmount(); } - return round($tax, 2); + return round($tax, 2, PHP_ROUND_HALF_UP); } public function getTotal(): float { - return $this->getSubtotal() + $this->getTax(); + return round($this->getSubtotal() + $this->getTax(), 2, PHP_ROUND_HALF_UP); } /** @@ -94,7 +128,8 @@ abstract class AbstractCalculator public function getTimeWorked(): int { $time = 0; - foreach ($this->model->getEntries() as $entry) { + // using the entries and not the raw data, so we make sure to use the same base for everything + foreach ($this->getEntries() as $entry) { if (null !== $entry->getDuration()) { $time += $entry->getDuration(); } diff --git a/src/Invoice/Calculator/AbstractMergedCalculator.php b/src/Invoice/Calculator/AbstractMergedCalculator.php index ab7b6fa3..97d0892b 100644 --- a/src/Invoice/Calculator/AbstractMergedCalculator.php +++ b/src/Invoice/Calculator/AbstractMergedCalculator.php @@ -43,7 +43,7 @@ abstract class AbstractMergedCalculator extends AbstractCalculator $invoiceItem->setAmount($invoiceItem->getAmount() + $amount); $invoiceItem->setUser($entry->getUser()); $invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate()); - $invoiceItem->setInternalRate($invoiceItem->getInternalRate() + ($entry->getInternalRate() ?? 0.00)); + $invoiceItem->setInternalRate($invoiceItem->getInternalRate() + ($entry->getInternalRate() ?? 0.00)); // @phpstan-ignore method.deprecated,method.deprecated $invoiceItem->setDuration($duration); if (null !== $entry->getFixedRate()) { diff --git a/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php b/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php index 4b6f4ca0..e5679dbd 100644 --- a/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php +++ b/src/Invoice/Calculator/AbstractSumInvoiceCalculator.php @@ -47,16 +47,16 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp $prefix = $this->calculateSumIdentifier($entry); if (null !== $entry->getFixedRate()) { - return $prefix . '_fixed_' . (string) $entry->getFixedRate(); + return $prefix . '_fixed_' . $entry->getFixedRate(); } - return $prefix . '_hourly_' . (string) $entry->getHourlyRate(); + return $prefix . '_hourly_' . ($entry->getHourlyRate() ?? '__NULL__'); } /** * @return InvoiceItem[] */ - public function getEntries(): array + protected function calculateEntries(): array { $entries = $this->model->getEntries(); if (empty($entries)) { diff --git a/src/Invoice/Calculator/DefaultCalculator.php b/src/Invoice/Calculator/DefaultCalculator.php index 3d412d9a..c1f89e26 100644 --- a/src/Invoice/Calculator/DefaultCalculator.php +++ b/src/Invoice/Calculator/DefaultCalculator.php @@ -23,7 +23,7 @@ final class DefaultCalculator extends AbstractMergedCalculator implements Calcul /** * @return InvoiceItem[] */ - public function getEntries(): array + protected function calculateEntries(): array { $entries = []; diff --git a/src/Invoice/Calculator/PriceInvoiceCalculator.php b/src/Invoice/Calculator/PriceInvoiceCalculator.php index bbef2f2f..9c8b8ed2 100644 --- a/src/Invoice/Calculator/PriceInvoiceCalculator.php +++ b/src/Invoice/Calculator/PriceInvoiceCalculator.php @@ -23,7 +23,7 @@ final class PriceInvoiceCalculator extends AbstractSumInvoiceCalculator implemen return ['fixed_' . $invoiceItem->getFixedRate()]; } - return ['hourly_' . $invoiceItem->getHourlyRate()]; + return ['hourly_' . ($invoiceItem->getHourlyRate() ?? '__NULL__')]; } public function getId(): string diff --git a/src/Invoice/Calculator/ShortInvoiceCalculator.php b/src/Invoice/Calculator/ShortInvoiceCalculator.php index 3286ae02..19ac57e5 100644 --- a/src/Invoice/Calculator/ShortInvoiceCalculator.php +++ b/src/Invoice/Calculator/ShortInvoiceCalculator.php @@ -21,7 +21,7 @@ final class ShortInvoiceCalculator extends AbstractMergedCalculator implements C /** * @return InvoiceItem[] */ - public function getEntries(): array + protected function calculateEntries(): array { $entries = $this->model->getEntries(); if (empty($entries)) { diff --git a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php index 7b47ff68..f178eba0 100644 --- a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php @@ -29,7 +29,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator $formatter = $this->model->getFormatter(); $rate = $item->getRate(); - $internalRate = $item->getInternalRate(); + $internalRate = $item->getInternalRate(); // @phpstan-ignore method.deprecated $appliedRate = $item->getHourlyRate(); $amount = $formatter->getFormattedDecimalDuration($item->getDuration()); $description = $item->getDescription(); @@ -63,9 +63,9 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator 'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency), 'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, $currency, false), 'entry.rate_plain' => $appliedRate, - 'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency), - 'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false), - 'entry.rate_internal_plain' => $internalRate, + 'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency), // @deprecated since 2.41 + 'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false), // @deprecated since 2.41 + 'entry.rate_internal_plain' => $internalRate, // @deprecated since 2.41 'entry.rate_fixed' => ($item->isFixedRate() ? $item->getFixedRate() : null), 'entry.total' => $formatter->getFormattedMoney($rate, $currency), 'entry.total_nc' => $formatter->getFormattedMoney($rate, $currency, false), diff --git a/src/Invoice/InvoiceItem.php b/src/Invoice/InvoiceItem.php index 20152dec..81271618 100644 --- a/src/Invoice/InvoiceItem.php +++ b/src/Invoice/InvoiceItem.php @@ -19,6 +19,9 @@ final class InvoiceItem private ?float $fixedRate = null; private ?float $hourlyRate = null; private float $rate = 0.00; + /** + * @deprecated since 2.41 - internal rate is not needed in invoices + */ private float $rateInternal = 0.00; private float $amount = 0.00; private ?string $description = null; @@ -85,9 +88,14 @@ final class InvoiceItem return $this; } + public function getAppliedRate(): float + { + return $this->fixedRate ?? $this->hourlyRate ?? 0.00; + } + public function isFixedRate(): bool { - return null !== $this->getFixedRate(); + return $this->fixedRate !== null; } public function getFixedRate(): ?float @@ -126,11 +134,17 @@ final class InvoiceItem return $this; } + /** + * @deprecated since 2.41 - internal rate is not needed in invoices + */ public function getInternalRate(): float { return $this->rateInternal; } + /** + * @deprecated since 2.41 - internal rate is not needed in invoices + */ public function setInternalRate(float $rateInternal): InvoiceItem { $this->rateInternal = $rateInternal; diff --git a/src/Project/ProjectDuplicationService.php b/src/Project/ProjectDuplicationService.php index 08ec71c7..3c954ff6 100644 --- a/src/Project/ProjectDuplicationService.php +++ b/src/Project/ProjectDuplicationService.php @@ -9,6 +9,7 @@ namespace App\Project; +use App\Activity\ActivityService; use App\Entity\Project; use App\Repository\ActivityRateRepository; use App\Repository\ActivityRepository; @@ -21,7 +22,8 @@ final class ProjectDuplicationService private readonly ProjectService $projectService, private readonly ActivityRepository $activityRepository, private readonly ProjectRateRepository $projectRateRepository, - private readonly ActivityRateRepository $activityRateRepository + private readonly ActivityRateRepository $activityRateRepository, + private readonly ActivityService $activityService ) { } @@ -68,7 +70,7 @@ final class ProjectDuplicationService $newActivity->setMetaField($newMetaField); } - $this->activityRepository->saveActivity($newActivity); + $this->activityService->saveActivity($newActivity); foreach ($this->activityRateRepository->getRatesForActivity($activity) as $rate) { $newRate = clone $rate; diff --git a/src/Repository/Query/TagQuery.php b/src/Repository/Query/TagQuery.php index 3c520b9e..9f342696 100644 --- a/src/Repository/Query/TagQuery.php +++ b/src/Repository/Query/TagQuery.php @@ -9,7 +9,7 @@ namespace App\Repository\Query; -class TagQuery extends BaseQuery +class TagQuery extends BaseQuery implements VisibilityInterface { use VisibilityTrait; diff --git a/src/Repository/Query/VisibilityTrait.php b/src/Repository/Query/VisibilityTrait.php index e2f3b923..1629a48a 100644 --- a/src/Repository/Query/VisibilityTrait.php +++ b/src/Repository/Query/VisibilityTrait.php @@ -20,10 +20,9 @@ trait VisibilityTrait public function setVisibility(int $visibility): void { - if (!\in_array($visibility, VisibilityInterface::ALLOWED_VISIBILITY_STATES, true)) { - throw new \InvalidArgumentException('Unknown visibility given'); + if (\in_array($visibility, VisibilityInterface::ALLOWED_VISIBILITY_STATES, true)) { + $this->visibility = $visibility; } - $this->visibility = $visibility; } public function isShowHidden(): bool @@ -36,6 +35,9 @@ trait VisibilityTrait return $this->visibility === VisibilityInterface::SHOW_VISIBLE; } + /** + * @deprecated since 2.41 + */ public function setShowBoth(): void { $this->setVisibility(VisibilityInterface::SHOW_BOTH); diff --git a/src/Repository/Search/SearchHelper.php b/src/Repository/Search/SearchHelper.php index e8cb4369..132a7a18 100644 --- a/src/Repository/Search/SearchHelper.php +++ b/src/Repository/Search/SearchHelper.php @@ -43,8 +43,10 @@ final class SearchHelper $rootAlias = $aliases[0]; $searchAnd = $qb->expr()->andX(); + $metaFieldClass = $this->configuration->getMetaFieldClass(); + $metaFieldName = $this->configuration->getMetaFieldName(); - if ($this->supportsMetaFields()) { + if ($metaFieldClass !== null && $metaFieldName !== null && $this->supportsMetaFields()) { $metaFieldRef = $rootAlias . '.' . $this->configuration->getEntityFieldName(); $i = 0; $c = 0; @@ -69,7 +71,7 @@ final class SearchHelper $and->add($qb->expr()->isNotNull($field)); } elseif ($metaValue === '~') { $and->add( - \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $this->configuration->getMetaFieldClass(), $subqueryName, $subqueryName, $this->configuration->getMetaFieldName(), $rootAlias, $subqueryName, $paramName) + \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $metaFieldClass, $subqueryName, $subqueryName, $metaFieldName, $rootAlias, $subqueryName, $paramName) ); } elseif ($metaValue === '') { $and->add( @@ -78,7 +80,7 @@ final class SearchHelper $qb->expr()->eq($alias . '.name', ':' . $paramName), $qb->expr()->isNull($field) ), - \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $this->configuration->getMetaFieldClass(), $subqueryName, $subqueryName, $this->configuration->getMetaFieldName(), $rootAlias, $subqueryName, $paramName) + \sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $metaFieldClass, $subqueryName, $subqueryName, $metaFieldName, $rootAlias, $subqueryName, $paramName) ) ); } else { diff --git a/src/Saml/SamlProvider.php b/src/Saml/SamlProvider.php index 185aaaa2..7cee3b52 100644 --- a/src/Saml/SamlProvider.php +++ b/src/Saml/SamlProvider.php @@ -54,7 +54,7 @@ final class SamlProvider } catch (\Exception $ex) { $this->logger->error($ex->getMessage()); 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() ?? '*unknown*', $ex->getMessage()) ); } diff --git a/src/Timesheet/Util.php b/src/Timesheet/Util.php index 663d5997..d9e6e786 100644 --- a/src/Timesheet/Util.php +++ b/src/Timesheet/Util.php @@ -23,8 +23,18 @@ final class Util */ public static function calculateRate(float $hourlyRate, int $seconds): float { - $rate = $hourlyRate * ($seconds / 3600); + $rate = $hourlyRate * round(($seconds / 3600), 2, PHP_ROUND_HALF_UP); - return round($rate, 4); + return round($rate, 2, PHP_ROUND_HALF_UP); + } + + /** + * Makes sure tha the duration is full compatible with decimal format, stripping away overflowing seconds. + */ + public static function decimalizeDuration(int $seconds): int + { + $decimal = round(($seconds / 3600), 2, PHP_ROUND_HALF_UP); + + return (int) round(($decimal * 3600), 0, PHP_ROUND_HALF_UP); } } diff --git a/src/Utils/Parsedown.php b/src/Utils/Parsedown.php index b3188459..d261bced 100644 --- a/src/Utils/Parsedown.php +++ b/src/Utils/Parsedown.php @@ -22,12 +22,15 @@ class Parsedown extends \Parsedown $block = parent::blockHeader($Line); $text = $block['element']['text']; - $id = $this->getIDfromText($text); - // add id-attribute - $block['element']['attributes'] = [ - 'id' => $id - ]; + if (\is_string($text) && $text !== '') { + $id = $this->getIDfromText($text); + + // add id-attribute + $block['element']['attributes'] = [ + 'id' => $id + ]; + } return $block; } @@ -67,7 +70,7 @@ class Parsedown extends \Parsedown return $text; } - protected function blockTable($Line, array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter + protected function blockTable($Line, ?array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter { $Block = parent::blockTable($Line, $Block); diff --git a/src/WorkingTime/Mode/WorkingTimeModeFactory.php b/src/WorkingTime/Mode/WorkingTimeModeFactory.php index 2b004936..c6acdd09 100644 --- a/src/WorkingTime/Mode/WorkingTimeModeFactory.php +++ b/src/WorkingTime/Mode/WorkingTimeModeFactory.php @@ -45,7 +45,7 @@ final class WorkingTimeModeFactory return $this->getMode($user->getWorkContractMode()); } catch (\InvalidArgumentException $ex) { $this->logger->error( - \sprintf('Unknown mode "%s" requested for user %s', $user->getWorkContractMode(), $user->getId()) + \sprintf('Unknown mode "%s" requested for user %s', $user->getWorkContractMode(), $user->getUserIdentifier()) ); return new WorkingTimeModeNone(); // @CloudRequired diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index 03803653..dab23688 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -422,12 +422,12 @@ class TimesheetControllerTest extends APIControllerBaseTestCase 'begin' => '2020-03-27T14:35:00+1300', 'end' => '2020-03-28T03:30:00+1300', 'description' => "**foo**\nbar", - 'duration' => 46500, + 'duration' => 46500, // 12,916 => rounded 12,92 * 137,21 = 46512 'exported' => true, 'metaFields' => [], 'hourlyRate' => 137.21, - 'rate' => 1772.2958, - 'internalRate' => 1772.2958, + 'rate' => 1772.75, // 12,92 * 137,21 + 'internalRate' => 1772.75, ]; foreach ($expected as $key => $value) { diff --git a/tests/API/ViewHandlerTest.php b/tests/API/ViewHandlerTest.php new file mode 100644 index 00000000..fe382d64 --- /dev/null +++ b/tests/API/ViewHandlerTest.php @@ -0,0 +1,72 @@ +createMock(ConfigurableViewHandlerInterface::class); + $base->expects($this->once())->method('supports')->willReturn(true); + $base->expects($this->exactly(2))->method('setExclusionStrategyGroups'); + $base->expects($this->once())->method('setExclusionStrategyVersion'); + $base->expects($this->once())->method('setSerializeNullStrategy'); + $base->expects($this->once())->method('registerHandler'); + $base->expects($this->once())->method('createRedirectResponse')->willReturn(new Response()); + $base->expects($this->once())->method('createResponse')->willReturn(new Response()); + $base->expects($this->once())->method('handle')->willReturn(new Response()); + + $sut = new ViewHandler($base); + self::assertTrue($sut->supports('asdf')); + $sut->setExclusionStrategyGroups(['bar', 'test']); + $sut->setExclusionStrategyGroups('foo'); + $sut->setExclusionStrategyVersion('1.0'); + $sut->setSerializeNullStrategy(true); + + $sut->registerHandler('bla', function () {}); + $response = $sut->createRedirectResponse(new View('bar123'), 'https://www.example.com', 'json'); + self::assertInstanceOf(Response::class, $response); + $response = $sut->createResponse(new View('bar123'), new Request(), 'json'); + self::assertInstanceOf(Response::class, $response); + + $results = ['foo' => 'bar', 'hello' => 'world']; + + $pagination = new Pagination(new ArrayAdapter($results)); + $view = new View($pagination); + $headers = $view->getHeaders(); + + self::assertSame($pagination, $view->getData()); + self::assertArrayNotHasKey('x-page', $headers); + self::assertArrayNotHasKey('x-total-count', $headers); + self::assertArrayNotHasKey('x-total-pages', $headers); + self::assertArrayNotHasKey('x-per-page', $headers); + + $response = $sut->handle($view, new Request()); + self::assertInstanceOf(Response::class, $response); + + $headers = $view->getHeaders(); + self::assertSame($results, $view->getData()); + self::assertArrayHasKey('x-page', $headers); + self::assertArrayHasKey('x-total-count', $headers); + self::assertArrayHasKey('x-total-pages', $headers); + self::assertArrayHasKey('x-per-page', $headers); + } +} diff --git a/tests/Export/ColumnConverterTest.php b/tests/Export/ColumnConverterTest.php index 26cf7db7..0978e65d 100644 --- a/tests/Export/ColumnConverterTest.php +++ b/tests/Export/ColumnConverterTest.php @@ -209,6 +209,7 @@ class ColumnConverterTest extends TestCase $template = new Template('bar', 'foo'); $template->setColumns([ + 'id', 'date', 'begin', 'end', @@ -252,6 +253,7 @@ class ColumnConverterTest extends TestCase $columns = $sut->getColumns($template, $query); $expected = [ + 'id', 'date', 'begin', 'end', diff --git a/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php b/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php index aef5ccd6..ab02e624 100644 --- a/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php @@ -68,13 +68,16 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTestCase $timesheet2 = new Timesheet(); $timesheet2->setBegin(new DateTime('2018-11-29')); $timesheet2->setEnd(new DateTime()); - $timesheet2->setDuration(400); + $timesheet2->setDuration(400); // 396 $timesheet2->setHourlyRate(293.27); - $timesheet2->setRate(84.75); + $timesheet2->setRate(84.75); // 32,26 $timesheet2->setUser($user); $timesheet2->setActivity((new Activity())->setName('bar')); $timesheet2->setProject($project2); + // 325,53 + // duration 4000 = 3996 + $timesheet3 = new Timesheet(); $timesheet3->setBegin(new DateTime('2018-11-28')); $timesheet3->setEnd(new DateTime()); @@ -85,25 +88,34 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTestCase $timesheet3->setActivity((new Activity())->setName('foo')); $timesheet3->setProject($project1); + // 325,53+111,11 + // duration 1800 + $timesheet4 = new Timesheet(); $timesheet4->setBegin(new DateTime('2018-11-28')); $timesheet4->setEnd(new DateTime()); - $timesheet4->setDuration(400); + $timesheet4->setDuration(400); // 396 $timesheet4->setHourlyRate(0); $timesheet4->setRate(1947.99); $timesheet4->setUser($user); $timesheet4->setActivity((new Activity())->setName('blub')); $timesheet4->setProject($project2); + // 325,53+111,11+1947,99 + // duration 400 + $timesheet5 = new Timesheet(); $timesheet5->setBegin(new DateTime('2018-11-28')); $timesheet5->setEnd(new DateTime()); - $timesheet5->setDuration(400); + $timesheet5->setDuration(400); // 396 $timesheet5->setRate(84); $timesheet5->setUser(new User()); $timesheet5->setActivity(new Activity()); $timesheet5->setProject($project3); + // 325,53+111,11+1947,99+84 + // duration 400 + $entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5]; $query = new InvoiceQuery(); @@ -115,12 +127,14 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTestCase $sut = $this->getCalculator(); $sut->setModel($model); + // (325,53+111,11+1947,99+84)*1,19 + self::assertEquals('price', $sut->getId()); - self::assertEquals(3000.13, $sut->getTotal()); + self::assertEquals(2937.67, $sut->getTotal()); $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); - self::assertEquals(2521.12, $sut->getSubtotal()); - self::assertEquals(6600, $sut->getTimeWorked()); + self::assertEquals(2468.63, $sut->getSubtotal()); + self::assertEquals(6596, $sut->getTimeWorked()); $entries = $sut->getEntries(); self::assertCount(4, $entries); @@ -130,7 +144,7 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('2018-11-28', $entries[2]->getBegin()?->format('Y-m-d')); self::assertEquals('2018-11-29', $entries[3]->getBegin()?->format('Y-m-d')); - self::assertEquals(378.02, $entries[3]->getRate()); + self::assertEquals(325.53, $entries[3]->getRate()); self::assertEquals(111.11, $entries[0]->getRate()); self::assertEquals(1947.99, $entries[1]->getRate()); self::assertEquals(84, $entries[2]->getRate()); diff --git a/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php index c3a3f388..1efa8bf6 100644 --- a/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php @@ -94,11 +94,11 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTestCase $sut->setModel($model); self::assertEquals('short', $sut->getId()); - self::assertEquals(562.28, $sut->getTotal()); + self::assertEquals(561.87, $sut->getTotal()); $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); - self::assertEquals(472.5, $sut->getSubtotal()); - self::assertEquals(5800, $sut->getTimeWorked()); + self::assertEquals(472.16, $sut->getSubtotal()); + self::assertEquals(5796, $sut->getTimeWorked()); self::assertEquals(1, \count($sut->getEntries())); $entries = $sut->getEntries(); @@ -109,8 +109,8 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('', $result->getDescription()); self::assertEquals(293.27, $result->getHourlyRate()); self::assertNull($result->getFixedRate()); - self::assertEquals(472.5, $result->getRate()); - self::assertEquals(5800, $result->getDuration()); + self::assertEquals(472.16, $result->getRate()); + self::assertEquals(5796, $result->getDuration()); self::assertEquals(3, $result->getAmount()); self::assertEquals(['foo', 'bar', 'bar1'], $result->getTags()); } diff --git a/tests/Invoice/InvoiceItemTest.php b/tests/Invoice/InvoiceItemTest.php index 8d231ae5..a275a424 100644 --- a/tests/Invoice/InvoiceItemTest.php +++ b/tests/Invoice/InvoiceItemTest.php @@ -25,7 +25,8 @@ class InvoiceItemTest extends TestCase self::assertNull($sut->getFixedRate()); self::assertNull($sut->getEnd()); self::assertEquals(0.00, $sut->getRate()); - self::assertEquals(0.00, $sut->getInternalRate()); + self::assertEquals(0.00, $sut->getInternalRate()); // @phpstan-ignore method.deprecated + self::assertEquals(0.00, $sut->getAppliedRate()); self::assertNull($sut->getProject()); self::assertIsArray($sut->getAdditionalFields()); self::assertEmpty($sut->getAdditionalFields()); @@ -53,4 +54,23 @@ class InvoiceItemTest extends TestCase $sut->addTag('foo1'); self::assertEquals(['foo', 'foo1', 'BaR'], $sut->getTags()); } + + public function testRates(): void + { + $sut = new InvoiceItem(); + + self::assertEquals(0.00, $sut->getHourlyRate()); + self::assertNull($sut->getFixedRate()); + self::assertEquals(0.00, $sut->getAppliedRate()); + + $sut->setHourlyRate(13.50); + self::assertEquals(13.50, $sut->getHourlyRate()); + self::assertNull($sut->getFixedRate()); + self::assertEquals(13.50, $sut->getAppliedRate()); + + $sut->setFixedRate(30.24); + self::assertEquals(13.50, $sut->getHourlyRate()); + self::assertEquals(30.24, $sut->getFixedRate()); + self::assertEquals(30.24, $sut->getAppliedRate()); + } } diff --git a/tests/Repository/Query/BaseQueryTest.php b/tests/Repository/Query/BaseQueryTest.php index 5204baec..7a6a8912 100644 --- a/tests/Repository/Query/BaseQueryTest.php +++ b/tests/Repository/Query/BaseQueryTest.php @@ -19,6 +19,7 @@ use App\Repository\Query\ActivityQuery; use App\Repository\Query\BaseQuery; use App\Repository\Query\DateRangeInterface; use App\Repository\Query\TimesheetQuery; +use App\Repository\Query\VisibilityInterface; use App\Utils\SearchTerm; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -342,6 +343,20 @@ class BaseQueryTest extends TestCase self::assertEquals([13, 27], $sut->getProjectIds()); } + protected function assertVisibility(VisibilityInterface $sut): void + { + self::assertEquals(VisibilityInterface::SHOW_VISIBLE, $sut->getVisibility()); + + $sut->setVisibility(VisibilityInterface::SHOW_BOTH); + self::assertEquals(VisibilityInterface::SHOW_BOTH, $sut->getVisibility()); + + $sut->setVisibility(VisibilityInterface::SHOW_HIDDEN); + self::assertEquals(VisibilityInterface::SHOW_HIDDEN, $sut->getVisibility()); + + $sut->setVisibility(VisibilityInterface::SHOW_VISIBLE); + self::assertEquals(VisibilityInterface::SHOW_VISIBLE, $sut->getVisibility()); + } + protected function assertDateRangeTrait(DateRangeInterface $sut): void { self::assertNull($sut->getBegin()); diff --git a/tests/Repository/Query/CustomerQueryTest.php b/tests/Repository/Query/CustomerQueryTest.php index cb41f650..f8869f75 100644 --- a/tests/Repository/Query/CustomerQueryTest.php +++ b/tests/Repository/Query/CustomerQueryTest.php @@ -20,6 +20,7 @@ class CustomerQueryTest extends BaseQueryTest $sut = new CustomerQuery(); $this->assertBaseQuery($sut, 'name'); + $this->assertVisibility($sut); $this->assertResetByFormError(new CustomerQuery(), 'name'); } } diff --git a/tests/Repository/Query/ProjectQueryTest.php b/tests/Repository/Query/ProjectQueryTest.php index c70d34be..84f43eb4 100644 --- a/tests/Repository/Query/ProjectQueryTest.php +++ b/tests/Repository/Query/ProjectQueryTest.php @@ -22,6 +22,7 @@ class ProjectQueryTest extends BaseQueryTest $sut = new ProjectQuery(); $this->assertBaseQuery($sut, 'name'); + $this->assertVisibility($sut); $this->assertCustomer($sut); $this->assertResetByFormError(new ProjectQuery(), 'name'); diff --git a/tests/Repository/Query/TagQueryTest.php b/tests/Repository/Query/TagQueryTest.php index 8e05000d..3cf0ce57 100644 --- a/tests/Repository/Query/TagQueryTest.php +++ b/tests/Repository/Query/TagQueryTest.php @@ -20,6 +20,7 @@ class TagQueryTest extends BaseQueryTest $sut = new TagQuery(); $this->assertBaseQuery($sut, 'name'); + $this->assertVisibility($sut); $this->assertResetByFormError(new TagQuery(), 'name'); } } diff --git a/tests/Repository/Query/UserQueryTest.php b/tests/Repository/Query/UserQueryTest.php index c286d641..7e4ccf66 100644 --- a/tests/Repository/Query/UserQueryTest.php +++ b/tests/Repository/Query/UserQueryTest.php @@ -20,6 +20,7 @@ class UserQueryTest extends BaseQueryTest { $sut = new UserQuery(); $this->assertBaseQuery($sut, 'username'); + $this->assertVisibility($sut); $this->assertRole($sut); $this->assertSearchTeam($sut); $this->assertResetByFormError(new UserQuery(), 'username'); diff --git a/tests/Timesheet/Calculator/RateCalculatorTest.php b/tests/Timesheet/Calculator/RateCalculatorTest.php index 3ba341aa..eac1a24f 100644 --- a/tests/Timesheet/Calculator/RateCalculatorTest.php +++ b/tests/Timesheet/Calculator/RateCalculatorTest.php @@ -28,7 +28,7 @@ use PHPUnit\Framework\TestCase; #[CoversClass(RateCalculator::class)] class RateCalculatorTest extends TestCase { - protected function getRateRepositoryMock(array $rates = []) + protected function getRateRepositoryMock(array $rates = []): TimesheetRepository { $mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock(); if (!empty($rates)) { @@ -38,18 +38,26 @@ class RateCalculatorTest extends TestCase return $mock; } - public function testCalculateWithTimesheetHourlyRate(): void + private function assertRateByTimesheetHourlyRate(int $duration, float $hourlyRate, float $rate): void { $record = new Timesheet(); $record->setEnd(new \DateTime()); - $record->setDuration(1800); - $record->setHourlyRate(100); + $record->setDuration($duration); + $record->setHourlyRate($hourlyRate); $record->setActivity(new Activity()); $record->setUser($this->getTestUser()); $sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock())); $sut->calculate($record, []); - self::assertEquals(50, $record->getRate()); + self::assertEquals($rate, $record->getRate()); + } + + public function testCalculateWithTimesheetHourlyRate(): void + { + $this->assertRateByTimesheetHourlyRate(1800, 100, 50); + $this->assertRateByTimesheetHourlyRate(400, 100, 11); + $this->assertRateByTimesheetHourlyRate(1234, 100, 34); + $this->assertRateByTimesheetHourlyRate(2739, 100, 76); } public function testCalculateWithTimesheetFixedRate(): void @@ -176,7 +184,7 @@ class RateCalculatorTest extends TestCase self::assertEquals($expectedInternalRate, $timesheet->getInternalRate()); } - protected function getTestUser($rate = 75, $internalRate = 75) + protected function getTestUser(?float $rate = 75.0, ?float $internalRate = 75.0): User { $user = new User(); @@ -230,19 +238,19 @@ class RateCalculatorTest extends TestCase self::assertEquals($expectedRate, $record->getRate()); } - public static function getRuleDefinitions() + public static function getRuleDefinitions(): array { $start = new \DateTime('12:00:00', new \DateTimeZone('UTC')); $day = $start->format('l'); return [ [ - 31837, + 31837, // 31824 = 8,84 [], - 663.2708 + 663 ], [ - 31837, + 31837, // 31824 = 8,84 [ 'default' => [ 'days' => [$day], @@ -253,10 +261,10 @@ class RateCalculatorTest extends TestCase 'factor' => 1.5 ], ], - 1326.5417 + 1326 // 8,84 * 75 (see user) * 2 ], [ - 31837, + 31837, // 31824 = 8,84 [ 'default' => [ 'days' => [$day], @@ -267,7 +275,7 @@ class RateCalculatorTest extends TestCase 'factor' => 1.5 ], ], - 2321.4479 + 2320.5 // 75 * 8,84 * 3,5 ], ]; } diff --git a/tests/Timesheet/RateServiceTest.php b/tests/Timesheet/RateServiceTest.php index e0e3e595..d26b399a 100644 --- a/tests/Timesheet/RateServiceTest.php +++ b/tests/Timesheet/RateServiceTest.php @@ -27,7 +27,7 @@ use PHPUnit\Framework\TestCase; #[CoversClass(RateService::class)] class RateServiceTest extends TestCase { - protected function getRateRepositoryMock(array $rates = []) + protected function getRateRepositoryMock(array $rates = []): TimesheetRepository { $mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock(); if (!empty($rates)) { @@ -37,7 +37,7 @@ class RateServiceTest extends TestCase return $mock; } - private static function createDateTime(string $datetime = null): \DateTime + private static function createDateTime(?string $datetime = null): \DateTime { return new \DateTime($datetime ?? 'now', new \DateTimeZone('UTC')); } @@ -124,7 +124,7 @@ class RateServiceTest extends TestCase $customerRate, $customerInternal, $customerIsFixed - ) { + ): void { $customer = new Customer('foo'); $project = new Project(); @@ -180,7 +180,7 @@ class RateServiceTest extends TestCase self::assertEquals($expectedInternalRate, $rate->getInternalRate()); } - protected function getTestUser($rate = 75, $internalRate = 75) + protected function getTestUser(?float $rate = 75.0, ?float $internalRate = 75.0): User { $user = new User(); @@ -234,7 +234,7 @@ class RateServiceTest extends TestCase self::assertEquals($expectedRate, $rate->getRate()); } - public static function getRuleDefinitions() + public static function getRuleDefinitions(): array { $start = self::createDateTime('12:00:00'); $day = $start->format('l'); @@ -243,7 +243,7 @@ class RateServiceTest extends TestCase [ 31837, [], - 663.2708 + 663 ], [ 31837, @@ -257,7 +257,7 @@ class RateServiceTest extends TestCase 'factor' => 1.5 ], ], - 1326.5417 + 1326 // 8,84 * 75 (see user) * 2 ], [ 31837, @@ -271,7 +271,7 @@ class RateServiceTest extends TestCase 'factor' => 1.5 ], ], - 2321.4479 + 2320.5 // 75 * 8,84 * 3,5 ], ]; } diff --git a/tests/Timesheet/UtilTest.php b/tests/Timesheet/UtilTest.php index b9850a49..75e1252e 100644 --- a/tests/Timesheet/UtilTest.php +++ b/tests/Timesheet/UtilTest.php @@ -18,23 +18,28 @@ use PHPUnit\Framework\TestCase; class UtilTest extends TestCase { #[DataProvider('getRateCalculationData')] - public function testCalculateRate(int|float $hourlyRate, int $duration, int|float $expectedRate): void + public function testCalculateRate(float $hourlyRate, int $duration, float $expectedRate): void { self::assertEquals($expectedRate, Util::calculateRate($hourlyRate, $duration)); } + /** + * @return array>|\Generator + */ public static function getRateCalculationData() { - yield [0, 0, 0]; - yield [1, 100, 0.0278]; - yield [1, 900, 0.25]; - yield [1, 1800, 0.5]; - yield [10000, 1, 2.7778]; - yield [736, 123, 25.1467]; - yield [7360, 1234, 2522.8444]; - yield [7360.34, 1234, 2522.961]; - yield [7360.01, 1234, 2522.8479]; - yield [7360.99, 1234, 2523.1838]; + yield [0.00, 0, 0.00]; + yield [10.00, 7260, 20.2]; + yield [1.00, 3600, 1.00]; + yield [1.00, 100, 0.03]; + yield [1.00, 900, 0.25]; + yield [1.00, 1800, 0.5]; + yield [10000.00, 60, 200.00]; + yield [736.00, 123, 22.08]; + yield [7360.00, 1234, 2502.4]; + yield [7360.34, 1234, 2502.52]; + yield [7360.01, 1234, 2502.4]; + yield [7360.99, 1234, 2502.74]; } public function testCalculateRateWithRounding(): void @@ -43,29 +48,37 @@ class UtilTest extends TestCase $seconds = 0; $repeat = 130; - for ($a = 0; $a < $repeat; $a++) { - $inputs = [ - 900, - 1600, - 4200, - 8763, - 3300, - 600, - 1300, - 1837, - 4217, - 5400, - 3283, - 600, - ]; + $inputs = [ + [900, 28.69, 0], + [1600, 50.49, 0], + [4200, 134.26, 0], + [8763, 278.84, 0], + [3300, 105.57, 0], + [600, 19.51, 0], + [1300, 41.31, 0], + [1837, 58.52, 0], + [4217, 134.26, 0], + [5400, 172.13, 0], + [3283, 104.42, 0], + [600, 19.51, 0], + ]; - foreach ($inputs as $i) { - $seconds += $i; - $total += Util::calculateRate(114.75, $i); + $totalExpected = 0.00; + + for ($a = 0; $a < $repeat; $a++) { + foreach ($inputs as $row) { + [$duration, $rate] = $row; + $seconds += $duration; + $totalExpected += $rate; + $tmp = Util::calculateRate(114.75, $duration); + self::assertEquals($rate, $tmp); + $total += $tmp; } } self::assertEquals(36000 * $repeat, $seconds); - self::assertEquals(1147.50 * $repeat, $total); + self::assertEquals($totalExpected, $total); + self::assertEqualsWithDelta(1147.51 * $repeat, $total, 0.00001); + self::assertEqualsWithDelta(149176.3, $total, 0.00001); } } diff --git a/tests/Utils/ProfileManagerTest.php b/tests/Utils/ProfileManagerTest.php index afd5a6f5..5f19a59d 100644 --- a/tests/Utils/ProfileManagerTest.php +++ b/tests/Utils/ProfileManagerTest.php @@ -31,7 +31,10 @@ class ProfileManagerTest extends TestCase self::assertEquals(ProfileManager::PROFILE_DESKTOP, $sut->getProfileFromSession($session)); } - public static function getInvalidProfiles() + /** + * @return array> + */ + public static function getInvalidProfiles(): array { return [ ['MOBILE'], @@ -52,7 +55,10 @@ class ProfileManagerTest extends TestCase self::assertFalse($sut->isValidProfile($profile)); } - public static function getDatatableNames() + /** + * @return array> + */ + public static function getDatatableNames(): array { return [ ['admin-timesheet_mobile', 'admin-timesheet', 'mobile'], @@ -73,7 +79,10 @@ class ProfileManagerTest extends TestCase self::assertEquals($expected, $sut->getDatatableName($datatable, $prefix)); } - public static function getProfileNames() + /** + * @return array> + */ + public static function getProfileNames(): array { return [ ['mobile', ProfileManager::PROFILE_MOBILE], @@ -119,7 +128,10 @@ class ProfileManagerTest extends TestCase self::assertNull($session->get(ProfileManager::SESSION_PROFILE)); } - public static function getCookieProfiles() + /** + * @return array> + */ + public static function getCookieProfiles(): array { return [ ['mobile', ProfileManager::PROFILE_MOBILE], @@ -146,7 +158,10 @@ class ProfileManagerTest extends TestCase self::assertEquals($expected, $profile); } - public static function getSessionProfiles() + /** + * @return array> + */ + public static function getSessionProfiles(): array { return [ ['mobile', ProfileManager::PROFILE_MOBILE], diff --git a/tests/WorkingTime/Mode/WorkingTimeModeFactoryTest.php b/tests/WorkingTime/Mode/WorkingTimeModeFactoryTest.php index a62d260c..beab52d5 100644 --- a/tests/WorkingTime/Mode/WorkingTimeModeFactoryTest.php +++ b/tests/WorkingTime/Mode/WorkingTimeModeFactoryTest.php @@ -44,6 +44,7 @@ class WorkingTimeModeFactoryTest extends TestCase $sut = new WorkingTimeModeFactory($modes, $logger); $user = new User(); + $user->setUsername('foo-bar'); $user->setWorkContractMode('foo'); self::assertInstanceOf(WorkingTimeModeNone::class, $sut->getModeForUser($user)); } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 1b13e776..16b2b0a6 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1965,11 +1965,6 @@ parameters: count: 1 path: Timesheet/Calculator/DurationCalculatorTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateRepositoryMock\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#" count: 1 @@ -1980,26 +1975,6 @@ parameters: count: 1 path: Timesheet/Calculator/RateCalculatorTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRuleDefinitions\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getTestUser\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getTestUser\\(\\) has parameter \\$internalRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getTestUser\\(\\) has parameter \\$rate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#" count: 1 @@ -2120,11 +2095,6 @@ parameters: count: 1 path: Timesheet/LockdownServiceTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateRepositoryMock\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#" count: 1 @@ -2135,26 +2105,6 @@ parameters: count: 1 path: Timesheet/RateServiceTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRuleDefinitions\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getTestUser\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getTestUser\\(\\) has parameter \\$internalRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getTestUser\\(\\) has parameter \\$rate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#" count: 1 @@ -2170,11 +2120,6 @@ parameters: count: 1 path: Timesheet/RateServiceTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityInternal with no type specified\\.$#" count: 1 @@ -2435,11 +2380,6 @@ parameters: count: 1 path: Timesheet/TrackingMode/DurationFixedBeginModeTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\UtilTest\\:\\:getRateCalculationData\\(\\) has no return type specified\\.$#" - count: 1 - path: Timesheet/UtilTest.php - - message: "#^Cannot access property \\$file on SimpleXMLElement\\|false\\.$#" count: 4 @@ -2675,31 +2615,6 @@ parameters: count: 1 path: Utils/FormFormatConverterTest.php - - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getCookieProfiles\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/ProfileManagerTest.php - - - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getDatatableNames\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/ProfileManagerTest.php - - - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getInvalidProfiles\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/ProfileManagerTest.php - - - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getProfileNames\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/ProfileManagerTest.php - - - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getSessionProfiles\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/ProfileManagerTest.php - - message: "#^Method App\\\\Tests\\\\Utils\\\\StringHelperTest\\:\\:getDdeAttackStrings\\(\\) has no return type specified\\.$#" count: 1 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index f454bb41..a9762e8e 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -2042,6 +2042,10 @@ payment_account_name IBAN oder Kartennummer + + electronic_invoice + E-Rechnung + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 01df16e9..2b75533e 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -2042,6 +2042,10 @@ payment_account_name IBAN or card number + + electronic_invoice + E-Invoice +