From cc64acf0f8e4d6866b8f645695842954f0c3a8a2 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 2 Nov 2025 11:24:17 +0100 Subject: [PATCH] Meta-fields for InvoiceTemplate, structured Customer address (#5519) --- composer.json | 1 + composer.lock | 1221 +++++++++++------ migrations/Version20251031142000.php | 46 + migrations/Version20251031143000.php | 43 + phpstan.neon | 74 +- src/Controller/InvoiceController.php | 6 +- src/Controller/QuickEntryController.php | 2 +- src/DataFixtures/CustomerFixtures.php | 4 + src/DataFixtures/InvoiceFixtures.php | 13 +- src/DataFixtures/TeamFixtures.php | 18 +- src/DataFixtures/TimesheetFixtures.php | 29 +- src/Entity/Customer.php | 140 +- src/Entity/Invoice.php | 6 - src/Entity/InvoiceTemplate.php | 172 ++- src/Entity/InvoiceTemplateMeta.php | 49 + src/Entity/Tax.php | 36 + src/Entity/TaxType.php | 17 + .../InvoiceTemplateMetaDefinitionEvent.php | 28 + src/Form/CustomerEditForm.php | 36 +- src/Form/InvoiceTemplateForm.php | 20 +- src/Form/MultiUpdate/MultiUpdateTable.php | 8 +- src/Form/MultiUpdate/TimesheetMultiUpdate.php | 8 +- src/Form/Type/DateRangeType.php | 2 +- src/Invoice/Calculator/AbstractCalculator.php | 29 +- src/Invoice/CalculatorInterface.php | 18 +- .../Hydrator/InvoiceModelCustomerHydrator.php | 18 +- .../Hydrator/InvoiceModelDefaultHydrator.php | 50 +- src/Invoice/InvoiceModel.php | 40 +- src/Invoice/InvoiceModelFactory.php | 4 +- .../ConfigurableNumberGenerator.php | 8 +- src/Invoice/Renderer/DocxRenderer.php | 5 + src/Invoice/TaxRow.php | 39 + src/Pdf/MPdfConverter.php | 12 +- src/Utils/Context.php | 4 +- templates/customer/details.html.twig | 23 +- templates/customer/edit.html.twig | 81 +- templates/invoice/renderer/default.pdf.twig | 10 + templates/invoice/renderer/invoice.html.twig | 10 + .../invoice/renderer/service-date.pdf.twig | 16 + templates/invoice/template_edit.html.twig | 27 +- templates/project/edit.html.twig | 40 +- tests/API/APIControllerBaseTestCase.php | 6 + tests/Controller/InvoiceControllerTest.php | 12 +- .../DataFixtures/InvoiceTemplateFixtures.php | 18 +- tests/Entity/CustomerTest.php | 58 + tests/Entity/InvoiceTemplateMetaTest.php | 40 + tests/Entity/InvoiceTemplateTest.php | 105 +- ...InvoiceTemplateMetaDefinitionEventTest.php | 26 + .../Spreadsheet/CustomerExporterTest.php | 15 +- .../Calculator/AbstractCalculatorTestCase.php | 13 +- .../ActivityInvoiceCalculatorTest.php | 2 +- .../ActivityUserInvoiceCalculatorTest.php | 2 +- .../Calculator/DateInvoiceCalculatorTest.php | 2 +- .../DateUserInvoiceCalculatorTest.php | 2 +- .../Calculator/DefaultCalculatorTest.php | 2 +- .../Calculator/PriceInvoiceCalculatorTest.php | 2 +- .../ProjectInvoiceCalculatorTest.php | 2 +- .../ProjectUserInvoiceCalculatorTest.php | 2 +- .../Calculator/ShortInvoiceCalculatorTest.php | 6 +- .../Calculator/UserInvoiceCalculatorTest.php | 2 +- .../WeeklyInvoiceCalculatorTest.php | 2 +- .../InvoiceModelCustomerHydratorTest.php | 14 + .../InvoiceModelDefaultHydratorTest.php | 5 +- tests/Invoice/InvoiceModelTest.php | 2 - tests/Invoice/Renderer/DebugRendererTest.php | 8 + tests/Invoice/Renderer/RendererTestTrait.php | 2 + tests/Invoice/Renderer/TwigRendererTest.php | 4 +- tests/Pdf/MPdfConverterTest.php | 12 +- tests/Utils/JavascriptFormatConverterTest.php | 2 +- tests/phpstan.neon | 15 - translations/messages.de.xlf | 58 +- translations/messages.en.xlf | 58 +- 72 files changed, 2111 insertions(+), 801 deletions(-) create mode 100644 migrations/Version20251031142000.php create mode 100644 migrations/Version20251031143000.php create mode 100644 src/Entity/InvoiceTemplateMeta.php create mode 100644 src/Entity/Tax.php create mode 100644 src/Entity/TaxType.php create mode 100644 src/Event/InvoiceTemplateMetaDefinitionEvent.php create mode 100644 src/Invoice/TaxRow.php create mode 100644 tests/Entity/InvoiceTemplateMetaTest.php create mode 100644 tests/Event/InvoiceTemplateMetaDefinitionEventTest.php diff --git a/composer.json b/composer.json index 39819f8e..1e7d277f 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "erusev/parsedown": "^1.6", "friendsofsymfony/rest-bundle": "^3.0", "gedmo/doctrine-extensions": "^3.6", + "horstoeko/zugferd": "^1.0", "jms/serializer-bundle": "^5.0", "kevinpapst/tabler-bundle": "^1.4", "league/csv": "^9.4", diff --git a/composer.lock b/composer.lock index f17b68a4..4a775c35 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05802c03af8f523624d93297a3a658eb", + "content-hash": "5de74433dcf1cf3ad650d7643b57e24f", "packages": [ { "name": "azuyalabs/yasumi", @@ -431,20 +431,21 @@ "type": "tidelift" } ], + "abandoned": true, "time": "2022-05-20T20:07:39+00:00" }, { "name": "doctrine/collections", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + "reference": "9acfeea2e8666536edff3d77c531261c63680160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", - "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "url": "https://api.github.com/repos/doctrine/collections/zipball/9acfeea2e8666536edff3d77c531261c63680160", + "reference": "9acfeea2e8666536edff3d77c531261c63680160", "shasum": "" }, "require": { @@ -453,11 +454,11 @@ "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^14", "ext-json": "*", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5" + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" }, "type": "library", "autoload": { @@ -501,7 +502,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.3.0" + "source": "https://github.com/doctrine/collections/tree/2.4.0" }, "funding": [ { @@ -517,7 +518,7 @@ "type": "tidelift" } ], - "time": "2025-03-22T10:17:19+00:00" + "time": "2025-10-25T09:18:13+00:00" }, { "name": "doctrine/common", @@ -612,16 +613,16 @@ }, { "name": "doctrine/dbal", - "version": "3.10.2", + "version": "3.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", - "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43", + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43", "shasum": "" }, "require": { @@ -637,14 +638,14 @@ }, "require-dev": { "doctrine/cache": "^1.11|^2.0", - "doctrine/coding-standard": "13.0.1", + "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.22", + "phpstan/phpstan": "2.1.30", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.23", - "slevomat/coding-standard": "8.16.2", - "squizlabs/php_codesniffer": "3.13.1", + "phpunit/phpunit": "9.6.29", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -706,7 +707,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.2" + "source": "https://github.com/doctrine/dbal/tree/3.10.3" }, "funding": [ { @@ -722,7 +723,7 @@ "type": "tidelift" } ], - "time": "2025-09-04T23:51:27+00:00" + "time": "2025-10-09T09:05:12+00:00" }, { "name": "doctrine/deprecations", @@ -774,20 +775,21 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.16.2", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f" + "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/1c10de0fe995f01eca6b073d1c2549ef0b603a7f", - "reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", + "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", "shasum": "" }, "require": { "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/deprecations": "^1.0", "doctrine/persistence": "^3.1 || ^4", "doctrine/sql-formatter": "^1.0.1", "php": "^8.1", @@ -795,7 +797,6 @@ "symfony/config": "^6.4 || ^7.0", "symfony/console": "^6.4 || ^7.0", "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/deprecation-contracts": "^2.1 || ^3", "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", "symfony/framework-bundle": "^6.4 || ^7.0", "symfony/service-contracts": "^2.5 || ^3" @@ -810,14 +811,13 @@ "require-dev": { "doctrine/annotations": "^1 || ^2", "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^13", - "doctrine/deprecations": "^1.0", + "doctrine/coding-standard": "^14", "doctrine/orm": "^2.17 || ^3.1", "friendsofphp/proxy-manager-lts": "^1.0", "phpstan/phpstan": "2.1.1", "phpstan/phpstan-phpunit": "2.0.3", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^10.5.53", + "phpunit/phpunit": "^10.5.53 || ^12.3.10", "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/doctrine-messenger": "^6.4 || ^7.0", "symfony/expression-language": "^6.4 || ^7.0", @@ -831,7 +831,7 @@ "symfony/var-exporter": "^6.4.1 || ^7.0.1", "symfony/web-profiler-bundle": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0", - "twig/twig": "^2.13 || ^3.0.4" + "twig/twig": "^2.14.7 || ^3.0.4" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -876,7 +876,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.16.2" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.0" }, "funding": [ { @@ -892,24 +892,24 @@ "type": "tidelift" } ], - "time": "2025-09-10T19:14:48+00:00" + "time": "2025-10-11T04:43:27+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.4.2", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9" + "reference": "71c81279ca0e907c3edc718418b93fd63074856c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9", - "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/71c81279ca0e907c3edc718418b93fd63074856c", + "reference": "71c81279ca0e907c3edc718418b93fd63074856c", "shasum": "" }, "require": { - "doctrine/doctrine-bundle": "^2.4", + "doctrine/doctrine-bundle": "^2.4 || ^3.0", "doctrine/migrations": "^3.2", "php": "^7.2 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", @@ -917,7 +917,7 @@ }, "require-dev": { "composer/semver": "^3.0", - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^12 || ^14", "doctrine/orm": "^2.6 || ^3", "phpstan/phpstan": "^1.4 || ^2", "phpstan/phpstan-deprecation-rules": "^1 || ^2", @@ -961,7 +961,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.2" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.5.0" }, "funding": [ { @@ -977,7 +977,7 @@ "type": "tidelift" } ], - "time": "2025-03-11T17:36:26+00:00" + "time": "2025-10-12T17:06:40+00:00" }, { "name": "doctrine/event-manager", @@ -1412,16 +1412,16 @@ }, { "name": "doctrine/orm", - "version": "2.20.6", + "version": "2.20.7", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "c322c71cd40da12d255dabd7b6ce0d9cf208a5d5" + "reference": "59938cae57c88b386cb79c81685426c83d27a120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/c322c71cd40da12d255dabd7b6ce0d9cf208a5d5", - "reference": "c322c71cd40da12d255dabd7b6ce0d9cf208a5d5", + "url": "https://api.github.com/repos/doctrine/orm/zipball/59938cae57c88b386cb79c81685426c83d27a120", + "reference": "59938cae57c88b386cb79c81685426c83d27a120", "shasum": "" }, "require": { @@ -1448,14 +1448,13 @@ }, "require-dev": { "doctrine/annotations": "^1.13 || ^2", - "doctrine/coding-standard": "^9.0.2 || ^13.0", + "doctrine/coding-standard": "^9.0.2 || ^14.0", "phpbench/phpbench": "^0.16.10 || ^1.0", "phpstan/extension-installer": "~1.1.0 || ^1.4", - "phpstan/phpstan": "~1.4.10 || 2.0.3", + "phpstan/phpstan": "~1.4.10 || 2.1.22", "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.12.0", "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" @@ -1508,22 +1507,22 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.20.6" + "source": "https://github.com/doctrine/orm/tree/2.20.7" }, - "time": "2025-08-08T06:55:44+00:00" + "time": "2025-10-27T21:19:59+00:00" }, { "name": "doctrine/persistence", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + "reference": "d59e6ef7caffe6a30f4b6f9e9079a75f52c64ae0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/d59e6ef7caffe6a30f4b6f9e9079a75f52c64ae0", + "reference": "d59e6ef7caffe6a30f4b6f9e9079a75f52c64ae0", "shasum": "" }, "require": { @@ -1535,11 +1534,11 @@ "doctrine/common": "<2.10" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^12 || ^14", "doctrine/common": "^3.0", - "phpstan/phpstan": "1.12.7", - "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan": "^1 || 2.1.30", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", "phpunit/phpunit": "^8.5.38 || ^9.5", "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" }, @@ -1590,7 +1589,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.4.0" + "source": "https://github.com/doctrine/persistence/tree/3.4.3" }, "funding": [ { @@ -1606,30 +1605,30 @@ "type": "tidelift" } ], - "time": "2024-10-30T19:48:12+00:00" + "time": "2025-10-21T15:21:39+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5" + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" }, "bin": [ "bin/sql-formatter" @@ -1659,9 +1658,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3" }, - "time": "2025-01-24T11:45:48+00:00" + "time": "2025-10-26T09:35:14+00:00" }, { "name": "easybill/zugferd-php", @@ -2157,6 +2156,252 @@ ], "time": "2025-09-22T17:04:34+00:00" }, + { + "name": "goetas-webservices/xsd2php-runtime", + "version": "v0.2.17", + "source": { + "type": "git", + "url": "https://github.com/goetas-webservices/xsd2php-runtime.git", + "reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/goetas-webservices/xsd2php-runtime/zipball/be15c48cda6adfab82e180a69dfa1937e208cfe1", + "reference": "be15c48cda6adfab82e180a69dfa1937e208cfe1", + "shasum": "" + }, + "require": { + "jms/serializer": "^1.2|^2.0|^3.0", + "php": ">=7.1", + "symfony/yaml": "^2.2|^3.0|^4.0|^5.0|^6.0|^7.0" + }, + "conflict": { + "jms/serializer": "1.4.1|1.6.1|1.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.2-dev" + } + }, + "autoload": { + "psr-4": { + "GoetasWebservices\\Xsd\\XsdToPhpRuntime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic" + } + ], + "description": "Convert XSD (XML Schema) definitions into PHP classes", + "keywords": [ + "converter", + "jms", + "php", + "serializer", + "xml", + "xsd" + ], + "support": { + "issues": "https://github.com/goetas-webservices/xsd2php-runtime/issues", + "source": "https://github.com/goetas-webservices/xsd2php-runtime/tree/v0.2.17" + }, + "time": "2024-04-12T22:55:31+00:00" + }, + { + "name": "horstoeko/mimedb", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/horstoeko/mimedb.git", + "reference": "2d50f2b6bf63f14741514682869b53fc97232308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/horstoeko/mimedb/zipball/2d50f2b6bf63f14741514682869b53fc97232308", + "reference": "2d50f2b6bf63f14741514682869b53fc97232308", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "pdepend/pdepend": "^2", + "phploc/phploc": "^7", + "phpmd/phpmd": "^2", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9", + "sebastian/phpcpd": "^6", + "squizlabs/php_codesniffer": "^3" + }, + "type": "package", + "autoload": { + "psr-4": { + "horstoeko\\mimedb\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Erling", + "email": "daniel@erling.com.de", + "role": "lead" + } + ], + "description": "Get mimetypes by fileextensions and visa versa", + "homepage": "https://github.com/horstoeko/mimedb", + "keywords": [ + "file extension", + "mimetype" + ], + "support": { + "issues": "https://github.com/horstoeko/mimedb/issues", + "source": "https://github.com/horstoeko/mimedb/tree/v1.0.8" + }, + "time": "2024-12-03T10:57:28+00:00" + }, + { + "name": "horstoeko/stringmanagement", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/horstoeko/stringmanagement.git", + "reference": "d304a094e34c39d35f275bccf4944176dd8f1722" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/horstoeko/stringmanagement/zipball/d304a094e34c39d35f275bccf4944176dd8f1722", + "reference": "d304a094e34c39d35f275bccf4944176dd8f1722", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "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\\stringmanagement\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Erling", + "email": "daniel@erling.com.de", + "role": "lead" + } + ], + "description": "A library for string manipulation utilities", + "homepage": "https://github.com/horstoeko/stringmanagement", + "keywords": [ + "stringmanagement" + ], + "support": { + "issues": "https://github.com/horstoeko/stringmanagement/issues", + "source": "https://github.com/horstoeko/stringmanagement/tree/v1.0.12" + }, + "time": "2025-03-15T09:04:46+00:00" + }, + { + "name": "horstoeko/zugferd", + "version": "v1.0.116", + "source": { + "type": "git", + "url": "https://github.com/horstoeko/zugferd.git", + "reference": "23fe5478a394ed96e15aaaff7451742f33d36f02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/23fe5478a394ed96e15aaaff7451742f33d36f02", + "reference": "23fe5478a394ed96e15aaaff7451742f33d36f02", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "goetas-webservices/xsd2php-runtime": "^0.2.13", + "horstoeko/mimedb": "^1", + "horstoeko/stringmanagement": "^1", + "jms/serializer": "^3", + "php": ">=7.3", + "setasign/fpdf": "^1", + "setasign/fpdi": "^2", + "smalot/pdfparser": "^0|^2", + "symfony/finder": "^5|^6|^7", + "symfony/process": "^5|^6|^7", + "symfony/validator": "^5|^6|^7", + "symfony/yaml": "^5|^6|^7" + }, + "require-dev": { + "goetas-webservices/xsd2php": "^0", + "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\\zugferd\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Erling", + "email": "daniel@erling.com.de", + "role": "lead" + } + ], + "description": "A library for creating and reading european electronic invoices", + "homepage": "https://github.com/horstoeko/zugferd", + "keywords": [ + "ZUGFeRD", + "electronic", + "factur-x", + "invoice", + "xrechnung" + ], + "support": { + "issues": "https://github.com/horstoeko/zugferd/issues", + "source": "https://github.com/horstoeko/zugferd/tree/v1.0.116" + }, + "time": "2025-10-14T12:07:57+00:00" + }, { "name": "jms/metadata", "version": "2.8.0", @@ -2489,16 +2734,16 @@ }, { "name": "league/csv", - "version": "9.25.0", + "version": "9.27.1", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86" + "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/f856f532866369fb1debe4e7c5a1db185f40ef86", - "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/26de738b8fccf785397d05ee2fc07b6cd8749797", + "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797", "shasum": "" }, "require": { @@ -2576,7 +2821,7 @@ "type": "github" } ], - "time": "2025-09-11T08:29:08+00:00" + "time": "2025-10-25T08:35:20+00:00" }, { "name": "lorenzo/pinky", @@ -3153,16 +3398,16 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v5.6.2", + "version": "v5.6.5", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "9b7ece3141b74699008be55231d6907b5e8bc883" + "reference": "eb0453607560e63bbbc6a746978933f77a787575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/9b7ece3141b74699008be55231d6907b5e8bc883", - "reference": "9b7ece3141b74699008be55231d6907b5e8bc883", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/eb0453607560e63bbbc6a746978933f77a787575", + "reference": "eb0453607560e63bbbc6a746978933f77a787575", "shasum": "" }, "require": { @@ -3185,7 +3430,7 @@ "zircote/swagger-php": "^4.11.1 || ^5.0" }, "conflict": { - "zircote/swagger-php": "4.8.7" + "zircote/swagger-php": "4.8.7 || 5.5.0" }, "require-dev": { "api-platform/core": "^3.2", @@ -3193,10 +3438,10 @@ "friendsofsymfony/rest-bundle": "^3.2.0", "jms/serializer": "^3.32", "jms/serializer-bundle": "^5.5", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-phpunit": "^1.3", - "phpstan/phpstan-strict-rules": "^1.5", - "phpstan/phpstan-symfony": "^1.3", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", "phpunit/phpunit": "^10.5", "symfony/asset": "^6.4 || ^7.1", "symfony/browser-kit": "^6.4 || ^7.1", @@ -3264,7 +3509,7 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.6.2" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.6.5" }, "funding": [ { @@ -3272,29 +3517,32 @@ "type": "github" } ], - "time": "2025-09-19T12:23:35+00:00" + "time": "2025-10-20T08:34:48+00:00" }, { "name": "nelmio/cors-bundle", - "version": "2.5.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioCorsBundle.git", - "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" + "reference": "530217472204881cacd3671909f634b960c7b948" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", - "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/530217472204881cacd3671909f634b960c7b948", + "reference": "530217472204881cacd3671909f634b960c7b948", "shasum": "" }, "require": { "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "mockery/mockery": "^1.3.6", - "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + "phpstan/phpstan": "^1.11.5", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-symfony": "^1.4.4", + "phpunit/phpunit": "^8" }, "type": "symfony-bundle", "extra": { @@ -3332,22 +3580,22 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", - "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.0" }, - "time": "2024-06-24T21:25:28+00:00" + "time": "2025-10-23T06:57:22+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -3390,9 +3638,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "onelogin/php-saml", @@ -3656,16 +3904,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.8.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "ce27936c8dfb73e3ab9c94469130428af9752c96" + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/ce27936c8dfb73e3ab9c94469130428af9752c96", - "reference": "ce27936c8dfb73e3ab9c94469130428af9752c96", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", "shasum": "" }, "require": { @@ -3719,7 +3967,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2025-09-22T20:41:46+00:00" + "time": "2025-09-24T15:12:37+00:00" }, { "name": "paragonie/random_compat", @@ -4929,6 +5177,52 @@ }, "time": "2024-11-29T19:22:48+00:00" }, + { + "name": "setasign/fpdf", + "version": "1.8.6", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDF.git", + "reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0", + "reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-zlib": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "fpdf.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Olivier Plathey", + "email": "oliver@fpdf.org", + "homepage": "http://fpdf.org/" + } + ], + "description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.", + "homepage": "http://www.fpdf.org", + "keywords": [ + "fpdf", + "pdf" + ], + "support": { + "source": "https://github.com/Setasign/FPDF/tree/1.8.6" + }, + "time": "2023-06-26T14:44:25+00:00" + }, { "name": "setasign/fpdi", "version": "v2.6.4", @@ -5001,6 +5295,57 @@ ], "time": "2025-08-05T09:57:14+00:00" }, + { + "name": "smalot/pdfparser", + "version": "v2.12.1", + "source": { + "type": "git", + "url": "https://github.com/smalot/pdfparser.git", + "reference": "98d31ba34ef5b5a98897ef4b6c3925d502ea53b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/98d31ba34ef5b5a98897ef4b6c3925d502ea53b1", + "reference": "98d31ba34ef5b5a98897ef4b6c3925d502ea53b1", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-zlib": "*", + "php": ">=7.1", + "symfony/polyfill-mbstring": "^1.18" + }, + "type": "library", + "autoload": { + "psr-0": { + "Smalot\\PdfParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Sebastien MALOT", + "email": "sebastien@malot.fr" + } + ], + "description": "Pdf parser library. Can read and extract information from pdf file.", + "homepage": "https://www.pdfparser.org", + "keywords": [ + "extract", + "parse", + "parser", + "pdf", + "text" + ], + "support": { + "issues": "https://github.com/smalot/pdfparser/issues", + "source": "https://github.com/smalot/pdfparser/tree/v2.12.1" + }, + "time": "2025-07-31T06:19:56+00:00" + }, { "name": "spomky-labs/otphp", "version": "11.3.0", @@ -5158,16 +5503,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.24", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175" + "reference": "3b9cf252b3bb54d5daddea5704b95ea7117b39f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/d038cd3054aeaf1c674022a77048b2ef6376a175", - "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175", + "url": "https://api.github.com/repos/symfony/cache/zipball/3b9cf252b3bb54d5daddea5704b95ea7117b39f4", + "reference": "3b9cf252b3bb54d5daddea5704b95ea7117b39f4", "shasum": "" }, "require": { @@ -5234,7 +5579,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.24" + "source": "https://github.com/symfony/cache/tree/v6.4.27" }, "funding": [ { @@ -5254,7 +5599,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T09:32:03+00:00" + "time": "2025-10-17T12:08:26+00:00" }, { "name": "symfony/cache-contracts", @@ -5412,16 +5757,16 @@ }, { "name": "symfony/config", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e" + "reference": "f18dc5926cb203e125956987def795d052ee774e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/80e2cf005cf17138c97193be0434cdcfd1b2212e", - "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e", + "url": "https://api.github.com/repos/symfony/config/zipball/f18dc5926cb203e125956987def795d052ee774e", + "reference": "f18dc5926cb203e125956987def795d052ee774e", "shasum": "" }, "require": { @@ -5467,7 +5812,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.24" + "source": "https://github.com/symfony/config/tree/v6.4.26" }, "funding": [ { @@ -5487,20 +5832,20 @@ "type": "tidelift" } ], - "time": "2025-07-26T13:50:30+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/console", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", "shasum": "" }, "require": { @@ -5565,7 +5910,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.25" + "source": "https://github.com/symfony/console/tree/v6.4.27" }, "funding": [ { @@ -5585,7 +5930,7 @@ "type": "tidelift" } ], - "time": "2025-08-22T10:21:53+00:00" + "time": "2025-10-06T10:25:16+00:00" }, { "name": "symfony/css-selector", @@ -5658,16 +6003,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3" + "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/900da8a42eceeb4a13a0ec34caa7db49328daff3", - "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5f311eaf0b321f8ec640f6bae12da43a14026898", + "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898", "shasum": "" }, "require": { @@ -5719,7 +6064,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.25" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.26" }, "funding": [ { @@ -5739,7 +6084,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5810,16 +6155,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "d6cda6208c77c4c1f28ba9ff9be4ceaa33416c8c" + "reference": "c14bb5a9125c411e73354954940e06b6e7fcc344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/d6cda6208c77c4c1f28ba9ff9be4ceaa33416c8c", - "reference": "d6cda6208c77c4c1f28ba9ff9be4ceaa33416c8c", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/c14bb5a9125c411e73354954940e06b6e7fcc344", + "reference": "c14bb5a9125c411e73354954940e06b6e7fcc344", "shasum": "" }, "require": { @@ -5898,7 +6243,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.25" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.26" }, "funding": [ { @@ -5918,7 +6263,7 @@ "type": "tidelift" } ], - "time": "2025-08-18T12:29:30+00:00" + "time": "2025-09-26T15:07:38+00:00" }, { "name": "symfony/dotenv", @@ -6000,16 +6345,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c" + "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/30fd0b3cf0e972e82636038ce4db0e4fe777112c", - "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/41bedcaec5b72640b0ec2096547b75fda72ead6c", + "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c", "shasum": "" }, "require": { @@ -6055,7 +6400,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.24" + "source": "https://github.com/symfony/error-handler/tree/v6.4.26" }, "funding": [ { @@ -6075,7 +6420,7 @@ "type": "tidelift" } ], - "time": "2025-07-24T08:25:04+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/event-dispatcher", @@ -6377,16 +6722,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.24", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "73089124388c8510efb8d2d1689285d285937b08" + "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", - "reference": "73089124388c8510efb8d2d1689285d285937b08", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b6aa435d2fba50793b994a839c32b6064f063b", + "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b", "shasum": "" }, "require": { @@ -6421,7 +6766,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.24" + "source": "https://github.com/symfony/finder/tree/v6.4.27" }, "funding": [ { @@ -6441,35 +6786,35 @@ "type": "tidelift" } ], - "time": "2025-07-15T12:02:45+00:00" + "time": "2025-10-15T18:32:00+00:00" }, { "name": "symfony/flex", - "version": "v2.8.2", + "version": "v2.9.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426" + "reference": "94b37978c9982dc41c5b6a4147892d2d3d1b9ce6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/f356aa35f3cf3d2f46c31d344c1098eb2d260426", - "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426", + "url": "https://api.github.com/repos/symfony/flex/zipball/94b37978c9982dc41c5b6a4147892d2d3d1b9ce6", + "reference": "94b37978c9982dc41c5b6a4147892d2d3d1b9ce6", "shasum": "" }, "require": { "composer-plugin-api": "^2.1", - "php": ">=8.0" + "php": ">=8.1" }, "conflict": { "composer/semver": "<1.7.2" }, "require-dev": { "composer/composer": "^2.1", - "symfony/dotenv": "^5.4|^6.0", - "symfony/filesystem": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0" + "symfony/dotenv": "^6.4|^7.4|^8.0", + "symfony/filesystem": "^6.4|^7.4|^8.0", + "symfony/phpunit-bridge": "^6.4|^7.4|^8.0", + "symfony/process": "^6.4|^7.4|^8.0" }, "type": "composer-plugin", "extra": { @@ -6493,7 +6838,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.8.2" + "source": "https://github.com/symfony/flex/tree/v2.9.0" }, "funding": [ { @@ -6513,20 +6858,20 @@ "type": "tidelift" } ], - "time": "2025-08-22T07:17:23+00:00" + "time": "2025-10-31T15:22:50+00:00" }, { "name": "symfony/form", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "ce4d50b8779556a18f72e6122107b3db2f116140" + "reference": "5d922aea68ffe1637535713b35b29f6f34b7d81c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/ce4d50b8779556a18f72e6122107b3db2f116140", - "reference": "ce4d50b8779556a18f72e6122107b3db2f116140", + "url": "https://api.github.com/repos/symfony/form/zipball/5d922aea68ffe1637535713b35b29f6f34b7d81c", + "reference": "5d922aea68ffe1637535713b35b29f6f34b7d81c", "shasum": "" }, "require": { @@ -6594,7 +6939,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.4.25" + "source": "https://github.com/symfony/form/tree/v6.4.27" }, "funding": [ { @@ -6614,20 +6959,20 @@ "type": "tidelift" } ], - "time": "2025-08-18T12:29:30+00:00" + "time": "2025-10-10T09:11:15+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447" + "reference": "ee58c2a73218d8f4763824e1414c5f9b4519c91f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1d6a764b58e4f780df00f71c20ba3a61095ea447", - "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/ee58c2a73218d8f4763824e1414c5f9b4519c91f", + "reference": "ee58c2a73218d8f4763824e1414c5f9b4519c91f", "shasum": "" }, "require": { @@ -6747,7 +7092,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.25" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.27" }, "funding": [ { @@ -6767,20 +7112,20 @@ "type": "tidelift" } ], - "time": "2025-08-26T10:44:20+00:00" + "time": "2025-10-15T17:35:09+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "b8e9dce2d8acba3c32af467bb58e0c3656886181" + "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/b8e9dce2d8acba3c32af467bb58e0c3656886181", - "reference": "b8e9dce2d8acba3c32af467bb58e0c3656886181", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6740cdc1a3bffa127966b6056e883b3fe3709849", + "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849", "shasum": "" }, "require": { @@ -6845,7 +7190,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.25" + "source": "https://github.com/symfony/http-client/tree/v6.4.26" }, "funding": [ { @@ -6865,7 +7210,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T07:01:16+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/http-client-contracts", @@ -6947,16 +7292,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272" + "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6bc974c0035b643aa497c58d46d9e25185e4b272", - "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/369241591d92bb5dfb4c6ccd6ee94378a45b1521", + "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521", "shasum": "" }, "require": { @@ -7004,7 +7349,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.25" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.26" }, "funding": [ { @@ -7024,20 +7369,20 @@ "type": "tidelift" } ], - "time": "2025-08-20T06:48:20+00:00" + "time": "2025-09-16T08:22:30+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15" + "reference": "4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", - "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb", + "reference": "4a4d0f6cafdbc09c9a7940db17c0fc23bb88a2bb", "shasum": "" }, "require": { @@ -7122,7 +7467,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.25" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.27" }, "funding": [ { @@ -7142,20 +7487,20 @@ "type": "tidelift" } ], - "time": "2025-08-29T07:55:45+00:00" + "time": "2025-10-28T10:06:47+00:00" }, { "name": "symfony/intl", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "84dc64e179360927eed8f3403b4927aa70702c4f" + "reference": "ccc52824610e7de72424cf516e52d4fb39e3bfa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/84dc64e179360927eed8f3403b4927aa70702c4f", - "reference": "84dc64e179360927eed8f3403b4927aa70702c4f", + "url": "https://api.github.com/repos/symfony/intl/zipball/ccc52824610e7de72424cf516e52d4fb39e3bfa5", + "reference": "ccc52824610e7de72424cf516e52d4fb39e3bfa5", "shasum": "" }, "require": { @@ -7209,7 +7554,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.25" + "source": "https://github.com/symfony/intl/tree/v6.4.27" }, "funding": [ { @@ -7229,20 +7574,20 @@ "type": "tidelift" } ], - "time": "2025-08-19T13:57:48+00:00" + "time": "2025-10-01T06:01:44+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "628b43b45a3e6b15c8a633fb22df547ed9b492a2" + "reference": "2f096718ed718996551f66e3a24e12b2ed027f95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/628b43b45a3e6b15c8a633fb22df547ed9b492a2", - "reference": "628b43b45a3e6b15c8a633fb22df547ed9b492a2", + "url": "https://api.github.com/repos/symfony/mailer/zipball/2f096718ed718996551f66e3a24e12b2ed027f95", + "reference": "2f096718ed718996551f66e3a24e12b2ed027f95", "shasum": "" }, "require": { @@ -7293,7 +7638,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.25" + "source": "https://github.com/symfony/mailer/tree/v6.4.27" }, "funding": [ { @@ -7313,20 +7658,20 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2025-10-24T13:29:09+00:00" }, { "name": "symfony/mime", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7" + "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/664d5e844a2de5e11c8255d0aef6bc15a9660ac7", - "reference": "664d5e844a2de5e11c8255d0aef6bc15a9660ac7", + "url": "https://api.github.com/repos/symfony/mime/zipball/61ab9681cdfe315071eb4fa79b6ad6ab030a9235", + "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235", "shasum": "" }, "require": { @@ -7382,7 +7727,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.24" + "source": "https://github.com/symfony/mime/tree/v6.4.26" }, "funding": [ { @@ -7402,20 +7747,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T12:02:45+00:00" + "time": "2025-09-16T08:22:30+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "6711737a4f6acc6cea94344d252fa5d457a5d647" + "reference": "b950e76daaa5117c367cff22ececce93b9819522" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/6711737a4f6acc6cea94344d252fa5d457a5d647", - "reference": "6711737a4f6acc6cea94344d252fa5d457a5d647", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/b950e76daaa5117c367cff22ececce93b9819522", + "reference": "b950e76daaa5117c367cff22ececce93b9819522", "shasum": "" }, "require": { @@ -7465,7 +7810,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.25" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.27" }, "funding": [ { @@ -7485,7 +7830,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2025-10-14T13:20:03+00:00" }, { "name": "symfony/monolog-bundle", @@ -7877,16 +8222,16 @@ }, { "name": "symfony/process", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8" + "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8", + "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8", "shasum": "" }, "require": { @@ -7918,7 +8263,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.25" + "source": "https://github.com/symfony/process/tree/v6.4.26" }, "funding": [ { @@ -7938,7 +8283,7 @@ "type": "tidelift" } ], - "time": "2025-08-14T06:23:17+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/property-access", @@ -8188,16 +8533,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" + "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", - "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "url": "https://api.github.com/repos/symfony/routing/zipball/6fc4c445f22857d4b8b40a02b73f423ddab295de", + "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de", "shasum": "" }, "require": { @@ -8251,7 +8596,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.24" + "source": "https://github.com/symfony/routing/tree/v6.4.26" }, "funding": [ { @@ -8271,20 +8616,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T08:46:37+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "c1cc6721646f546627236c57f835272806087337" + "reference": "59933ca737fd60fad548241b6d879cd0e4be31ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/c1cc6721646f546627236c57f835272806087337", - "reference": "c1cc6721646f546627236c57f835272806087337", + "url": "https://api.github.com/repos/symfony/runtime/zipball/59933ca737fd60fad548241b6d879cd0e4be31ab", + "reference": "59933ca737fd60fad548241b6d879cd0e4be31ab", "shasum": "" }, "require": { @@ -8334,7 +8679,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.24" + "source": "https://github.com/symfony/runtime/tree/v6.4.26" }, "funding": [ { @@ -8354,20 +8699,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2025-09-11T15:30:54+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "9780abdfc2011f0998f3cb6c7c88ad8453bd34ef" + "reference": "b83773107a5b83a5507df9e88bd50d495f6e8b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/9780abdfc2011f0998f3cb6c7c88ad8453bd34ef", - "reference": "9780abdfc2011f0998f3cb6c7c88ad8453bd34ef", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/b83773107a5b83a5507df9e88bd50d495f6e8b72", + "reference": "b83773107a5b83a5507df9e88bd50d495f6e8b72", "shasum": "" }, "require": { @@ -8450,7 +8795,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.25" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.26" }, "funding": [ { @@ -8470,20 +8815,20 @@ "type": "tidelift" } ], - "time": "2025-08-05T13:23:39+00:00" + "time": "2025-09-22T15:03:07+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "b70b1ac76c89eb76faed5721b0dcad3b61a1d747" + "reference": "673018434b38e504eb04ca3c6d7e2e7c86735bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/b70b1ac76c89eb76faed5721b0dcad3b61a1d747", - "reference": "b70b1ac76c89eb76faed5721b0dcad3b61a1d747", + "url": "https://api.github.com/repos/symfony/security-core/zipball/673018434b38e504eb04ca3c6d7e2e7c86735bfb", + "reference": "673018434b38e504eb04ca3c6d7e2e7c86735bfb", "shasum": "" }, "require": { @@ -8540,7 +8885,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.25" + "source": "https://github.com/symfony/security-core/tree/v6.4.27" }, "funding": [ { @@ -8560,7 +8905,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T19:23:36+00:00" + "time": "2025-10-23T19:49:35+00:00" }, { "name": "symfony/security-csrf", @@ -8636,16 +8981,16 @@ }, { "name": "symfony/security-http", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "878667b04451f2e17b4161237379e5d062575181" + "reference": "6c2e236f0fc3e0853770a5574ef7af471486ba4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/878667b04451f2e17b4161237379e5d062575181", - "reference": "878667b04451f2e17b4161237379e5d062575181", + "url": "https://api.github.com/repos/symfony/security-http/zipball/6c2e236f0fc3e0853770a5574ef7af471486ba4c", + "reference": "6c2e236f0fc3e0853770a5574ef7af471486ba4c", "shasum": "" }, "require": { @@ -8704,7 +9049,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.25" + "source": "https://github.com/symfony/security-http/tree/v6.4.26" }, "funding": [ { @@ -8724,20 +9069,20 @@ "type": "tidelift" } ], - "time": "2025-08-21T13:09:57+00:00" + "time": "2025-09-05T18:17:25+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "26f877808e20da1c0f8bc366d54c726003b0088b" + "reference": "28779bbdb398cac3421d0e51f7ca669e4a27c5ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/26f877808e20da1c0f8bc366d54c726003b0088b", - "reference": "26f877808e20da1c0f8bc366d54c726003b0088b", + "url": "https://api.github.com/repos/symfony/serializer/zipball/28779bbdb398cac3421d0e51f7ca669e4a27c5ac", + "reference": "28779bbdb398cac3421d0e51f7ca669e4a27c5ac", "shasum": "" }, "require": { @@ -8806,7 +9151,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.25" + "source": "https://github.com/symfony/serializer/tree/v6.4.27" }, "funding": [ { @@ -8826,7 +9171,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:31:57+00:00" + "time": "2025-10-08T04:24:22+00:00" }, { "name": "symfony/service-contracts", @@ -8979,16 +9324,16 @@ }, { "name": "symfony/string", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1" + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", - "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", + "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", "shasum": "" }, "require": { @@ -9002,7 +9347,6 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -9045,7 +9389,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.25" + "source": "https://github.com/symfony/string/tree/v6.4.26" }, "funding": [ { @@ -9065,20 +9409,20 @@ "type": "tidelift" } ], - "time": "2025-08-22T12:33:20+00:00" + "time": "2025-09-11T14:32:46+00:00" }, { "name": "symfony/translation", - "version": "v6.4.24", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1" + "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/300b72643e89de0734d99a9e3f8494a3ef6936e1", - "reference": "300b72643e89de0734d99a9e3f8494a3ef6936e1", + "url": "https://api.github.com/repos/symfony/translation/zipball/c8559fe25c7ee7aa9d28f228903a46db008156a4", + "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4", "shasum": "" }, "require": { @@ -9144,7 +9488,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.24" + "source": "https://github.com/symfony/translation/tree/v6.4.26" }, "funding": [ { @@ -9164,7 +9508,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:30:48+00:00" + "time": "2025-09-05T18:17:25+00:00" }, { "name": "symfony/translation-contracts", @@ -9447,16 +9791,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "9352177c0e937793423053846f80bee805552324" + "reference": "60dd71e219cd3d76fde906eb6b6c1271db628f5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/9352177c0e937793423053846f80bee805552324", - "reference": "9352177c0e937793423053846f80bee805552324", + "url": "https://api.github.com/repos/symfony/validator/zipball/60dd71e219cd3d76fde906eb6b6c1271db628f5b", + "reference": "60dd71e219cd3d76fde906eb6b6c1271db628f5b", "shasum": "" }, "require": { @@ -9524,7 +9868,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.25" + "source": "https://github.com/symfony/validator/tree/v6.4.27" }, "funding": [ { @@ -9544,20 +9888,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:31:57+00:00" + "time": "2025-10-23T19:49:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c" + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6cd92486e9fc32506370822c57bc02353a5a92c", - "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfae1497a2f1eaad78dbc0590311c599c7178d4a", + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a", "shasum": "" }, "require": { @@ -9612,7 +9956,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.25" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.26" }, "funding": [ { @@ -9632,20 +9976,20 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2025-09-25T15:37:27+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358" + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", - "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/466fcac5fa2e871f83d31173f80e9c2684743bfc", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc", "shasum": "" }, "require": { @@ -9693,7 +10037,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.25" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.26" }, "funding": [ { @@ -9713,7 +10057,7 @@ "type": "tidelift" } ], - "time": "2025-08-18T13:06:32+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "symfony/webpack-encore-bundle", @@ -9793,16 +10137,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565" + "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e54b060bc9c3dc3d4258bf0d165d0064e755f565", - "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", + "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", "shasum": "" }, "require": { @@ -9845,7 +10189,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.25" + "source": "https://github.com/symfony/yaml/tree/v6.4.26" }, "funding": [ { @@ -9865,7 +10209,7 @@ "type": "tidelift" } ], - "time": "2025-08-26T16:59:00+00:00" + "time": "2025-09-26T15:07:38+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -9924,16 +10268,16 @@ }, { "name": "twig/cssinliner-extra", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/cssinliner-extra.git", - "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8" + "reference": "9bcbf04ca515e98fcde479fdceaa1d9d9e76173e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/378d29b61d6406c456e3a4afbd15bbeea0b72ea8", - "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/9bcbf04ca515e98fcde479fdceaa1d9d9e76173e", + "reference": "9bcbf04ca515e98fcde479fdceaa1d9d9e76173e", "shasum": "" }, "require": { @@ -9977,7 +10321,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.22.0" }, "funding": [ { @@ -9989,20 +10333,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2025-07-29T08:07:07+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896" + "reference": "6d253f0fe28a83a045497c8fb3ea9bfe84e82cf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/62d1cf47a1aa009cbd07b21045b97d3d5cb79896", - "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6d253f0fe28a83a045497c8fb3ea9bfe84e82cf4", + "reference": "6d253f0fe28a83a045497c8fb3ea9bfe84e82cf4", "shasum": "" }, "require": { @@ -10012,7 +10356,7 @@ "twig/twig": "^3.2|^4.0" }, "require-dev": { - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", "twig/cssinliner-extra": "^3.0", @@ -10051,7 +10395,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.21.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.0" }, "funding": [ { @@ -10063,20 +10407,20 @@ "type": "tidelift" } ], - "time": "2025-02-19T14:29:33+00:00" + "time": "2025-09-15T05:57:37+00:00" }, { "name": "twig/inky-extra", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/inky-extra.git", - "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d" + "reference": "631f42c7123240d9c2497903679ec54bb25f2f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", - "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/631f42c7123240d9c2497903679ec54bb25f2f52", + "reference": "631f42c7123240d9c2497903679ec54bb25f2f52", "shasum": "" }, "require": { @@ -10121,7 +10465,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/inky-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/inky-extra/tree/v3.22.0" }, "funding": [ { @@ -10133,20 +10477,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2025-07-29T08:07:07+00:00" }, { "name": "twig/intl-extra", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146" + "reference": "7393fc911c7315db18a805d3a541ac7bb9e4fdc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/05bc5d46b9df9e62399eae53e7c0b0633298b146", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/7393fc911c7315db18a805d3a541ac7bb9e4fdc0", + "reference": "7393fc911c7315db18a805d3a541ac7bb9e4fdc0", "shasum": "" }, "require": { @@ -10185,7 +10529,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/intl-extra/tree/v3.22.0" }, "funding": [ { @@ -10197,11 +10541,11 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2025-09-15T06:05:04+00:00" }, { "name": "twig/string-extra", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", @@ -10252,7 +10596,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.22.0" }, "funding": [ { @@ -10268,16 +10612,16 @@ }, { "name": "twig/twig", - "version": "v3.21.1", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + "reference": "4509984193026de413baf4ba80f68590a7f2c51d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d", + "reference": "4509984193026de413baf4ba80f68590a7f2c51d", "shasum": "" }, "require": { @@ -10331,7 +10675,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + "source": "https://github.com/twigphp/Twig/tree/v3.22.0" }, "funding": [ { @@ -10343,32 +10687,32 @@ "type": "tidelift" } ], - "time": "2025-05-03T07:21:55+00:00" + "time": "2025-10-29T15:56:47+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -10399,9 +10743,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" }, { "name": "willdurand/jsonp-callback-validator", @@ -10504,22 +10848,23 @@ }, { "name": "zircote/swagger-php", - "version": "5.4.0", + "version": "5.5.2", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "e25c377ec04db4d2b91186e2debaa1fb135f5cc5" + "reference": "0ca908380414596f5ed3a7ad33a04abb4cffe613" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e25c377ec04db4d2b91186e2debaa1fb135f5cc5", - "reference": "e25c377ec04db4d2b91186e2debaa1fb135f5cc5", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/0ca908380414596f5ed3a7ad33a04abb4cffe613", + "reference": "0ca908380414596f5ed3a7ad33a04abb4cffe613", "shasum": "" }, "require": { "ext-json": "*", "nikic/php-parser": "^4.19 || ^5.0", "php": ">=7.4", + "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0", @@ -10538,7 +10883,8 @@ "vimeo/psalm": "^4.30 || ^5.0" }, "suggest": { - "doctrine/annotations": "^2.0" + "doctrine/annotations": "^2.0", + "radebatz/type-info-extras": "^1.0.2" }, "bin": [ "bin/openapi" @@ -10584,9 +10930,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.4.0" + "source": "https://github.com/zircote/swagger-php/tree/5.5.2" }, - "time": "2025-09-12T03:49:27+00:00" + "time": "2025-10-27T04:40:08+00:00" } ], "packages-dev": [ @@ -10722,25 +11068,25 @@ }, { "name": "dama/doctrine-test-bundle", - "version": "v8.3.1", + "version": "v8.4.0", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6" + "reference": "ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/9bc47e02a0d67cbfef6773837249f71e65c95bf6", - "reference": "9bc47e02a0d67cbfef6773837249f71e65c95bf6", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2", + "reference": "ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2", "shasum": "" }, "require": { "doctrine/dbal": "^3.3 || ^4.0", - "doctrine/doctrine-bundle": "^2.11.0", + "doctrine/doctrine-bundle": "^2.11.0 || ^3.0", "php": ">= 8.1", "psr/cache": "^2.0 || ^3.0", - "symfony/cache": "^6.4 || ^7.2 || ^8.0", - "symfony/framework-bundle": "^6.4 || ^7.2 || ^8.0" + "symfony/cache": "^6.4 || ^7.3 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0" }, "conflict": { "phpunit/phpunit": "<10.0" @@ -10749,9 +11095,9 @@ "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", - "symfony/process": "^6.4 || ^7.2 || ^8.0", - "symfony/yaml": "^6.4 || ^7.2 || ^8.0" + "phpunit/phpunit": "^10.5.57 || ^11.5.41|| ^12.3.14", + "symfony/dotenv": "^6.4 || ^7.3 || ^8.0", + "symfony/process": "^6.4 || ^7.3 || ^8.0" }, "type": "symfony-bundle", "extra": { @@ -10785,22 +11131,22 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.3.1" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.4.0" }, - "time": "2025-08-05T17:55:02+00:00" + "time": "2025-10-11T15:24:02+00:00" }, { "name": "doctrine/data-fixtures", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "f161e20f04ba5440a09330e156b40f04dd70d47f" + "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f161e20f04ba5440a09330e156b40f04dd70d47f", - "reference": "f161e20f04ba5440a09330e156b40f04dd70d47f", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/7a615ba135e45d67674bb623d90f34f6c7b6bd97", + "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97", "shasum": "" }, "require": { @@ -10814,14 +11160,14 @@ "doctrine/phpcr-odm": "<1.3.0" }, "require-dev": { - "doctrine/coding-standard": "^13", + "doctrine/coding-standard": "^14", "doctrine/dbal": "^3.5 || ^4", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", "doctrine/orm": "^2.14 || ^3", "ext-sqlite3": "*", "fig/log-test": "^1", - "phpstan/phpstan": "2.1.17", - "phpunit/phpunit": "10.5.45", + "phpstan/phpstan": "2.1.31", + "phpunit/phpunit": "10.5.45 || 12.4.0", "symfony/cache": "^6.4 || ^7", "symfony/var-exporter": "^6.4 || ^7" }, @@ -10854,7 +11200,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/2.1.0" + "source": "https://github.com/doctrine/data-fixtures/tree/2.2.0" }, "funding": [ { @@ -10870,20 +11216,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T17:48:20+00:00" + "time": "2025-10-17T20:06:20+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "bd59519a7532b9e1a41cef4049d5326dfac7def9" + "reference": "0afaecd65e5791e855edddf125b77576e7bcbbfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/bd59519a7532b9e1a41cef4049d5326dfac7def9", - "reference": "bd59519a7532b9e1a41cef4049d5326dfac7def9", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/0afaecd65e5791e855edddf125b77576e7bcbbfb", + "reference": "0afaecd65e5791e855edddf125b77576e7bcbbfb", "shasum": "" }, "require": { @@ -10904,10 +11250,10 @@ "doctrine/dbal": "< 3" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^2", - "phpunit/phpunit": "^9.6.13", - "symfony/phpunit-bridge": "^6.3.6" + "doctrine/coding-standard": "14.0.0", + "phpstan/phpstan": "2.1.11", + "phpunit/phpunit": "^9.6.13 || 11.4.14", + "symfony/phpunit-bridge": "7.2.0" }, "type": "symfony-bundle", "autoload": { @@ -10941,7 +11287,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.7.1" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.7.2" }, "funding": [ { @@ -10957,7 +11303,7 @@ "type": "tidelift" } ], - "time": "2024-12-03T17:07:51+00:00" + "time": "2025-10-11T16:14:44+00:00" }, { "name": "evenement/evenement", @@ -11132,16 +11478,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.87.2", + "version": "v3.89.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992" + "reference": "f34967da2866ace090a2b447de1f357356474573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992", - "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f34967da2866ace090a2b447de1f357356474573", + "reference": "f34967da2866ace090a2b447de1f357356474573", "shasum": "" }, "require": { @@ -11156,7 +11502,6 @@ "php": "^7.4 || ^8.0", "react/child-process": "^0.6.6", "react/event-loop": "^1.5", - "react/promise": "^3.3", "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", @@ -11168,12 +11513,13 @@ "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.29.14", + "infection/infection": "^0.31.0", "justinrainbow/json-schema": "^6.5", "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", @@ -11181,7 +11527,6 @@ "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", - "symfony/polyfill-php84": "^1.33", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" }, @@ -11224,7 +11569,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.87.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.1" }, "funding": [ { @@ -11232,7 +11577,7 @@ "type": "github" } ], - "time": "2025-09-10T09:51:40+00:00" + "time": "2025-10-24T12:05:10+00:00" }, { "name": "masterminds/html5", @@ -11421,16 +11766,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "578fa296a166605d97b94091f724f1257185d278" - }, + "version": "2.1.31", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", - "reference": "578fa296a166605d97b94091f724f1257185d278", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", + "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", "shasum": "" }, "require": { @@ -11475,7 +11815,7 @@ "type": "github" } ], - "time": "2025-09-19T08:58:49+00:00" + "time": "2025-10-10T14:14:11+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11526,16 +11866,16 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.6", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "934f5734812341358fc41c44006b30fa00c785f0" + "reference": "5eaf37b87288474051469aee9f937fc9d862f330" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/934f5734812341358fc41c44006b30fa00c785f0", - "reference": "934f5734812341358fc41c44006b30fa00c785f0", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/5eaf37b87288474051469aee9f937fc9d862f330", + "reference": "5eaf37b87288474051469aee9f937fc9d862f330", "shasum": "" }, "require": { @@ -11569,7 +11909,8 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", - "symfony/cache": "^5.4" + "symfony/cache": "^5.4", + "symfony/uid": "^5.4 || ^6.4 || ^7.3" }, "type": "phpstan-extension", "extra": { @@ -11592,9 +11933,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.6" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.10" }, - "time": "2025-09-10T07:06:30+00:00" + "time": "2025-10-06T10:01:02+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -11651,21 +11992,21 @@ }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.6", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0.4" + "phpstan/phpstan": "^2.1.29" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -11693,9 +12034,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" }, - "time": "2025-07-21T12:19:29+00:00" + "time": "2025-09-26T11:19:08+00:00" }, { "name": "phpstan/phpstan-symfony", @@ -12091,16 +12432,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.56", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e00fdca14e854ed26256da31f4ab1c9a58453742" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e00fdca14e854ed26256da31f4ab1c9a58453742", - "reference": "e00fdca14e854ed26256da31f4ab1c9a58453742", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { @@ -12124,7 +12465,7 @@ "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.3", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", @@ -12172,7 +12513,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.56" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -12196,7 +12537,7 @@ "type": "tidelift" } ], - "time": "2025-09-23T06:21:55+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "react/cache", @@ -13172,16 +13513,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "9e7e86260de48e405ec3086bcb62e677ef192e7f" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/9e7e86260de48e405ec3086bcb62e677ef192e7f", - "reference": "9e7e86260de48e405ec3086bcb62e677ef192e7f", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -13238,7 +13579,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { @@ -13258,7 +13599,7 @@ "type": "tidelift" } ], - "time": "2025-09-22T05:25:48+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -13751,16 +14092,16 @@ }, { "name": "symfony/debug-bundle", - "version": "v6.4.13", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "7bcfaff39e094cc09455201916d016d9b2ae08ff" + "reference": "21a61c55192d558a6b81cdb12e8c010fc9474fe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/7bcfaff39e094cc09455201916d016d9b2ae08ff", - "reference": "7bcfaff39e094cc09455201916d016d9b2ae08ff", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/21a61c55192d558a6b81cdb12e8c010fc9474fe0", + "reference": "21a61c55192d558a6b81cdb12e8c010fc9474fe0", "shasum": "" }, "require": { @@ -13805,7 +14146,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v6.4.13" + "source": "https://github.com/symfony/debug-bundle/tree/v6.4.27" }, "funding": [ { @@ -13816,12 +14157,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-25T14:18:03+00:00" + "time": "2025-10-11T17:35:31+00:00" }, { "name": "symfony/dom-crawler", @@ -13896,16 +14241,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "ddce87db4f5c12fcff191f294f3b70125b333038" + "reference": "406aa80401bf960e7a173a3ccf268ae82b6bc93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/ddce87db4f5c12fcff191f294f3b70125b333038", - "reference": "ddce87db4f5c12fcff191f294f3b70125b333038", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/406aa80401bf960e7a173a3ccf268ae82b6bc93f", + "reference": "406aa80401bf960e7a173a3ccf268ae82b6bc93f", "shasum": "" }, "require": { @@ -13961,7 +14306,7 @@ "testing" ], "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.25" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.26" }, "funding": [ { @@ -13981,20 +14326,20 @@ "type": "tidelift" } ], - "time": "2025-08-02T14:05:23+00:00" + "time": "2025-09-12T08:37:02+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v6.4.25", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "4c1754d6b3ffe52e9eaed0d9a392eb43a60fc910" + "reference": "4c2ab411372e8bd854678cd7c81f1a9bfd6914aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4c1754d6b3ffe52e9eaed0d9a392eb43a60fc910", - "reference": "4c1754d6b3ffe52e9eaed0d9a392eb43a60fc910", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4c2ab411372e8bd854678cd7c81f1a9bfd6914aa", + "reference": "4c2ab411372e8bd854678cd7c81f1a9bfd6914aa", "shasum": "" }, "require": { @@ -14047,7 +14392,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.25" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.27" }, "funding": [ { @@ -14067,7 +14412,7 @@ "type": "tidelift" } ], - "time": "2025-08-07T12:02:05+00:00" + "time": "2025-10-05T13:55:43+00:00" }, { "name": "theseer/tokenizer", diff --git a/migrations/Version20251031142000.php b/migrations/Version20251031142000.php new file mode 100644 index 00000000..be9bed51 --- /dev/null +++ b/migrations/Version20251031142000.php @@ -0,0 +1,46 @@ +addSql('ALTER TABLE kimai2_customers ADD address_line1 VARCHAR(150) DEFAULT NULL, ADD address_line2 VARCHAR(150) DEFAULT NULL, ADD address_line3 VARCHAR(150) DEFAULT NULL, ADD postcode VARCHAR(20) DEFAULT NULL, ADD city VARCHAR(50) DEFAULT NULL, ADD buyer_reference VARCHAR(50) DEFAULT NULL'); + $this->addSql('ALTER TABLE kimai2_invoice_templates ADD customer_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE kimai2_invoice_templates MODIFY company VARCHAR(255) NULL'); + $this->addSql('ALTER TABLE kimai2_invoice_templates ADD CONSTRAINT FK_1626CFE99395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_1626CFE99395C3F3 ON kimai2_invoice_templates (customer_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE kimai2_invoice_templates DROP FOREIGN KEY FK_1626CFE99395C3F3'); + $this->addSql('DROP INDEX IDX_1626CFE99395C3F3 ON kimai2_invoice_templates'); + $this->addSql('ALTER TABLE kimai2_invoice_templates DROP customer_id'); + $this->addSql('ALTER TABLE kimai2_customers DROP address_line1, DROP address_line2, DROP address_line3, DROP postcode, DROP city, DROP buyer_reference'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20251031143000.php b/migrations/Version20251031143000.php new file mode 100644 index 00000000..3fd0c7da --- /dev/null +++ b/migrations/Version20251031143000.php @@ -0,0 +1,43 @@ +addSql('CREATE TABLE kimai2_invoice_templates_meta (id INT AUTO_INCREMENT NOT NULL, template_id INT NOT NULL, name VARCHAR(50) NOT NULL, value TEXT DEFAULT NULL, visible TINYINT(1) DEFAULT 0 NOT NULL, INDEX IDX_A165B0555DA0FB8 (template_id), UNIQUE INDEX UNIQ_A165B0555DA0FB85E237E06 (template_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE kimai2_invoice_templates_meta ADD CONSTRAINT FK_A165B0555DA0FB8 FOREIGN KEY (template_id) REFERENCES kimai2_invoice_templates (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + $table = $schema->getTable('kimai2_invoice_templates_meta'); + $table->removeForeignKey('FK_A165B0555DA0FB8'); + + $schema->dropTable('kimai2_invoice_templates_meta'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/phpstan.neon b/phpstan.neon index f8769f24..6eba82c3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -41,6 +41,8 @@ parameters: symfony: containerXmlPath: %rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml ignoreErrors: + - identifier: throws.unusedType + - identifier: offsetAccess.notFound - '#^Method .*\(\) has parameter \$builder with generic interface Symfony\\Component\\Form\\FormBuilderInterface but does not specify its types\: TData$#' - @@ -753,11 +755,6 @@ parameters: count: 1 path: src/Controller/InvoiceController.php - - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" - count: 1 - path: src/Controller/InvoiceController.php - - message: "#^Cannot call method getName\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#" count: 1 @@ -804,7 +801,7 @@ parameters: path: src/Controller/ProjectController.php - - message: "#^Call to function array_key_exists\\(\\) with 'entry' and array\\{day\\: DateTime, entry\\: App\\\\Entity\\\\Timesheet\\} will always evaluate to true\\.$#" + 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 @@ -983,11 +980,6 @@ parameters: count: 1 path: src/Customer/CustomerStatisticService.php - - - message: "#^Method App\\\\DataFixtures\\\\InvoiceFixtures\\:\\:getInvoiceConfigs\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/DataFixtures/InvoiceFixtures.php - - message: "#^Method App\\\\DataFixtures\\\\TeamFixtures\\:\\:getGroups\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2763,11 +2755,6 @@ parameters: count: 1 path: src/Form/UserTwoFactorType.php - - - message: "#^Cannot call method getVat\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" - count: 1 - path: src/Invoice/Calculator/AbstractCalculator.php - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#" count: 2 @@ -2818,31 +2805,6 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php - - - message: "#^Cannot call method getSubtotal\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - - message: "#^Cannot call method getTax\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - - message: "#^Cannot call method getTimeWorked\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" - count: 2 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - - message: "#^Cannot call method getTotal\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - - message: "#^Cannot call method getVat\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydrator\\:\\:hydrate\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2873,16 +2835,6 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceModelUserHydrator.php - - - message: "#^Cannot call method getCompany\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" - count: 1 - path: src/Invoice/InvoiceFilename.php - - - - message: "#^Cannot call method getName\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" - count: 1 - path: src/Invoice/InvoiceFilename.php - - message: "#^Parameter \\#1 \\$filename of method App\\\\Invoice\\\\InvoiceFilename\\:\\:convert\\(\\) expects string, string\\|null given\\.$#" count: 1 @@ -2928,11 +2880,6 @@ parameters: count: 1 path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php - - - message: "#^Cannot call method getTitle\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" - count: 1 - path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php - - message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#" count: 5 @@ -2953,11 +2900,6 @@ parameters: count: 1 path: src/Invoice/Renderer/AbstractTwigRenderer.php - - - message: "#^Cannot call method getLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" - count: 1 - path: src/Invoice/Renderer/AbstractTwigRenderer.php - - message: "#^Method App\\\\Invoice\\\\Renderer\\\\AbstractTwigRenderer\\:\\:renderTwigTemplate\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2983,11 +2925,6 @@ parameters: count: 1 path: src/Invoice/ServiceInvoice.php - - - message: "#^Cannot call method getRenderer\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" - count: 6 - path: src/Invoice/ServiceInvoice.php - - message: "#^Cannot call method hasInvoiceTemplate\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 1 @@ -4207,3 +4144,8 @@ parameters: message: "#^Method App\\\\Form\\\\MultiUpdate\\\\MultiUpdateTableDTO\\:\\:setEntities\\(\\) has parameter \\$entities with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#" count: 1 path: src/Form/MultiUpdate/MultiUpdateTableDTO.php + + - + message: "#^Method App\\\\Entity\\\\InvoiceTemplate\\:\\:getMetaFields\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" + count: 1 + path: src/Entity/InvoiceTemplate.php diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 45f13687..13aec0ad 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -17,6 +17,7 @@ use App\Entity\MetaTableTypeInterface; use App\Event\InvoiceDocumentsEvent; use App\Event\InvoiceMetaDefinitionEvent; use App\Event\InvoiceMetaDisplayEvent; +use App\Event\InvoiceTemplateMetaDefinitionEvent; use App\Export\Spreadsheet\EntityWithMetaFieldsExporter; use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter; use App\Export\Spreadsheet\Writer\XlsxWriter; @@ -425,7 +426,7 @@ final class InvoiceController extends AbstractController $table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]); $table->addColumn('title', ['class' => 'd-none text-nowrap', 'orderBy' => false]); - $table->addColumn('company', ['class' => 'd-none', 'orderBy' => false]); + $table->addColumn('company', ['class' => 'd-none', 'orderBy' => false, 'title' => 'sending_company']); $table->addColumn('vat_id', ['class' => 'd-none text-nowrap', 'orderBy' => false]); $table->addColumn('tax_rate', ['class' => 'd-none text-nowrap', 'orderBy' => false]); $table->addColumn('due_days', ['class' => 'd-none text-nowrap', 'orderBy' => false]); @@ -728,6 +729,9 @@ final class InvoiceController extends AbstractController private function renderTemplateForm(InvoiceTemplate $template, Request $request): Response { + $event = new InvoiceTemplateMetaDefinitionEvent($template); + $this->dispatcher->dispatch($event); + $editForm = $this->createTemplateEditForm($template); $editForm->handleRequest($request); diff --git a/src/Controller/QuickEntryController.php b/src/Controller/QuickEntryController.php index 3eb0bc8d..7a297231 100644 --- a/src/Controller/QuickEntryController.php +++ b/src/Controller/QuickEntryController.php @@ -127,7 +127,7 @@ final class QuickEntryController extends AbstractController $startFrom = null; if ($takeOverWeeks !== null && \intval($takeOverWeeks) > 0) { $startFrom = clone $startWeek; - $startFrom->modify(\sprintf('-%s weeks', $takeOverWeeks)); + $startFrom->modify(\sprintf('-%s weeks', (string) $takeOverWeeks)); } $favorites = $this->favoriteRecordService->favoriteEntries($user, $amount); diff --git a/src/DataFixtures/CustomerFixtures.php b/src/DataFixtures/CustomerFixtures.php index 8bfbafd2..c6a58114 100644 --- a/src/DataFixtures/CustomerFixtures.php +++ b/src/DataFixtures/CustomerFixtures.php @@ -92,6 +92,10 @@ final class CustomerFixtures extends Fixture $entry->setTimezone($faker->timezone()); $entry->setVisible($visible); $entry->setVatId($faker->creditCardNumber()); + $entry->setPostCode($faker->postcode()); + $entry->setCity($faker->city()); + $entry->setAddressLine1($faker->streetAddress()); + $entry->setAddressLine2($faker->streetAddress()); if (rand(0, 3) % 3) { $entry->setBudget(rand(self::MIN_BUDGET, self::MAX_BUDGET)); diff --git a/src/DataFixtures/InvoiceFixtures.php b/src/DataFixtures/InvoiceFixtures.php index 330382e4..ed9468d4 100644 --- a/src/DataFixtures/InvoiceFixtures.php +++ b/src/DataFixtures/InvoiceFixtures.php @@ -9,6 +9,7 @@ namespace App\DataFixtures; +use App\Entity\Customer; use App\Entity\InvoiceTemplate; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; @@ -29,6 +30,8 @@ final class InvoiceFixtures extends Fixture public function load(ObjectManager $manager): void { $faker = Factory::create('en_US'); + /** @var non-empty-array $customers */ + $customers = $manager->getRepository(Customer::class)->findAll(); foreach ($this->getInvoiceConfigs($faker) as $invoiceConfig) { // name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms @@ -38,15 +41,16 @@ final class InvoiceFixtures extends Fixture $template->setRenderer($invoiceConfig[2]); $template->setCalculator($invoiceConfig[3]); $template->setNumberGenerator($invoiceConfig[4]); - $template->setCompany($invoiceConfig[5]); + $template->setCompany($invoiceConfig[5]); // @phpstan-ignore method.deprecated + $template->setCustomer($customers[array_rand($customers)]); $template->setVat($invoiceConfig[6]); $template->setDueDays($invoiceConfig[7]); $template->setPaymentTerms($invoiceConfig[8]); $template->setLanguage('en'); - $template->setAddress($this->generateAddress($faker)); + $template->setAddress($this->generateAddress($faker)); // @phpstan-ignore method.deprecated $template->setContact($this->generateContact($faker)); $template->setPaymentDetails($this->generatePaymentDetails($faker)); - $template->setVatId($faker->creditCardNumber()); + $template->setVatId($faker->creditCardNumber()); // @phpstan-ignore method.deprecated $manager->persist($template); $manager->flush(); @@ -54,8 +58,7 @@ final class InvoiceFixtures extends Fixture } /** - * @param Generator $faker - * @return array + * @return array */ private function getInvoiceConfigs(Generator $faker): array { diff --git a/src/DataFixtures/TeamFixtures.php b/src/DataFixtures/TeamFixtures.php index 03aa1a77..eacd4574 100644 --- a/src/DataFixtures/TeamFixtures.php +++ b/src/DataFixtures/TeamFixtures.php @@ -37,8 +37,7 @@ final class TeamFixtures extends Fixture } /** - * @param ObjectManager $manager - * @return array + * @return non-empty-array */ private function getAllUsers(ObjectManager $manager): array { @@ -46,15 +45,18 @@ final class TeamFixtures extends Fixture /** @var User[] $entries */ $entries = $manager->getRepository(User::class)->findAll(); foreach ($entries as $temp) { - $all[$temp->getId()] = $temp; + $all[(string) $temp->getId()] = $temp; + } + + if (\count($all) === 0) { + throw new \Exception('Need users to setup teams'); } return $all; } /** - * @param ObjectManager $manager - * @return array + * @return non-empty-array */ private function getAllProjects(ObjectManager $manager): array { @@ -63,7 +65,11 @@ final class TeamFixtures extends Fixture /** @var Project[] $entries */ $entries = $manager->getRepository(Project::class)->findAll(); foreach ($entries as $temp) { - $all[$temp->getId()] = $temp; + $all[(string) $temp->getId()] = $temp; + } + + if (\count($all) === 0) { + throw new \Exception('Need projects to setup teams'); } return $all; diff --git a/src/DataFixtures/TimesheetFixtures.php b/src/DataFixtures/TimesheetFixtures.php index cfc1f289..42dbb2dd 100644 --- a/src/DataFixtures/TimesheetFixtures.php +++ b/src/DataFixtures/TimesheetFixtures.php @@ -130,10 +130,9 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface /** * @template T of object - * @param ObjectManager $manager * @param class-string $class * @param int $amount - * @return array + * @return non-empty-array */ private function findRandom(ObjectManager $manager, string $class, int $amount): array { @@ -154,14 +153,17 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface $qb = $manager->getRepository($class)->createQueryBuilder('entity'); - /** @var array $result */ - $result = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult(); + /** @var array $all */ + $all = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult(); - return $result; + if (\count($all) === 0) { + throw new \Exception('Need users to setup teams'); + } + + return $all; } /** - * @param ObjectManager $manager * @return array */ private function getAllTags(ObjectManager $manager): array @@ -170,8 +172,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface } /** - * @param ObjectManager $manager - * @return array + * @return non-empty-array */ private function getAllUsers(ObjectManager $manager): array { @@ -179,15 +180,18 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface /** @var User[] $entries */ $entries = $manager->getRepository(User::class)->findAll(); foreach ($entries as $temp) { - $all[$temp->getId()] = $temp; + $all[(string) $temp->getId()] = $temp; + } + + if (\count($all) === 0) { + throw new \Exception('Need users to setup timesheets'); } return $all; } /** - * @param ObjectManager $manager - * @return array + * @return non-empty-array */ private function getAllProjects(ObjectManager $manager): array { @@ -195,8 +199,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface } /** - * @param ObjectManager $manager - * @return array + * @return non-empty-array */ private function getAllActivities(ObjectManager $manager): array { diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 30c06e45..ec192c52 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -27,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: CustomerRepository::class)] #[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')] #[Serializer\ExclusionPolicy('all')] -#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable'])] +#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'addressLine1', 'addressLine2', 'addressLine3', 'postCode', 'city', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable'])] #[Constraints\Customer] class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt { @@ -86,13 +86,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'vat_id')] private ?string $vatId = null; - #[ORM\Column(name: 'contact', type: 'string', length: 100, nullable: true)] + /** + * Contact person (name) + */ + #[ORM\Column(name: 'contact', type: Types::STRING, length: 100, nullable: true)] #[Assert\Length(max: 100)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'contact')] private ?string $contact = null; - #[ORM\Column(name: 'address', type: 'text', nullable: true)] + /** + * Unstructured address, better use the fields: address_line1-3, postcode, city, country + */ + #[ORM\Column(name: 'address', type: Types::TEXT, nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'address')] @@ -132,7 +138,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt #[Exporter\Expose(label: 'mobile')] private ?string $mobile = null; /** - * Customers contact email + * Contact email */ #[ORM\Column(name: 'email', type: Types::STRING, length: 75, nullable: true)] #[Assert\Length(max: 75)] @@ -190,6 +196,43 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt private ?InvoiceTemplate $invoiceTemplate = null; #[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)] private ?string $invoiceText = null; + #[ORM\Column(name: 'address_line1', type: Types::STRING, length: 150, nullable: true)] + #[Assert\Length(max: 150)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'address_line1')] + private ?string $addressLine1 = null; + #[ORM\Column(name: 'address_line2', type: Types::STRING, length: 150, nullable: true)] + #[Assert\Length(max: 150)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'address_line2')] + private ?string $addressLine2 = null; + #[ORM\Column(name: 'address_line3', type: Types::STRING, length: 150, nullable: true)] + #[Assert\Length(max: 150)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'address_line3')] + private ?string $addressLine3 = null; + #[ORM\Column(name: 'postcode', type: Types::STRING, length: 20, nullable: true)] + #[Assert\Length(max: 20)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'postcode')] + private ?string $postCode = null; + // this should be more than enough to cover 99.99% - https://en.wikipedia.org/wiki/List_of_long_place_names + #[ORM\Column(name: 'city', type: Types::STRING, length: 50, nullable: true)] + #[Assert\Length(max: 50)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'city')] + private ?string $city = null; + #[ORM\Column(name: 'buyer_reference', type: Types::STRING, length: 50, nullable: true)] + #[Assert\Length(max: 50)] + #[Serializer\Expose] + #[Serializer\Groups(['Customer_Entity'])] + #[Exporter\Expose(label: 'buyer_reference')] + private ?string $buyerReference = null; public function __construct(string $name) { @@ -299,6 +342,35 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt return $this->address; } + public function getFormattedAddress(): ?string + { + $address = $this->getAddressLine1(); + if ($this->getAddressLine2() !== null) { + $address .= PHP_EOL; + $address .= $this->getAddressLine2(); + } + if ($this->getAddressLine3() !== null) { + $address .= PHP_EOL; + $address .= $this->getAddressLine3(); + } + if ($this->getPostCode() !== null || $this->getCity() !== null) { + $address .= PHP_EOL; + if ($this->getPostCode() !== null) { + $address .= $this->getPostCode(); + if ($this->getCity() !== null) { + $address .= ' '; + } + } + $address .= $this->getCity() ?? ''; + } + + if ($address !== null && $address !== '') { + return $address; + } + + return $this->getAddress(); + } + public function setCountry(?string $country): void { $this->country = $country; @@ -479,6 +551,66 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt return $this->teams; } + public function getAddressLine1(): ?string + { + return $this->addressLine1; + } + + public function setAddressLine1(?string $addressLine1): void + { + $this->addressLine1 = $addressLine1; + } + + public function getAddressLine2(): ?string + { + return $this->addressLine2; + } + + public function setAddressLine2(?string $addressLine2): void + { + $this->addressLine2 = $addressLine2; + } + + public function getAddressLine3(): ?string + { + return $this->addressLine3; + } + + public function setAddressLine3(?string $addressLine3): void + { + $this->addressLine3 = $addressLine3; + } + + public function getPostCode(): ?string + { + return $this->postCode; + } + + public function setPostCode(?string $postCode): void + { + $this->postCode = $postCode; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(?string $city): void + { + $this->city = $city; + } + + public function getBuyerReference(): ?string + { + return $this->buyerReference; + } + + public function setBuyerReference(?string $buyerReference): void + { + $this->buyerReference = $buyerReference; + } + public function __toString(): string { return $this->getName(); diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 1a24189f..b9fc1d86 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -221,18 +221,12 @@ class Invoice implements EntityWithMetaFields public function setModel(InvoiceModel $model): Invoice { $template = $model->getTemplate(); - if ($template === null) { - throw new \InvalidArgumentException('Missing invoice template'); - } if ($template->getDueDays() === null || $template->getVat() === null) { throw new \InvalidArgumentException('Missing due-days or vat setting'); } $customer = $model->getCustomer(); - if ($customer === null) { - throw new \InvalidArgumentException('Missing invoice customer'); - } $user = $model->getUser(); if ($user === null) { diff --git a/src/Entity/InvoiceTemplate.php b/src/Entity/InvoiceTemplate.php index 7b5f1fe1..4aab7267 100644 --- a/src/Entity/InvoiceTemplate.php +++ b/src/Entity/InvoiceTemplate.php @@ -10,6 +10,8 @@ namespace App\Entity; use App\Repository\InvoiceTemplateRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -20,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: InvoiceTemplateRepository::class)] #[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')] #[UniqueEntity('name')] -class InvoiceTemplate +class InvoiceTemplate implements EntityWithMetaFields { #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Id] @@ -34,9 +36,8 @@ class InvoiceTemplate #[Assert\Length(max: 255)] #[Assert\NotBlank] private ?string $title = null; - #[ORM\Column(name: 'company', type: 'string', length: 255, nullable: false)] + #[ORM\Column(name: 'company', type: Types::STRING, length: 255, nullable: true)] #[Assert\Length(max: 255)] - #[Assert\NotBlank] private ?string $company = null; #[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)] #[Assert\Length(max: 50)] @@ -75,17 +76,32 @@ class InvoiceTemplate #[ORM\Column(name: 'language', type: Types::STRING, length: 6, nullable: false)] #[Assert\NotBlank] private ?string $language = 'en'; + /** + * Customer for this invoice template + */ + #[ORM\ManyToOne(targetEntity: Customer::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Assert\NotNull] + private ?Customer $customer = null; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'template', targetEntity: InvoiceTemplateMeta::class, cascade: ['persist'])] + private Collection $meta; + + public function __construct() + { + $this->meta = new ArrayCollection(); + } public function getId(): ?int { return $this->id; } - public function setName(string $name): InvoiceTemplate + public function setName(string $name): void { $this->name = $name; - - return $this; } public function getName(): ?string @@ -93,6 +109,16 @@ class InvoiceTemplate return $this->name; } + public function getCustomer(): ?Customer + { + return $this->customer; + } + + public function setCustomer(?Customer $customer): void + { + $this->customer = $customer; + } + // ---- trait methods below --- public function getTitle(): ?string @@ -100,23 +126,22 @@ class InvoiceTemplate return $this->title; } - public function setTitle(?string $title): InvoiceTemplate + public function setTitle(?string $title): void { $this->title = $title; - - return $this; } public function getAddress(): ?string { - return $this->address; + return $this->customer?->getFormattedAddress() ?? $this->address; } - public function setAddress(?string $address): InvoiceTemplate + /** + * @deprecated since 2.41 + */ + public function setAddress(?string $address): void { $this->address = $address; - - return $this; } public function getNumberGenerator(): string @@ -124,11 +149,9 @@ class InvoiceTemplate return $this->numberGenerator; } - public function setNumberGenerator(string $numberGenerator): InvoiceTemplate + public function setNumberGenerator(string $numberGenerator): void { $this->numberGenerator = $numberGenerator; - - return $this; } public function getDueDays(): ?int @@ -136,11 +159,9 @@ class InvoiceTemplate return $this->dueDays; } - public function setDueDays(?int $dueDays): InvoiceTemplate + public function setDueDays(?int $dueDays): void { $this->dueDays = $dueDays; - - return $this; } public function getVat(): ?float @@ -148,23 +169,22 @@ class InvoiceTemplate return $this->vat; } - public function setVat(?float $vat): InvoiceTemplate + public function setVat(?float $vat): void { $this->vat = $vat; - - return $this; } public function getCompany(): ?string { - return $this->company; + return $this->customer?->getCompany() ?? $this->customer?->getName() ?? $this->company; } - public function setCompany(?string $company): InvoiceTemplate + /** + * @deprecated since 2.41 + */ + public function setCompany(?string $company): void { $this->company = $company; - - return $this; } public function getRenderer(): string @@ -172,11 +192,9 @@ class InvoiceTemplate return $this->renderer; } - public function setRenderer(string $renderer): InvoiceTemplate + public function setRenderer(string $renderer): void { $this->renderer = $renderer; - - return $this; } public function getCalculator(): string @@ -184,11 +202,9 @@ class InvoiceTemplate return $this->calculator; } - public function setCalculator(string $calculator): InvoiceTemplate + public function setCalculator(string $calculator): void { $this->calculator = $calculator; - - return $this; } public function getPaymentTerms(): ?string @@ -196,23 +212,22 @@ class InvoiceTemplate return $this->paymentTerms; } - public function setPaymentTerms(?string $paymentTerms): InvoiceTemplate + public function setPaymentTerms(?string $paymentTerms): void { $this->paymentTerms = $paymentTerms; - - return $this; } public function getVatId(): ?string { - return $this->vatId; + return $this->customer?->getVatId() ?? $this->vatId; } - public function setVatId(?string $vatId): InvoiceTemplate + /** + * @deprecated since 2.41 + */ + public function setVatId(?string $vatId): void { $this->vatId = $vatId; - - return $this; } public function getContact(): ?string @@ -220,11 +235,9 @@ class InvoiceTemplate return $this->contact; } - public function setContact(?string $contact): InvoiceTemplate + public function setContact(?string $contact): void { $this->contact = $contact; - - return $this; } public function getPaymentDetails(): ?string @@ -232,11 +245,9 @@ class InvoiceTemplate return $this->paymentDetails; } - public function setPaymentDetails(?string $paymentDetails): InvoiceTemplate + public function setPaymentDetails(?string $paymentDetails): void { $this->paymentDetails = $paymentDetails; - - return $this; } /** @@ -252,9 +263,78 @@ class InvoiceTemplate return $this->language; } - public function setLanguage(?string $language): InvoiceTemplate + public function setLanguage(?string $language): void { $this->language = $language; + } + + /** + * @return Tax[] + */ + public function getTaxRates(): array + { + // TODO make me configurable via UI + $tax = new Tax( + TaxType::STANDARD, + 'VAT', + $this->vat ?? 0.00 + ); + + return [$tax]; + } + + /** + * @return Collection|MetaTableTypeInterface[] + */ + public function getMetaFields(): Collection + { + return $this->meta; + } + + /** + * @return MetaTableTypeInterface[] + */ + public function getVisibleMetaFields(): array + { + $all = []; + foreach ($this->meta as $meta) { + if ($meta->isVisible()) { + $all[] = $meta; + } + } + + return $all; + } + + public function getMetaField(string $name): ?MetaTableTypeInterface + { + foreach ($this->meta as $field) { + if ($field->getName() !== null && strtolower($field->getName()) === strtolower($name)) { + return $field; + } + } + + return null; + } + + public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields + { + if ($meta->getName() === null) { + throw new \InvalidArgumentException('Meta-field needs to have a name'); + } + + if (!$meta instanceof InvoiceTemplateMeta) { + throw new \InvalidArgumentException('Meta-field needs to be an instanceof InvoiceTemplateMeta'); + } + + if (null === ($current = $this->getMetaField($meta->getName()))) { + $meta->setEntity($this); + $this->meta->add($meta); + + return $this; + } + + $current->merge($meta); return $this; } diff --git a/src/Entity/InvoiceTemplateMeta.php b/src/Entity/InvoiceTemplateMeta.php new file mode 100644 index 00000000..a1fc627a --- /dev/null +++ b/src/Entity/InvoiceTemplateMeta.php @@ -0,0 +1,49 @@ +template = $entity; + + return $this; + } + + /** + * @return InvoiceTemplate|null + */ + public function getEntity(): ?EntityWithMetaFields + { + return $this->template; + } +} diff --git a/src/Entity/Tax.php b/src/Entity/Tax.php new file mode 100644 index 00000000..1d2ad048 --- /dev/null +++ b/src/Entity/Tax.php @@ -0,0 +1,36 @@ +type; + } + + public function getName(): string + { + return $this->name; + } + + public function getRate(): float + { + return $this->rate; + } +} diff --git a/src/Entity/TaxType.php b/src/Entity/TaxType.php new file mode 100644 index 00000000..46dd5d3e --- /dev/null +++ b/src/Entity/TaxType.php @@ -0,0 +1,17 @@ +entity; + } +} diff --git a/src/Form/CustomerEditForm.php b/src/Form/CustomerEditForm.php index 1e7cb27c..e2b92260 100644 --- a/src/Form/CustomerEditForm.php +++ b/src/Form/CustomerEditForm.php @@ -34,12 +34,14 @@ class CustomerEditForm extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $isNew = false; + $hasAddress = false; if (isset($options['data'])) { /** @var Customer $customer */ $customer = $options['data']; $isNew = $customer->getId() === null; $options['currency'] = $customer->getCurrency(); + $hasAddress = $customer->getAddress() !== null && $customer->getAddress() !== ''; } $builder @@ -69,10 +71,38 @@ class CustomerEditForm extends AbstractType 'label' => 'contact', 'required' => false, ]) - ->add('address', TextareaType::class, [ + ; + + if ($hasAddress) { + $builder + ->add('address', TextareaType::class, [ + 'label' => 'address', + 'help' => 'address_deprecated', + 'required' => false, + ]); + } + + $builder + ->add('address_line1', TextType::class, [ 'label' => 'address', 'required' => false, ]) + ->add('address_line2', TextType::class, [ + 'label' => false, + 'required' => false, + ]) + ->add('address_line3', TextType::class, [ + 'label' => false, + 'required' => false, + ]) + ->add('postcode', TextType::class, [ + 'label' => 'postcode', + 'required' => false, + ]) + ->add('city', TextType::class, [ + 'label' => 'city', + 'required' => false, + ]) ->add('country', CountryType::class, [ 'label' => 'country', ]) @@ -117,6 +147,10 @@ class CustomerEditForm extends AbstractType 'help' => 'help.invoiceTemplate_customer', 'required' => false, ]) + ->add('buyerReference', TextType::class, [ + 'label' => 'buyerReference', + 'required' => false, + ]) ; if ($isNew) { diff --git a/src/Form/InvoiceTemplateForm.php b/src/Form/InvoiceTemplateForm.php index 0fd8e29c..716dc129 100644 --- a/src/Form/InvoiceTemplateForm.php +++ b/src/Form/InvoiceTemplateForm.php @@ -10,10 +10,12 @@ namespace App\Form; use App\Entity\InvoiceTemplate; +use App\Form\Type\CustomerType; use App\Form\Type\InvoiceCalculatorType; use App\Form\Type\InvoiceNumberGeneratorType; use App\Form\Type\InvoiceRendererType; use App\Form\Type\LanguageType; +use App\Form\Type\MetaFieldsCollectionType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\NumberType; @@ -36,17 +38,6 @@ final class InvoiceTemplateForm extends AbstractType ->add('title', TextType::class, [ 'label' => 'title', ]) - ->add('company', TextType::class, [ - 'label' => 'company', - ]) - ->add('vatId', TextType::class, [ - 'label' => 'vat_id', - 'required' => false, - ]) - ->add('address', TextareaType::class, [ - 'label' => 'address', - 'required' => false, - ]) ->add('contact', TextareaType::class, [ 'label' => 'contact', 'required' => false, @@ -70,6 +61,13 @@ final class InvoiceTemplateForm extends AbstractType ->add('calculator', InvoiceCalculatorType::class) ->add('numberGenerator', InvoiceNumberGeneratorType::class) ->add('language', LanguageType::class) + ->add('customer', CustomerType::class, [ + 'required' => true, + 'label' => 'sending_company', + 'placeholder' => '', + 'help' => 'sending_company.help', + ]) + ->add('metaFields', MetaFieldsCollectionType::class) ; } diff --git a/src/Form/MultiUpdate/MultiUpdateTable.php b/src/Form/MultiUpdate/MultiUpdateTable.php index 584ac442..67a08fbe 100644 --- a/src/Form/MultiUpdate/MultiUpdateTable.php +++ b/src/Form/MultiUpdate/MultiUpdateTable.php @@ -9,7 +9,6 @@ namespace App\Form\MultiUpdate; -use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; @@ -42,7 +41,12 @@ final class MultiUpdateTable extends AbstractType return []; } - return $repository->matching((new Criteria())->where(Criteria::expr()->in('id', explode(',', $ids)))); + $temp = explode(',', $ids); + if (\count($temp) === 0) { + return []; + } + + return $repository->findBy(['id' => $temp]); } ) ); diff --git a/src/Form/MultiUpdate/TimesheetMultiUpdate.php b/src/Form/MultiUpdate/TimesheetMultiUpdate.php index 5097d949..44b0d8c9 100644 --- a/src/Form/MultiUpdate/TimesheetMultiUpdate.php +++ b/src/Form/MultiUpdate/TimesheetMultiUpdate.php @@ -21,7 +21,6 @@ use App\Form\Type\UserType; use App\Form\Type\YesNoType; use App\Repository\CustomerRepository; use App\Repository\TimesheetRepository; -use Doctrine\Common\Collections\Criteria; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -253,7 +252,12 @@ final class TimesheetMultiUpdate extends AbstractType return []; } - return $this->timesheet->matching((new Criteria())->where(Criteria::expr()->in('id', explode(',', $ids)))); + $temp = explode(',', $ids); + if (\count($temp) === 0) { + return []; + } + + return $this->timesheet->findBy(['id' => $temp]); } ) ); diff --git a/src/Form/Type/DateRangeType.php b/src/Form/Type/DateRangeType.php index 2cb78edc..104c68d9 100644 --- a/src/Form/Type/DateRangeType.php +++ b/src/Form/Type/DateRangeType.php @@ -147,7 +147,7 @@ final class DateRangeType extends AbstractType } /** - * @param array{'format': non-empty-string, 'separator': non-empty-string, 'allow_empty': true, 'timezone': non-empty-string} $options + * @param array{'format': non-empty-string, 'separator': non-empty-string, 'allow_empty': true, 'timezone': non-empty-string, 'user': User} $options */ public function buildForm(FormBuilderInterface $builder, array $options): void // @phpstan-ignore-line { diff --git a/src/Invoice/Calculator/AbstractCalculator.php b/src/Invoice/Calculator/AbstractCalculator.php index d9484420..bf958a44 100644 --- a/src/Invoice/Calculator/AbstractCalculator.php +++ b/src/Invoice/Calculator/AbstractCalculator.php @@ -11,6 +11,7 @@ namespace App\Invoice\Calculator; use App\Invoice\InvoiceItem; use App\Invoice\InvoiceModel; +use App\Invoice\TaxRow; abstract class AbstractCalculator { @@ -51,21 +52,35 @@ abstract class AbstractCalculator return round($amount, 2); } + /** + * @deprecated use getTaxRows() instead + */ public function getVat(): float { return $this->model->getTemplate()->getVat() ?? 0.00; } - public function getTax(): float + /** + * @return array + */ + public function getTaxRows(): array { - $vat = $this->getVat(); - if (0.00 === $vat) { - return 0.00; + $rows = []; + foreach ($this->model->getTemplate()->getTaxRates() as $taxRate) { + $rows[] = new TaxRow($taxRate, $this->getSubtotal()); } - $percent = $vat / 100.00; + return $rows; + } - return round($this->getSubtotal() * $percent, 2); + public function getTax(): float + { + $tax = 0.00; + foreach ($this->getTaxRows() as $row) { + $tax += $row->getAmount(); + } + + return round($tax, 2); } public function getTotal(): float @@ -75,8 +90,6 @@ abstract class AbstractCalculator /** * Returns the total amount of worked time in seconds. - * - * @return int */ public function getTimeWorked(): int { diff --git a/src/Invoice/CalculatorInterface.php b/src/Invoice/CalculatorInterface.php index bcc94845..34353c04 100644 --- a/src/Invoice/CalculatorInterface.php +++ b/src/Invoice/CalculatorInterface.php @@ -26,39 +26,35 @@ interface CalculatorInterface /** * Set the invoice model and can be used to fetch the customer. - * - * @param InvoiceModel $model */ public function setModel(InvoiceModel $model): void; /** * Returns the subtotal before taxes. - * - * @return float */ public function getSubtotal(): float; /** * Returns the tax amount for this invoice. - * - * @return float */ public function getTax(): float; /** * Returns the total amount for this invoice including taxes. - * - * @return float */ public function getTotal(): float; /** * Returns the percentage for the value-added tax (VAT) calculation. - * - * @return float + * @deprecated use getTaxRows() instead */ public function getVat(): float; + /** + * @return array + */ + public function getTaxRows(): array; + /** * Returns the total amount of worked time in seconds. * @@ -71,8 +67,6 @@ interface CalculatorInterface * * Prefix it with your company name followed by a hyphen (e.g. "acme-"), * if this is a third-party calculator. - * - * @return string */ public function getId(): string; } diff --git a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php index 8c862bfb..00ab8346 100644 --- a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php @@ -12,6 +12,7 @@ namespace App\Invoice\Hydrator; use App\Customer\CustomerStatisticService; use App\Invoice\InvoiceModel; use App\Invoice\InvoiceModelHydrator; +use Symfony\Component\Intl\Countries; final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator { @@ -25,22 +26,26 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator { $customer = $model->getCustomer(); - if (null === $customer) { - return []; - } - $prefix = 'customer.'; + $language = $model->getTemplate()->getLanguage(); + $country = $customer->getCountry(); $values = [ $prefix . 'id' => $customer->getId(), - $prefix . 'address' => $customer->getAddress() ?? '', + $prefix . 'address' => $customer->getFormattedAddress() ?? '', + $prefix . 'address_line1' => $customer->getAddressLine1() ?? '', + $prefix . 'address_line2' => $customer->getAddressLine2() ?? '', + $prefix . 'address_line3' => $customer->getAddressLine3() ?? '', + $prefix . 'postcode' => $customer->getPostCode() ?? '', + $prefix . 'city' => $customer->getCity() ?? '', $prefix . 'name' => $customer->getName() ?? '', $prefix . 'contact' => $customer->getContact() ?? '', $prefix . 'company' => $customer->getCompany() ?? '', $prefix . 'vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15 $prefix . 'vat_id' => $customer->getVatId() ?? '', $prefix . 'number' => $customer->getNumber() ?? '', - $prefix . 'country' => $customer->getCountry(), + $prefix . 'country' => $country, + $prefix . 'country_name' => $country !== null ? Countries::getName($country, $language) : null, $prefix . 'homepage' => $customer->getHomepage() ?? '', $prefix . 'comment' => $customer->getComment() ?? '', $prefix . 'email' => $customer->getEmail() ?? '', @@ -48,6 +53,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator $prefix . 'phone' => $customer->getPhone() ?? '', $prefix . 'mobile' => $customer->getMobile() ?? '', $prefix . 'invoice_text' => $customer->getInvoiceText() ?? '', + $prefix . 'buyer_reference' => $customer->getBuyerReference() ?? '', ]; $end = $model->getQuery()?->getEnd(); diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index cb9ba75f..4dd67ffe 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -11,6 +11,7 @@ namespace App\Invoice\Hydrator; use App\Invoice\InvoiceModel; use App\Invoice\InvoiceModelHydrator; +use Symfony\Component\Intl\Countries; final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator { @@ -19,16 +20,23 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator public function hydrate(InvoiceModel $model): array { $template = $model->getTemplate(); - if ($template === null) { - throw new \InvalidArgumentException('InvoiceModel needs a template'); + $calculator = $model->getCalculator(); + if ($calculator === null) { + throw new \InvalidArgumentException('InvoiceModel needs a calculator'); } $currency = $model->getCurrency(); - $tax = $model->getCalculator()->getTax(); - $total = $model->getCalculator()->getTotal(); - $subtotal = $model->getCalculator()->getSubtotal(); + $tax = $calculator->getTax(); + $total = $calculator->getTotal(); + $subtotal = $calculator->getSubtotal(); $formatter = $model->getFormatter(); $language = $template->getLanguage(); + $taxRows = $calculator->getTaxRows(); + + $vat = 0.00; + foreach ($taxRows as $taxRow) { + $vat += $taxRow->getTax()->getRate(); + } $values = [ 'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()), @@ -39,13 +47,13 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'invoice.currency' => $currency, 'invoice.language' => $language, // since 1.9 'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency), - 'invoice.vat' => $model->getCalculator()->getVat(), + 'invoice.vat' => $vat, // @deprecated, use invoice.tax_rows instead 'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00, - 'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), - 'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), - 'invoice.tax_plain' => $tax, - 'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()), - 'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()), + 'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), // @deprecated, use invoice.tax_rows instead + 'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), // @deprecated, use invoice.tax_rows instead + 'invoice.tax_plain' => $tax, // @deprecated, use invoice.tax_rows instead + 'invoice.total_time' => $formatter->getFormattedDuration($calculator->getTimeWorked()), + 'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($calculator->getTimeWorked()), 'invoice.total' => $formatter->getFormattedMoney($total, $currency), 'invoice.total_nc' => $formatter->getFormattedMoney($total, $currency, false), 'invoice.total_plain' => $total, @@ -82,6 +90,26 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'user.see_others' => ($model->getQuery()?->getUser() === null), ]; + $values['invoice.tax_rows'] = []; + foreach ($taxRows as $taxRow) { + $values['invoice.tax_rows'][] = [ + 'type' => $taxRow->getTax()->getType()->value, + 'name' => $taxRow->getTax()->getName(), + 'rate' => $taxRow->getTax()->getRate(), + 'amount' => $taxRow->getAmount(), // do not format, only available in twig anyway + 'base' => $taxRow->getBasePrice(), // do not format, only available in twig anyway + ]; + } + + $seller = $template->getCustomer(); + if ($seller !== null) { + $country = $seller->getCountry(); + if ($country !== null) { + $values['template.country'] = $country; + $values['template.country_name'] = Countries::getName($country, $language); + } + } + $query = $model->getQuery(); if ($query !== null) { $begin = $query->getBegin(); diff --git a/src/Invoice/InvoiceModel.php b/src/Invoice/InvoiceModel.php index 94108949..3858daa0 100644 --- a/src/Invoice/InvoiceModel.php +++ b/src/Invoice/InvoiceModel.php @@ -23,20 +23,20 @@ use App\Invoice\Hydrator\InvoiceModelProjectHydrator; use App\Invoice\Hydrator\InvoiceModelUserHydrator; use App\Project\ProjectStatisticService; use App\Repository\Query\InvoiceQuery; +use Symfony\Component\DependencyInjection\Attribute\Exclude; /** * InvoiceModel is the ONLY value that a RendererInterface receives for generating the invoice, * besides the InvoiceDocument which is used as a "template". */ +#[Exclude] final class InvoiceModel { - private ?Customer $customer = null; private ?InvoiceQuery $query = null; /** * @var ExportableItem[] */ private array $entries = []; - private ?InvoiceTemplate $template = null; private ?CalculatorInterface $calculator = null; private ?NumberGeneratorInterface $generator = null; private \DateTimeInterface $invoiceDate; @@ -61,7 +61,14 @@ final class InvoiceModel /** * @internal use InvoiceModelFactory */ - public function __construct(InvoiceFormatter $formatter, CustomerStatisticService $customerStatistic, ProjectStatisticService $projectStatistic, ActivityStatisticService $activityStatistic) + public function __construct( + InvoiceFormatter $formatter, + CustomerStatisticService $customerStatistic, + ProjectStatisticService $projectStatistic, + ActivityStatisticService $activityStatistic, + private readonly Customer $customer, + private readonly InvoiceTemplate $template, + ) { $this->invoiceDate = new \DateTimeImmutable(); $this->formatter = $formatter; @@ -138,37 +145,20 @@ final class InvoiceModel return $this; } - public function getTemplate(): ?InvoiceTemplate + public function getTemplate(): InvoiceTemplate { return $this->template; } - public function setTemplate(InvoiceTemplate $template): void - { - $this->template = $template; - } - - public function getCustomer(): ?Customer + public function getCustomer(): Customer { return $this->customer; } - public function setCustomer(Customer $customer): void - { - $this->customer = $customer; - } - - /** - * Requires the template and invoice date to be set - */ public function getDueDate(): \DateTimeInterface { $date = \DateTimeImmutable::createFromInterface($this->getInvoiceDate()); - - $dueDays = 14; - if ($this->getTemplate() !== null) { - $dueDays = $this->getTemplate()->getDueDays(); - } + $dueDays = $this->template->getDueDays(); return $date->add(new \DateInterval('P' . $dueDays . 'D')); } @@ -246,8 +236,8 @@ final class InvoiceModel public function getCurrency(): string { - if (null !== $this->getCustomer() && $this->getCustomer()->getCurrency() !== null) { - return $this->getCustomer()->getCurrency(); + if ($this->customer->getCurrency() !== null) { + return $this->customer->getCurrency(); } return Customer::DEFAULT_CURRENCY; diff --git a/src/Invoice/InvoiceModelFactory.php b/src/Invoice/InvoiceModelFactory.php index e24916ee..c467c663 100644 --- a/src/Invoice/InvoiceModelFactory.php +++ b/src/Invoice/InvoiceModelFactory.php @@ -27,10 +27,8 @@ final class InvoiceModelFactory public function createModel(InvoiceFormatter $formatter, Customer $customer, InvoiceTemplate $template, InvoiceQuery $query): InvoiceModel { - $model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService); + $model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template); - $model->setCustomer($customer); - $model->setTemplate($template); $model->setQuery($query); return $model; diff --git a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php index c3ee8682..2949979e 100644 --- a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php +++ b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php @@ -53,11 +53,11 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface throw new \InvalidArgumentException('Missing invoice model, cannot calculate invoice number'); } - if ($format === 'cname' && $this->model->getCustomer()?->getName() === null) { + if ($format === 'cname' && $this->model->getCustomer()->getName() === null) { throw new \InvalidArgumentException('Customer has no name, replacer {cname} failed evaluation'); } - if ($format === 'cnumber' && $this->model->getCustomer()?->getNumber() === null) { + if ($format === 'cnumber' && $this->model->getCustomer()->getNumber() === null) { throw new \InvalidArgumentException('Customer has no number, replacer {cnumber} failed evaluation'); } @@ -87,8 +87,8 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface 'cy' => $this->repository->getCounterForYear($invoiceDate) + $increaseBy, 'cm' => $this->repository->getCounterForMonth($invoiceDate) + $increaseBy, 'cd' => $this->repository->getCounterForDay($invoiceDate) + $increaseBy, - 'cname' => (string) $this->model->getCustomer()?->getName(), - 'cnumber' => (string) $this->model->getCustomer()?->getNumber(), + 'cname' => (string) $this->model->getCustomer()->getName(), + 'cnumber' => (string) $this->model->getCustomer()->getNumber(), default => $originalFormat, }; }); diff --git a/src/Invoice/Renderer/DocxRenderer.php b/src/Invoice/Renderer/DocxRenderer.php index 0cb72eea..62831015 100644 --- a/src/Invoice/Renderer/DocxRenderer.php +++ b/src/Invoice/Renderer/DocxRenderer.php @@ -29,6 +29,11 @@ final class DocxRenderer extends AbstractRenderer implements RendererInterface $template = new TemplateProcessor($document->getFilename()); foreach ($model->toArray() as $search => $replace) { + if (\is_array($replace)) { + // TODO tax rows + continue; + } + $replace = $xmlEscaper->escape($replace); $replace = preg_replace('/\n|\r\n?/', '', $replace); diff --git a/src/Invoice/TaxRow.php b/src/Invoice/TaxRow.php new file mode 100644 index 00000000..8fca4b2e --- /dev/null +++ b/src/Invoice/TaxRow.php @@ -0,0 +1,39 @@ +tax; + } + + public function getBasePrice(): float + { + return $this->basePrice; + } + + public function getAmount(): float + { + $percent = $this->tax->getRate() / 100.00; + + return round($this->basePrice * $percent, 4); + } +} diff --git a/src/Pdf/MPdfConverter.php b/src/Pdf/MPdfConverter.php index e9b93d92..9419bbec 100644 --- a/src/Pdf/MPdfConverter.php +++ b/src/Pdf/MPdfConverter.php @@ -34,7 +34,7 @@ final class MPdfConverter implements HtmlToPdfConverter $filtered = array_filter($options, function ($key): bool { $allowed = [ 'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top', - 'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', 'associated_files' + 'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', 'associated_files', 'additional_xmp_rdf' ]; if (!\in_array($key, $allowed)) { $configs = new ConfigVariables(); @@ -110,6 +110,12 @@ final class MPdfConverter implements HtmlToPdfConverter unset($options['associated_files']); } + $additionalXmpRdf = null; + if (\array_key_exists('additional_xmp_rdf', $options) && \is_string($options['additional_xmp_rdf'])) { + $additionalXmpRdf = $options['additional_xmp_rdf']; + unset($options['additional_xmp_rdf']); + } + $mpdf = new Mpdf($options); $mpdf->creator = Constants::SOFTWARE; @@ -117,6 +123,10 @@ final class MPdfConverter implements HtmlToPdfConverter $mpdf->SetAssociatedFiles($associatedFiles); } + if ($additionalXmpRdf !== null) { + $mpdf->SetAdditionalXmpRdf($additionalXmpRdf); + } + return $mpdf; } diff --git a/src/Utils/Context.php b/src/Utils/Context.php index aafc54f1..d5844d96 100644 --- a/src/Utils/Context.php +++ b/src/Utils/Context.php @@ -10,10 +10,12 @@ namespace App\Utils; use App\Entity\User; +use Symfony\Component\DependencyInjection\Attribute\Exclude; +#[Exclude] final class Context { - public function __construct(private User $user) + public function __construct(private readonly User $user) { } diff --git a/templates/customer/details.html.twig b/templates/customer/details.html.twig index 34e16467..ec2ffa93 100644 --- a/templates/customer/details.html.twig +++ b/templates/customer/details.html.twig @@ -38,10 +38,31 @@ {% endif %} + {% if customer.addressLine1 is not empty %} + + {{ 'address'|trans }} + + {{ customer.addressLine1 }}
+ {% if customer.addressLine2 is not empty %} + {{ customer.addressLine2 }}
+ {% endif %} + {% if customer.addressLine3 is not empty %} + {{ customer.addressLine3 }}
+ {% endif %} + {{ customer.postCode }} {{ customer.city }} + + + {% endif %} {% if customer.address is not empty %} {{ 'address'|trans }} - {{ customer.address|nl2br }} + + {{ widgets.alert('info', 'address_deprecated'|trans) }} +
+ + {{ customer.address|nl2br }} + + {% endif %} {% if customer.country is not empty %} diff --git a/templates/customer/edit.html.twig b/templates/customer/edit.html.twig index ceeea3bb..9f1844f9 100644 --- a/templates/customer/edit.html.twig +++ b/templates/customer/edit.html.twig @@ -18,28 +18,16 @@ {{ form_row(form.comment) }} - {{ form_row(form.address) }}
-
- {{ form_row(form.company) }} -
{{ form_row(form.number) }}
{{ form_row(form.vatId) }}
-
-
-
- {{ form_row(form.country) }} -
{{ form_row(form.currency) }}
-
- {{ form_row(form.timezone) }} -
@@ -87,21 +75,66 @@
{% endif %} -
-
- {{ form_row(form.visible) }} -
-
- {{ form_row(form.billable) }} -
-
{% if form.metaFields is defined and form.metaFields is not empty %} {{ form_row(form.metaFields) }} {% endif %} + {% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_address_settings'} %} + {% import "macros/widgets.html.twig" as widgets %} + {% block title %}{{ 'address'|trans }}{% endblock %} + {% block body %} +
+
+ {{ form_row(form.company) }} +
+
+ {% if form.address is defined %} +
+
+ {{ form_row(form.address) }} +
+
+ {% endif %} +
+
+ {{ form_row(form.address_line1, {attr: {placeholder: 'address_row'|trans({'%row%': 1})}}) }} +
+
+
+
+ {{ form_row(form.address_line2, {attr: {placeholder: 'address_row'|trans({'%row%': 2})}}) }} +
+
+
+
+ {{ form_row(form.address_line3, {attr: {placeholder: 'address_row'|trans({'%row%': 3})}}) }} +
+
+
+
+ {{ form_row(form.postcode) }} +
+
+ {{ form_row(form.city) }} +
+
+
+
+ {{ form_row(form.country) }} +
+
+ {{ form_row(form.timezone) }} +
+
+ + {% endblock %} + {% endembed %} {% embed '@theme/embeds/collapsible.html.twig' with {id: 'customer_invoice_settings'} %} {% import "macros/widgets.html.twig" as widgets %} {% block title %}{{ 'invoices'|trans }}{% endblock %} {% block body %} + {% if form.buyerReference is defined %} + {{ form_row(form.buyerReference) }} + {% endif %} {% if form.invoiceTemplate is defined %} {{ form_row(form.invoiceTemplate) }} {% endif %} @@ -110,6 +143,14 @@ {% endif %} {% endblock %} {% endembed %} +
+
+ {{ form_row(form.visible) }} +
+
+ {{ form_row(form.billable) }} +
+
{{ form_rest(form) }} {% endblock %} {% endembed %} diff --git a/templates/invoice/renderer/default.pdf.twig b/templates/invoice/renderer/default.pdf.twig index 1cf0433e..0c4dabd1 100644 --- a/templates/invoice/renderer/default.pdf.twig +++ b/templates/invoice/renderer/default.pdf.twig @@ -48,6 +48,10 @@ mpdf--> {{ invoice['customer.company']|default(invoice['customer.name']) }}
{{ invoice['customer.address']|nl2br }} + {% set country = invoice['customer.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['customer.address'] %} +
{{ country }} + {% endif %} {% if invoice['customer.vat_id'] is not empty %}
{{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }} @@ -59,6 +63,12 @@ mpdf--> {{ invoice['template.company'] }}
{{ invoice['template.address']|trim|nl2br }} + {% if invoice['template.country'] is not null %} + {% set country = invoice['template.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['template.address'] %} +
{{ country }} + {% endif %} + {% endif %} {% if invoice['template.vat_id'] is not empty %}
{{ 'vat_id'|trans }}: diff --git a/templates/invoice/renderer/invoice.html.twig b/templates/invoice/renderer/invoice.html.twig index 726f0195..6ae1eb6b 100644 --- a/templates/invoice/renderer/invoice.html.twig +++ b/templates/invoice/renderer/invoice.html.twig @@ -25,6 +25,10 @@
{{ invoice['customer.company']|default(invoice['customer.name']) }}
{{ invoice['customer.address']|nl2br }} + {% set country = invoice['customer.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['customer.address'] %} +
{{ country }} + {% endif %} {% if invoice['customer.vat_id'] is not empty %}
{{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }} @@ -45,6 +49,12 @@
{{ invoice['template.company'] }}
{{ invoice['template.address']|trim|nl2br }} + {% if invoice['template.country'] is not null %} + {% set country = invoice['template.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['template.address'] %} +
{{ country }} + {% endif %} + {% endif %} {% if invoice['template.vat_id'] is not empty %}
{{ 'vat_id'|trans }}: diff --git a/templates/invoice/renderer/service-date.pdf.twig b/templates/invoice/renderer/service-date.pdf.twig index b247b2ab..9fe08718 100644 --- a/templates/invoice/renderer/service-date.pdf.twig +++ b/templates/invoice/renderer/service-date.pdf.twig @@ -14,6 +14,12 @@ {{ invoice['template.company'] }} – {{ invoice['template.address']|nl2str(' – ') }} + {% if invoice['template.country'] is not null %} + {% set country = invoice['template.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['template.address'] %} + – {{ country }} + {% endif %} + {% endif %} {% if invoice['template.vat_id'] is not empty %} – {{ 'vat_id'|trans }}: {{ invoice['template.vat_id'] }} {% endif %} @@ -33,6 +39,12 @@
{{ invoice['template.company'] }}
{{ invoice['template.address']|nl2br }} + {% if invoice['template.country'] is not null %} + {% set country = invoice['template.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['template.address'] %} +
{{ country }} + {% endif %} + {% endif %} {{ 'invoice_bank_account'|trans }} @@ -54,6 +66,10 @@ mpdf--> {{ invoice['customer.company']|default(invoice['customer.name']) }}
{{ invoice['customer.address']|nl2br }} + {% set country = invoice['customer.country']|country_name(invoice['invoice.language']) %} + {% if country not in invoice['customer.address'] %} +
{{ country }} + {% endif %} {% set classLeft = 'text-left' %} diff --git a/templates/invoice/template_edit.html.twig b/templates/invoice/template_edit.html.twig index 4943295c..9e58c7d2 100644 --- a/templates/invoice/template_edit.html.twig +++ b/templates/invoice/template_edit.html.twig @@ -18,25 +18,19 @@
-
- {{ form_row(form.company) }} -
-
- {{ form_row(form.vatId) }} +
+ {{ form_row(form.customer) }}
-
- {{ form_row(form.address) }} +
+ {{ form_row(form.paymentTerms) }}
+
+
{{ form_row(form.contact) }}
-
-
-
- {{ form_row(form.paymentTerms) }} -
{{ form_row(form.paymentDetails) }}
@@ -70,6 +64,15 @@
+ {% if form.metaFields is defined and form.metaFields is not empty %} +
+
+
+ {{ form_row(form.metaFields) }} +
+
+
+ {% endif %} {{ form_rest(form) }} {% endblock %} {% endembed %} diff --git a/templates/project/edit.html.twig b/templates/project/edit.html.twig index f03bce51..aeede91a 100644 --- a/templates/project/edit.html.twig +++ b/templates/project/edit.html.twig @@ -26,14 +26,6 @@ {{ form_row(form.number) }}
-
-
- {{ form_row(form.orderNumber) }} -
-
- {{ form_row(form.orderDate) }} -
-
{{ form_row(form.start) }} @@ -66,6 +58,26 @@
{% endif %} + {% if form.metaFields is defined and form.metaFields is not empty %} + {{ form_row(form.metaFields) }} + {% endif %} + {% embed '@theme/embeds/collapsible.html.twig' with {id: 'project_invoice_settings'} %} + {% import "macros/widgets.html.twig" as widgets %} + {% block title %}{{ 'invoices'|trans }}{% endblock %} + {% block body %} +
+
+ {{ form_row(form.orderNumber) }} +
+
+ {{ form_row(form.orderDate) }} +
+
+ {% if form.invoiceText is defined %} + {{ form_row(form.invoiceText) }} + {% endif %} + {% endblock %} + {% endembed %}
{{ form_row(form.visible) }} @@ -77,18 +89,6 @@ {{ form_row(form.globalActivities) }}
- {% if form.metaFields is defined and form.metaFields is not empty %} - {{ form_row(form.metaFields) }} - {% endif %} - {% embed '@theme/embeds/collapsible.html.twig' with {id: 'project_invoice_settings'} %} - {% import "macros/widgets.html.twig" as widgets %} - {% block title %}{{ 'invoices'|trans }}{% endblock %} - {% block body %} - {% if form.invoiceText is defined %} - {{ form_row(form.invoiceText) }} - {% endif %} - {% endblock %} - {% endembed %} {{ form_rest(form) }} {% endblock %} {% endembed %} diff --git a/tests/API/APIControllerBaseTestCase.php b/tests/API/APIControllerBaseTestCase.php index b730486d..cae77a01 100644 --- a/tests/API/APIControllerBaseTestCase.php +++ b/tests/API/APIControllerBaseTestCase.php @@ -487,6 +487,12 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase 'timeBudget' => 'int', 'vatId' => '@string', // since 1.10 'budgetType' => '@string', // since 1.15 + 'addressLine1' => '@string', // since 2.32 + 'addressLine2' => '@string', // since 2.32 + 'addressLine3' => '@string', // since 2.32 + 'city' => '@string', // since 2.32 + 'postCode' => '@string', // since 2.32 + 'buyerReference' => '@string', // since 2.41 ]; // if a project is embedded diff --git a/tests/Controller/InvoiceControllerTest.php b/tests/Controller/InvoiceControllerTest.php index 40a06502..8c601a6a 100644 --- a/tests/Controller/InvoiceControllerTest.php +++ b/tests/Controller/InvoiceControllerTest.php @@ -102,9 +102,9 @@ class InvoiceControllerTest extends AbstractControllerBaseTestCase $form = $client->getCrawler()->filter('form[name=invoice_template_form]')->form(); $client->submit($form, [ 'invoice_template_form' => [ - 'name' => 'Test', + 'name' => 'FooBar Template', 'title' => 'Test invoice template', - 'company' => 'Company name', + 'customer' => 1, 'renderer' => 'default', 'calculator' => 'default', 'vat' => '27,937', @@ -117,9 +117,9 @@ class InvoiceControllerTest extends AbstractControllerBaseTestCase $this->assertHasFlashSuccess($client); $template = $this->getEntityManager()->getRepository(InvoiceTemplate::class)->findAll()[0]; - self::assertEquals('Test', $template->getName()); + self::assertEquals('FooBar Template', $template->getName()); self::assertEquals('Test invoice template', $template->getTitle()); - self::assertEquals('Company name', $template->getCompany()); + self::assertEquals('Test', $template->getCompany()); self::assertEquals('default', $template->getRenderer()); self::assertEquals('default', $template->getCalculator()); self::assertEquals('27.937', $template->getVat()); @@ -145,8 +145,6 @@ class InvoiceControllerTest extends AbstractControllerBaseTestCase self::assertEquals($template->getCalculator(), $values['calculator']); self::assertEquals($template->getVat(), $values['vat']); self::assertEquals($template->getRenderer(), $values['renderer']); - self::assertEquals($template->getCompany(), $values['company']); - self::assertEquals($template->getAddress(), $values['address']); self::assertEquals($template->getPaymentTerms(), $values['paymentTerms']); } @@ -408,7 +406,7 @@ class InvoiceControllerTest extends AbstractControllerBaseTestCase 'invoice_template_form' => [ 'name' => 'Test 2!', 'title' => 'Test invoice template', - 'company' => 'Company name', + 'customer' => 1, 'renderer' => 'default', 'calculator' => 'default', ] diff --git a/tests/DataFixtures/InvoiceTemplateFixtures.php b/tests/DataFixtures/InvoiceTemplateFixtures.php index ff260a66..bb768218 100644 --- a/tests/DataFixtures/InvoiceTemplateFixtures.php +++ b/tests/DataFixtures/InvoiceTemplateFixtures.php @@ -9,6 +9,7 @@ namespace App\Tests\DataFixtures; +use App\Entity\Customer; use App\Entity\InvoiceTemplate; use Doctrine\Persistence\ObjectManager; use Faker\Factory; @@ -27,10 +28,21 @@ class InvoiceTemplateFixtures implements TestFixture $faker = Factory::create(); + $customer = new Customer($faker->company()); + $customer->setCountry($faker->countryCode()); + $customer->setTimezone($faker->timezone()); + $customer->setCompany('Company name'); + $customer->setAddressLine1($faker->streetAddress()); + $customer->setAddressLine2($faker->streetAddress()); + $customer->setPostCode($faker->postcode()); + $customer->setCity($faker->city()); + + $manager->persist($customer); + $template = new InvoiceTemplate(); $template->setName('Invoice'); $template->setTitle('Your company name'); - $template->setCompany($faker->company()); + $template->setCustomer($customer); $template->setVat(19); $template->setDueDays(14); $template->setPaymentTerms( @@ -39,9 +51,7 @@ class InvoiceTemplateFixtures implements TestFixture 'Please transfer the total amount within 14 days to the given account and use the invoice number ' . 'as reference.' ); - $template->setAddress( - $faker->streetAddress() . PHP_EOL . - $faker->city() . ' ' . $faker->postcode() . ', ' . $faker->country() . PHP_EOL . + $template->setContact( 'Phone: ' . $faker->phoneNumber() . PHP_EOL . 'Email: ' . $faker->safeEmail() ); diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php index 892bb96d..d8bf43c8 100644 --- a/tests/Entity/CustomerTest.php +++ b/tests/Entity/CustomerTest.php @@ -35,6 +35,13 @@ class CustomerTest extends AbstractEntityTestCase self::assertNull($sut->getVatId()); self::assertNull($sut->getContact()); self::assertNull($sut->getAddress()); + self::assertNull($sut->getAddressLine1()); + self::assertNull($sut->getAddressLine2()); + self::assertNull($sut->getAddressLine3()); + self::assertNull($sut->getFormattedAddress()); + self::assertNull($sut->getCity()); + self::assertNull($sut->getPostCode()); + self::assertNull($sut->getBuyerReference()); self::assertNull($sut->getCountry()); self::assertEquals('EUR', $sut->getCurrency()); self::assertEquals('EUR', Customer::DEFAULT_CURRENCY); @@ -137,6 +144,51 @@ class CustomerTest extends AbstractEntityTestCase $sut->setCurrency(null); self::assertNull($sut->getCurrency()); + + $sut->setBuyerReference('BR-876876876876'); + self::assertEquals('BR-876876876876', $sut->getBuyerReference()); + + $sut->setBuyerReference(null); + self::assertNull($sut->getBuyerReference()); + + $sut->setAddressLine1('address line 1'); + $sut->setAddressLine2('address line 2'); + $sut->setAddressLine3('address line 3'); + $sut->setCity('looney toon'); + $sut->setPostcode('zip 12345'); + $sut->setAddress('foo bar +sdfsadf +sdfarwt34 +786876 uitiutziuz'); + + self::assertEquals('address line 1', $sut->getAddressLine1()); + self::assertEquals('address line 2', $sut->getAddressLine2()); + self::assertEquals('address line 3', $sut->getAddressLine3()); + self::assertEquals('looney toon', $sut->getCity()); + self::assertEquals('zip 12345', $sut->getPostCode()); + self::assertEquals('foo bar +sdfsadf +sdfarwt34 +786876 uitiutziuz', $sut->getAddress()); + self::assertEquals('address line 1 +address line 2 +address line 3 +zip 12345 looney toon', $sut->getFormattedAddress()); + + $sut->setAddressLine1(null); + $sut->setAddressLine2(null); + $sut->setAddressLine3(null); + $sut->setCity(null); + $sut->setPostcode(null); + $sut->setAddress(null); + + self::assertNull($sut->getAddress()); + self::assertNull($sut->getAddressLine1()); + self::assertNull($sut->getAddressLine2()); + self::assertNull($sut->getAddressLine3()); + self::assertNull($sut->getFormattedAddress()); + self::assertNull($sut->getCity()); + self::assertNull($sut->getPostCode()); } public function testMetaFields(): void @@ -212,6 +264,11 @@ class CustomerTest extends AbstractEntityTestCase ['mobile', 'string'], ['fax', 'string'], ['homepage', 'string'], + ['address_line1', 'string'], + ['address_line2', 'string'], + ['address_line3', 'string'], + ['postcode', 'string'], + ['city', 'string'], ['country', 'string'], ['currency', 'string'], ['timezone', 'string'], @@ -222,6 +279,7 @@ class CustomerTest extends AbstractEntityTestCase ['visible', 'boolean'], ['comment', 'string'], ['billable', 'boolean'], + ['buyer_reference', 'string'], ]; self::assertCount(\count($expected), $columns); diff --git a/tests/Entity/InvoiceTemplateMetaTest.php b/tests/Entity/InvoiceTemplateMetaTest.php new file mode 100644 index 00000000..4898566c --- /dev/null +++ b/tests/Entity/InvoiceTemplateMetaTest.php @@ -0,0 +1,40 @@ +expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected instanceof InvoiceTemplate, received "App\Entity\Timesheet"'); + + $sut = new InvoiceTemplateMeta(); + $sut->setEntity(new Timesheet()); + } +} diff --git a/tests/Entity/InvoiceTemplateTest.php b/tests/Entity/InvoiceTemplateTest.php index 7130bd67..02fffb29 100644 --- a/tests/Entity/InvoiceTemplateTest.php +++ b/tests/Entity/InvoiceTemplateTest.php @@ -9,7 +9,11 @@ namespace App\Tests\Entity; +use App\Entity\Customer; +use App\Entity\CustomerMeta; use App\Entity\InvoiceTemplate; +use App\Entity\InvoiceTemplateMeta; +use Doctrine\Common\Collections\Collection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -35,59 +39,118 @@ class InvoiceTemplateTest extends TestCase self::assertEquals('default', $sut->getRenderer()); self::assertEquals(30, $sut->getDueDays()); self::assertTrue($sut->isDecimalDuration()); // @phpstan-ignore method.deprecated - } - - public function testSetNullForOptionalValues(): void - { - $sut = new InvoiceTemplate(); - - self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentDetails(null)); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setVatId(null)); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setContact(null)); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setAddress(null)); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentTerms(null)); + self::assertInstanceOf(Collection::class, $sut->getMetaFields()); + self::assertEquals(0, $sut->getMetaFields()->count()); + self::assertNull($sut->getMetaField('foo')); } public function testSetterAndGetter(): void { $sut = new InvoiceTemplate(); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentTerms('foo bar')); + $sut->setPaymentTerms('foo bar'); self::assertEquals('foo bar', $sut->getPaymentTerms()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setPaymentDetails('iuasdzgf isdfhvlksdjfbnvl ksdfbglisbdf')); + $sut->setPaymentDetails('iuasdzgf isdfhvlksdjfbnvl ksdfbglisbdf'); self::assertEquals('iuasdzgf isdfhvlksdjfbnvl ksdfbglisbdf', $sut->getPaymentDetails()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setContact('hello world')); + $sut->setContact('hello world'); self::assertEquals('hello world', $sut->getContact()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setVat(7.31)); + $sut->setVat(7.31); self::assertEquals(7.31, $sut->getVat()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setVatId('1234567890')); + $sut->setVatId('1234567890'); // @phpstan-ignore method.deprecated self::assertEquals('1234567890', $sut->getVatId()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setLanguage('de')); + $sut->setLanguage('de'); self::assertEquals('de', $sut->getLanguage()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setNumberGenerator('foo')); + $sut->setNumberGenerator('foo'); self::assertEquals('foo', $sut->getNumberGenerator()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setRenderer('bar')); + $sut->setRenderer('bar'); self::assertEquals('bar', $sut->getRenderer()); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setCalculator('fooBar')); + $sut->setCalculator('fooBar'); self::assertEquals('fooBar', $sut->getCalculator()); + $sut->setCompany('looney toon'); // @phpstan-ignore method.deprecated + self::assertEquals('looney toon', $sut->getCompany()); + + $sut->setAddress('acme street, 1234 looney town, rainbow'); // @phpstan-ignore method.deprecated + self::assertEquals('acme street, 1234 looney town, rainbow', $sut->getAddress()); + self::assertEquals($sut, clone $sut); + + $customer = new Customer('foo'); + $customer->setVatId('0987654321'); + $customer->setCompany('bar'); + $customer->setAddressLine1('elmstreet'); + $customer->setAddressLine2('2nd floor'); + $customer->setPostCode('4711'); + $customer->setCity('Over the rainbow'); + $sut->setCustomer($customer); + + self::assertEquals('elmstreet +2nd floor +4711 Over the rainbow', $sut->getAddress()); + self::assertEquals('bar', $sut->getCompany()); + self::assertEquals('0987654321', $sut->getVatId()); } public function testToString(): void { $sut = new InvoiceTemplate(); - self::assertInstanceOf(InvoiceTemplate::class, $sut->setName('a template name')); + $sut->setName('a template name'); self::assertEquals('a template name', $sut->__toString()); self::assertEquals('a template name', (string) $sut); } + + public function testMetaFields(): void + { + $sut = new InvoiceTemplate(); + $meta = new InvoiceTemplateMeta(); + $meta->setName('foo')->setValue('bar2')->setType('test'); + self::assertInstanceOf(InvoiceTemplate::class, $sut->setMetaField($meta)); + self::assertEquals(1, $sut->getMetaFields()->count()); + $result = $sut->getMetaField('foo'); + self::assertSame($result, $meta); + self::assertEquals('test', $result->getType()); + self::assertEquals('bar2', $result->getValue()); + + $meta2 = new InvoiceTemplateMeta(); + $meta2->setName('foo')->setValue('bar')->setType('test2'); + self::assertInstanceOf(InvoiceTemplate::class, $sut->setMetaField($meta2)); + self::assertEquals(1, $sut->getMetaFields()->count()); + self::assertCount(0, $sut->getVisibleMetaFields()); + + $result = $sut->getMetaField('foo'); + self::assertSame($result, $meta); + self::assertEquals('test2', $result->getType()); + + $sut->setMetaField((new InvoiceTemplateMeta())->setName('blub')->setIsVisible(true)); + $sut->setMetaField((new InvoiceTemplateMeta())->setName('blab')->setIsVisible(true)); + self::assertEquals(3, $sut->getMetaFields()->count()); + self::assertCount(2, $sut->getVisibleMetaFields()); + } + + public function testThrowsOnMetaFieldWithoutName(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Meta-field needs to have a name'); + $sut = new InvoiceTemplate(); + $sut->setMetaField(new CustomerMeta()); + } + + public function testThrowsOnMetaFieldsWrongType(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Meta-field needs to be an instanceof InvoiceTemplateMeta'); + $sut = new InvoiceTemplate(); + $meta = new CustomerMeta(); + $meta->setName('foo'); + $sut->setMetaField($meta); + } } diff --git a/tests/Event/InvoiceTemplateMetaDefinitionEventTest.php b/tests/Event/InvoiceTemplateMetaDefinitionEventTest.php new file mode 100644 index 00000000..535f2904 --- /dev/null +++ b/tests/Event/InvoiceTemplateMetaDefinitionEventTest.php @@ -0,0 +1,26 @@ +getEntity()); + } +} diff --git a/tests/Export/Spreadsheet/CustomerExporterTest.php b/tests/Export/Spreadsheet/CustomerExporterTest.php index 0667a5e2..42e8a780 100644 --- a/tests/Export/Spreadsheet/CustomerExporterTest.php +++ b/tests/Export/Spreadsheet/CustomerExporterTest.php @@ -52,6 +52,13 @@ class CustomerExporterTest extends TestCase $customer->setMetaField((new CustomerMeta())->setName('foo meta')->setValue('some magic')->setIsVisible(true)); $customer->setMetaField((new CustomerMeta())->setName('hidden meta')->setValue('will not be seen')->setIsVisible(false)); $customer->setMetaField((new CustomerMeta())->setName('bar meta')->setValue('is happening')->setIsVisible(true)); + $customer->setCountry('AT'); + $customer->setAddressLine1('1234 Main Road'); + $customer->setAddressLine2('Third Floor'); + $customer->setAddressLine3('Golden Office Tower'); + $customer->setCity('Acme City'); + $customer->setPostCode('MT 543.6'); + $customer->setBuyerReference('BR-0987654321'); $sut = new EntityWithMetaFieldsExporter($spreadsheetExporter, $annotationExtractor, $metaFieldExtractor); $spreadsheet = $sut->export(Customer::class, [$customer], new CustomerMetaDisplayEvent(new CustomerQuery(), CustomerMetaDisplayEvent::EXPORT)); @@ -71,7 +78,12 @@ class CustomerExporterTest extends TestCase self::assertEquals(null, $worksheet->getCell([++$i, 2])->getValue()); // mobile self::assertEquals(null, $worksheet->getCell([++$i, 2])->getValue()); // fax self::assertEquals(null, $worksheet->getCell([++$i, 2])->getValue()); // homepage - self::assertEquals(null, $worksheet->getCell([++$i, 2])->getValue()); // country + self::assertEquals('1234 Main Road', $worksheet->getCell([++$i, 2])->getValue()); // address_line1 + self::assertEquals('Third Floor', $worksheet->getCell([++$i, 2])->getValue()); // address_line2 + self::assertEquals('Golden Office Tower', $worksheet->getCell([++$i, 2])->getValue()); // address_line3 + self::assertEquals('MT 543.6', $worksheet->getCell([++$i, 2])->getValue()); // postcode + self::assertEquals('Acme City', $worksheet->getCell([++$i, 2])->getValue()); // city + self::assertEquals('AT', $worksheet->getCell([++$i, 2])->getValue()); // country self::assertEquals('EUR', $worksheet->getCell([++$i, 2])->getValue()); // currency self::assertEquals(null, $worksheet->getCell([++$i, 2])->getValue()); // timezone self::assertEquals('123456.789', $worksheet->getCell([++$i, 2])->getValue()); // budget @@ -81,6 +93,7 @@ class CustomerExporterTest extends TestCase self::assertFalse($worksheet->getCell([++$i, 2])->getValue()); // visible self::assertEquals('Lorem Ipsum', $worksheet->getCell([++$i, 2])->getValue()); // comment self::assertTrue($worksheet->getCell([++$i, 2])->getValue()); // billable + self::assertEquals('BR-0987654321', $worksheet->getCell([++$i, 2])->getValue()); // buyer reference self::assertEquals('some magic', $worksheet->getCell([++$i, 2])->getValue()); self::assertEquals('is happening', $worksheet->getCell([++$i, 2])->getValue()); } diff --git a/tests/Invoice/Calculator/AbstractCalculatorTestCase.php b/tests/Invoice/Calculator/AbstractCalculatorTestCase.php index e17bf503..a02966a8 100644 --- a/tests/Invoice/Calculator/AbstractCalculatorTestCase.php +++ b/tests/Invoice/Calculator/AbstractCalculatorTestCase.php @@ -17,6 +17,7 @@ use App\Entity\Timesheet; use App\Entity\User; use App\Invoice\CalculatorInterface; use App\Invoice\InvoiceModel; +use App\Invoice\TaxRow; use App\Repository\Query\InvoiceQuery; use App\Tests\Invoice\DebugFormatter; use App\Tests\Mocks\InvoiceModelFactoryFactory; @@ -43,11 +44,21 @@ abstract class AbstractCalculatorTestCase extends TestCase $sut->setModel($model); self::assertEquals(0, $sut->getTotal()); - self::assertEquals(0, $sut->getVat()); self::assertEquals(0, $sut->getSubtotal()); self::assertEquals(0, $sut->getTimeWorked()); self::assertEquals(0, \count($sut->getEntries())); self::assertEquals(0, $sut->getTax()); + self::assertTax($sut, 0); + } + + protected function assertTax(CalculatorInterface $sut, int $rate): void + { + self::assertEquals($rate, $sut->getVat()); // @phpstan-ignore method.deprecated + $rows = $sut->getTaxRows(); + self::assertCount(1, $rows); + + self::assertInstanceOf(TaxRow::class, $rows[0]); + self::assertEquals($rate, $rows[0]->getTax()->getRate()); } private function getEmptyModel(): InvoiceModel diff --git a/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php index 65413aad..fd9f97cd 100644 --- a/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php @@ -138,7 +138,7 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('activity', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/ActivityUserInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ActivityUserInvoiceCalculatorTest.php index 12b16b7c..005a1c2a 100644 --- a/tests/Invoice/Calculator/ActivityUserInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ActivityUserInvoiceCalculatorTest.php @@ -150,7 +150,7 @@ class ActivityUserInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('activity_user', $sut->getId()); self::assertEquals(3100.09, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2605.12, $sut->getSubtotal()); self::assertEquals(7000, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/DateInvoiceCalculatorTest.php b/tests/Invoice/Calculator/DateInvoiceCalculatorTest.php index 3e9769ff..8112103b 100644 --- a/tests/Invoice/Calculator/DateInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/DateInvoiceCalculatorTest.php @@ -114,7 +114,7 @@ class DateInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('date', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/DateUserInvoiceCalculatorTest.php b/tests/Invoice/Calculator/DateUserInvoiceCalculatorTest.php index e8f3bf47..e4b7d660 100644 --- a/tests/Invoice/Calculator/DateUserInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/DateUserInvoiceCalculatorTest.php @@ -116,7 +116,7 @@ class DateUserInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('date_user', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/DefaultCalculatorTest.php b/tests/Invoice/Calculator/DefaultCalculatorTest.php index f5c1ff0e..5142e291 100644 --- a/tests/Invoice/Calculator/DefaultCalculatorTest.php +++ b/tests/Invoice/Calculator/DefaultCalculatorTest.php @@ -72,7 +72,7 @@ class DefaultCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('default', $sut->getId()); self::assertEquals(581.17, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(488.38, $sut->getSubtotal()); self::assertEquals(5800, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php b/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php index 721f996b..aef5ccd6 100644 --- a/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/PriceInvoiceCalculatorTest.php @@ -117,7 +117,7 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('price', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/ProjectInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ProjectInvoiceCalculatorTest.php index 419c0d85..aa654f56 100644 --- a/tests/Invoice/Calculator/ProjectInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ProjectInvoiceCalculatorTest.php @@ -113,7 +113,7 @@ class ProjectInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('project', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/ProjectUserInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ProjectUserInvoiceCalculatorTest.php index b40bff2b..fe88b783 100644 --- a/tests/Invoice/Calculator/ProjectUserInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ProjectUserInvoiceCalculatorTest.php @@ -116,7 +116,7 @@ class ProjectUserInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('project_user', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php index 76079d2c..c3a3f388 100644 --- a/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/ShortInvoiceCalculatorTest.php @@ -95,7 +95,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('short', $sut->getId()); self::assertEquals(562.28, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(472.5, $sut->getSubtotal()); self::assertEquals(5800, $sut->getTimeWorked()); @@ -171,7 +171,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('short', $sut->getId()); self::assertEquals(581.17, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(488.38, $sut->getSubtotal()); self::assertEquals(5800, $sut->getTimeWorked()); @@ -241,7 +241,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('short', $sut->getId()); self::assertEquals(581.17, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(488.38, $sut->getSubtotal()); self::assertEquals(5800, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/UserInvoiceCalculatorTest.php b/tests/Invoice/Calculator/UserInvoiceCalculatorTest.php index 847147af..176d7e3b 100644 --- a/tests/Invoice/Calculator/UserInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/UserInvoiceCalculatorTest.php @@ -110,7 +110,7 @@ class UserInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('user', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Calculator/WeeklyInvoiceCalculatorTest.php b/tests/Invoice/Calculator/WeeklyInvoiceCalculatorTest.php index bbd0dc8a..81d28523 100644 --- a/tests/Invoice/Calculator/WeeklyInvoiceCalculatorTest.php +++ b/tests/Invoice/Calculator/WeeklyInvoiceCalculatorTest.php @@ -116,7 +116,7 @@ class WeeklyInvoiceCalculatorTest extends AbstractCalculatorTestCase self::assertEquals('weekly', $sut->getId()); self::assertEquals(3000.13, $sut->getTotal()); - self::assertEquals(19, $sut->getVat()); + $this->assertTax($sut, 19); self::assertEquals('EUR', $model->getCurrency()); self::assertEquals(2521.12, $sut->getSubtotal()); self::assertEquals(6600, $sut->getTimeWorked()); diff --git a/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php index 6fd0068d..b233781c 100644 --- a/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php @@ -36,6 +36,12 @@ class InvoiceModelCustomerHydratorTest extends TestCase self::assertEquals([ 'customer.id' => null, 'customer.address' => "Foo\nStreet\n1111 City", + 'customer.address_line1' => '', + 'customer.address_line2' => '', + 'customer.address_line3' => '', + 'customer.buyer_reference' => '', + 'customer.city' => '', + 'customer.postcode' => '', 'customer.name' => 'customer,with/special#name', 'customer.contact' => '', 'customer.company' => '', @@ -43,6 +49,7 @@ class InvoiceModelCustomerHydratorTest extends TestCase 'customer.vat_id' => '', 'customer.number' => '', 'customer.country' => 'AT', + 'customer.country_name' => 'Austria', 'customer.homepage' => '', 'customer.comment' => '', 'customer.email' => '', @@ -63,12 +70,19 @@ class InvoiceModelCustomerHydratorTest extends TestCase $keys = [ 'customer.id', 'customer.address', + 'customer.address_line1', + 'customer.address_line2', + 'customer.address_line3', + 'customer.buyer_reference', + 'customer.city', + 'customer.postcode', 'customer.name', 'customer.contact', 'customer.company', 'customer.vat', 'customer.vat_id', 'customer.country', + 'customer.country_name', 'customer.number', 'customer.homepage', 'customer.comment', diff --git a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php index d29c3596..96752657 100644 --- a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php @@ -33,12 +33,12 @@ class InvoiceModelDefaultHydratorTest extends TestCase public function testHydrateThrowsOnMissing(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('InvoiceModel needs a template'); + $this->expectExceptionMessage('InvoiceModel needs a calculator'); $model = $this->getInvoiceModel(); $obj = new ReflectionObject($model); - $prop = $obj->getProperty('template'); + $prop = $obj->getProperty('calculator'); $prop->setAccessible(true); $prop->setValue($model, null); @@ -63,6 +63,7 @@ class InvoiceModelDefaultHydratorTest extends TestCase 'invoice.tax_hide', 'invoice.tax_nc', 'invoice.tax_plain', + 'invoice.tax_rows', 'invoice.total_time', 'invoice.duration_decimal', 'invoice.first', diff --git a/tests/Invoice/InvoiceModelTest.php b/tests/Invoice/InvoiceModelTest.php index 124d2fd3..b3dd3be7 100644 --- a/tests/Invoice/InvoiceModelTest.php +++ b/tests/Invoice/InvoiceModelTest.php @@ -28,8 +28,6 @@ class InvoiceModelTest extends TestCase $sut = (new InvoiceModelFactoryFactory($this))->create()->createModel($formatter, new Customer('foo'), new InvoiceTemplate(), new InvoiceQuery()); self::assertNotNull($sut->getQuery()); - self::assertNotNull($sut->getCustomer()); - self::assertNotNull($sut->getTemplate()); self::assertFalse($sut->isPreview()); self::assertFalse($sut->isHideZeroTax()); diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index bc2ae2db..e40fc38e 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -96,6 +96,7 @@ class DebugRendererTest extends TestCase 'invoice.language', 'invoice.tax_nc', 'invoice.tax_plain', + 'invoice.tax_rows', 'invoice.total_time', 'invoice.duration_decimal', 'invoice.first', @@ -137,12 +138,19 @@ class DebugRendererTest extends TestCase 'query.end_year', 'customer.id', 'customer.address', + 'customer.address_line1', + 'customer.address_line2', + 'customer.address_line3', + 'customer.buyer_reference', + 'customer.city', + 'customer.postcode', 'customer.name', 'customer.contact', 'customer.company', 'customer.vat', 'customer.vat_id', 'customer.country', + 'customer.country_name', 'customer.number', 'customer.homepage', 'customer.comment', diff --git a/tests/Invoice/Renderer/RendererTestTrait.php b/tests/Invoice/Renderer/RendererTestTrait.php index ddc4d766..fb8f79e9 100644 --- a/tests/Invoice/Renderer/RendererTestTrait.php +++ b/tests/Invoice/Renderer/RendererTestTrait.php @@ -95,6 +95,7 @@ trait RendererTestTrait $template->setTitle('a very *long* test invoice / template title with [ßpecial] chäracter'); $template->setVat(19); $template->setLanguage('en'); + $template->setCustomer($customer); $pMeta = new ProjectMeta(); $pMeta->setName('foo-project')->setValue('bar-project')->setIsVisible(true); @@ -279,6 +280,7 @@ trait RendererTestTrait $template->setTitle('a test invoice template title'); $template->setVat(19); $template->setLanguage('it'); + $template->setCustomer($customer); $project = new Project(); $project->setName('project name'); diff --git a/tests/Invoice/Renderer/TwigRendererTest.php b/tests/Invoice/Renderer/TwigRendererTest.php index 8b9de829..66eaac73 100644 --- a/tests/Invoice/Renderer/TwigRendererTest.php +++ b/tests/Invoice/Renderer/TwigRendererTest.php @@ -59,7 +59,7 @@ class TwigRendererTest extends KernelTestCase $sut = new TwigRenderer($twig); $model = $this->getInvoiceModel(); - $model->getTemplate()?->setLanguage('de'); + $model->getTemplate()->setLanguage('de'); $document = $this->getInvoiceDocument('timesheet.html.twig'); $response = $sut->render($document, $model); @@ -118,7 +118,7 @@ class TwigRendererTest extends KernelTestCase $sut = new TwigRenderer($twig); $model = $this->getInvoiceModel(); - $model->getTemplate()?->setLanguage('de'); + $model->getTemplate()->setLanguage('de'); foreach ($files as $filename) { $document = new InvoiceDocument(new \SplFileInfo($filename)); diff --git a/tests/Pdf/MPdfConverterTest.php b/tests/Pdf/MPdfConverterTest.php index fde01588..b5a55c78 100644 --- a/tests/Pdf/MPdfConverterTest.php +++ b/tests/Pdf/MPdfConverterTest.php @@ -19,14 +19,20 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; #[Group('integration')] class MPdfConverterTest extends KernelTestCase { - public function test() + public function testConvertToPdf(): void { $kernel = self::bootKernel(); $cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir'); + $options = [ + 'foo' => 'bar', + 'additional_xmp_rdf' => '', + 'margin_top' => 2, + ]; + $sut = new MPdfConverter((new FileHelperFactory($this))->create(), $cacheDir); - $result = $sut->convertToPdf('

Test

'); - // Yeah, thats not a real test, I know ;-) + $result = $sut->convertToPdf('

Test

', $options); + // Yeah, that's not a real test, I know ;-) self::assertNotEmpty($result); preg_match('/\/Creator \((.*)\)/', $result, $matches); self::assertCount(2, $matches); diff --git a/tests/Utils/JavascriptFormatConverterTest.php b/tests/Utils/JavascriptFormatConverterTest.php index d29e333b..1b3e0c37 100644 --- a/tests/Utils/JavascriptFormatConverterTest.php +++ b/tests/Utils/JavascriptFormatConverterTest.php @@ -16,7 +16,7 @@ use PHPUnit\Framework\TestCase; #[CoversClass(JavascriptFormatConverter::class)] class JavascriptFormatConverterTest extends TestCase { - public function test() + public function testConvert(): void { $sut = new JavascriptFormatConverter(); self::assertEquals('DD.MM.YYYY HH:mm', $sut->convert('dd.MM.y HH:mm')); diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 776b0d87..1b13e776 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1721,11 +1721,6 @@ parameters: count: 1 path: Invoice/Renderer/XlsxRendererTest.php - - - message: "#^Cannot call method getLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" - count: 1 - path: Invoice/ServiceInvoiceTest.php - - message: "#^Cannot call method getTotal\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#" count: 4 @@ -1855,11 +1850,6 @@ parameters: count: 1 path: Model/Statistic/MonthTest.php - - - message: "#^Method App\\\\Tests\\\\Pdf\\\\MPdfConverterTest\\:\\:test\\(\\) has no return type specified\\.$#" - count: 1 - path: Pdf/MPdfConverterTest.php - - message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" count: 1 @@ -2685,11 +2675,6 @@ parameters: count: 1 path: Utils/FormFormatConverterTest.php - - - message: "#^Method App\\\\Tests\\\\Utils\\\\JavascriptFormatConverterTest\\:\\:test\\(\\) has no return type specified\\.$#" - count: 1 - path: Utils/JavascriptFormatConverterTest.php - - message: "#^Method App\\\\Tests\\\\Utils\\\\ProfileManagerTest\\:\\:getCookieProfiles\\(\\) has no return type specified\\.$#" count: 1 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index f7cb47bb..f454bb41 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1952,7 +1952,11 @@ sending_company - Rechnungsabsender + Absender der Rechnung + + + sending_company.help + Dieses Unternehmen wird auf der Rechnung als Aussteller angegeben. Der Name, die Adresse und die Steuerinformationen werden verwendet. Customer portal @@ -1986,6 +1990,58 @@ font Schriftart + + address_row + Zeile %row% + + + buyerReference + Bestellreferenz + + + payment_type + Zahlart + + + SEPA Credit Transfer + SEPA-Überweisung + + + SEPA Direct Debit + SEPA-Lastschrift + + + Cash + Bar + + + Cheque + Check + + + Credit Transfer + Überweisung + + + Payment To Bank Account + Zahlung auf Bankkonto + + + Card Payment + Kartenzahlung + + + Direct Debit + Lastschrift + + + payment_account_name + Konto- oder Karteninhaber + + + payment_account_name + IBAN oder Kartennummer + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 62d69949..01df16e9 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1952,7 +1952,11 @@ sending_company - Invoice Sender + Invoice issuer + + + sending_company.help + This company is shown as the issuer on the invoice. The name, address and tax information will be used. Customer portal @@ -1986,6 +1990,58 @@ font Font + + address_row + Line %row% + + + buyerReference + Buyer reference + + + payment_type + Payment type + + + SEPA Credit Transfer + SEPA Credit Transfer + + + SEPA Direct Debit + SEPA Direct Debit + + + Cash + Cash + + + Cheque + Cheque + + + Credit Transfer + Credit Transfer + + + Payment To Bank Account + Payment To Bank Account + + + Card Payment + Card Payment + + + Direct Debit + Direct Debit + + + payment_account_name + Account or card holder + + + payment_account_name + IBAN or card number +