Release 2.41 (#5653)
This commit is contained in:
@@ -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",
|
||||
|
||||
253
composer.lock
generated
253
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $pagination);
|
||||
|
||||
return $view;
|
||||
if (\array_key_exists('orderBy', $all)) {
|
||||
$orderBy = $all['orderBy'];
|
||||
if (\is_string($orderBy) && $orderBy !== '') {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string> $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') {
|
||||
|
||||
78
src/API/ViewHandler.php
Normal file
78
src/API/ViewHandler.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\View\ConfigurableViewHandlerInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ViewHandler implements ConfigurableViewHandlerInterface
|
||||
{
|
||||
public function __construct(private readonly ConfigurableViewHandlerInterface $baseViewHandler)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $groups
|
||||
*/
|
||||
public function setExclusionStrategyGroups($groups): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ final class QuickEntryController extends AbstractController
|
||||
$endWeek = $factory->getEndOfWeek($begin);
|
||||
|
||||
$tmpDay = clone $startWeek;
|
||||
/** @var array<string, array{day: \DateTime}> $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));
|
||||
}
|
||||
|
||||
@@ -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++);
|
||||
|
||||
@@ -153,18 +153,18 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
|
||||
$qb = $manager->getRepository($class)->createQueryBuilder('entity');
|
||||
|
||||
/** @var array<int, T> $all */
|
||||
$all = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult();
|
||||
/** @var array<int, T> $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<int|string, Tag>
|
||||
* @return non-empty-array<int|string, Tag>
|
||||
*/
|
||||
private function getAllTags(ObjectManager $manager): array
|
||||
{
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -49,6 +49,7 @@ final class ExportColumnsType extends AbstractType
|
||||
{
|
||||
$columns = [
|
||||
'timesheet' => [
|
||||
'id' => 'id',
|
||||
'date' => 'date',
|
||||
'begin' => 'begin',
|
||||
'end' => 'end',
|
||||
|
||||
@@ -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<InvoiceItem> $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();
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -23,7 +23,7 @@ final class DefaultCalculator extends AbstractMergedCalculator implements Calcul
|
||||
/**
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
protected function calculateEntries(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
class TagQuery extends BaseQuery
|
||||
class TagQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
|
||||
|
||||
@@ -20,11 +20,10 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,15 @@ class Parsedown extends \Parsedown
|
||||
$block = parent::blockHeader($Line);
|
||||
|
||||
$text = $block['element']['text'];
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
72
tests/API/ViewHandlerTest.php
Normal file
72
tests/API/ViewHandlerTest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\API;
|
||||
|
||||
use App\API\ViewHandler;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\View\ConfigurableViewHandlerInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
#[CoversClass(ViewHandler::class)]
|
||||
class ViewHandlerTest extends TestCase
|
||||
{
|
||||
public function testValues(): void
|
||||
{
|
||||
$base = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -20,6 +20,7 @@ class CustomerQueryTest extends BaseQueryTest
|
||||
$sut = new CustomerQuery();
|
||||
|
||||
$this->assertBaseQuery($sut, 'name');
|
||||
$this->assertVisibility($sut);
|
||||
$this->assertResetByFormError(new CustomerQuery(), 'name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class TagQueryTest extends BaseQueryTest
|
||||
$sut = new TagQuery();
|
||||
|
||||
$this->assertBaseQuery($sut, 'name');
|
||||
$this->assertVisibility($sut);
|
||||
$this->assertResetByFormError(new TagQuery(), 'name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<int, array<float, int, >>|\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,
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ class ProfileManagerTest extends TestCase
|
||||
self::assertEquals(ProfileManager::PROFILE_DESKTOP, $sut->getProfileFromSession($session));
|
||||
}
|
||||
|
||||
public static function getInvalidProfiles()
|
||||
/**
|
||||
* @return array<int, array<string>>
|
||||
*/
|
||||
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<int, array<string|null>>
|
||||
*/
|
||||
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<int, array<string>>
|
||||
*/
|
||||
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<int, array<string>>
|
||||
*/
|
||||
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<int, array<string>>
|
||||
*/
|
||||
public static function getSessionProfiles(): array
|
||||
{
|
||||
return [
|
||||
['mobile', ProfileManager::PROFILE_MOBILE],
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2042,6 +2042,10 @@
|
||||
<source>payment_account_name</source>
|
||||
<target>IBAN oder Kartennummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="jcT9sdfOg6" resname="electronic_invoice">
|
||||
<source>electronic_invoice</source>
|
||||
<target>E-Rechnung</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -2042,6 +2042,10 @@
|
||||
<source>payment_account_name</source>
|
||||
<target>IBAN or card number</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="jcT9sdfOg6" resname="electronic_invoice">
|
||||
<source>electronic_invoice</source>
|
||||
<target>E-Invoice</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user